web-dev-qa-db-fra.com

Erreur: impossible de rattacher ActivatedRouteSnapshot créé à partir d'un autre itinéraire

J'essaie d'implémenter la classe RouteReuseStrategy. Cela fonctionne bien lorsque je navigue vers les chemins de niveau supérieur.

Dès qu'un chemin a des chemins enfants et que je navigue dans ce chemin, puis que je retourne à un chemin de niveau supérieur, le message d'erreur suivant s'affiche:

Erreur: Non capturé (promis): Erreur: Impossible de rattacher ActivatedRouteSnapshot créé à partir d'un autre itinéraire.

J'ai créé un plunker pour démontrer l'erreur. Je vois que le plunker ne fonctionne pas dans IE 11, veuillez l’afficher dans la dernière version de Chrome.

Étapes à suivre pour reproduire l'erreur:

Étape 1: enter image description here

Étape 2 enter image description here

Étape 3 enter image description here

Étape 4 enter image description here

Vous pouvez voir l’erreur dans la console:  enter image description here

J'ai essayé l'implémentation trouvée sur cet article article

export class CustomReuseStrategy implements RouteReuseStrategy {

    handlers: {[key: string]: DetachedRouteHandle} = {};

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        console.debug('CustomReuseStrategy:shouldDetach', route);
        return true;
    }

    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        console.debug('CustomReuseStrategy:store', route, handle);
        this.handlers[route.routeConfig.path] = handle;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.debug('CustomReuseStrategy:shouldAttach', route);
        return !!route.routeConfig && !!this.handlers[route.routeConfig.path];
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        console.debug('CustomReuseStrategy:retrieve', route);
        if (!route.routeConfig) return null;
        return this.handlers[route.routeConfig.path];
    }

    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        console.debug('CustomReuseStrategy:shouldReuseRoute', future, curr);
        return future.routeConfig === curr.routeConfig;
    }

}

Et la mise en oeuvre de ce stackoverflow answer

/**
 * reuse-strategy.ts
 * by corbfon 1/6/17
 */

import { ActivatedRouteSnapshot, RouteReuseStrategy, DetachedRouteHandle } from '@angular/router';

/** Interface for object which can store both: 
 * An ActivatedRouteSnapshot, which is useful for determining whether or not you should attach a route (see this.shouldAttach)
 * A DetachedRouteHandle, which is offered up by this.retrieve, in the case that you do want to attach the stored route
 */
interface RouteStorageObject {
    snapshot: ActivatedRouteSnapshot;
    handle: DetachedRouteHandle;
}

export class CustomReuseStrategy implements RouteReuseStrategy {

    /** 
     * Object which will store RouteStorageObjects indexed by keys
     * The keys will all be a path (as in route.routeConfig.path)
     * This allows us to see if we've got a route stored for the requested path
     */
    storedRoutes: { [key: string]: RouteStorageObject } = {};

    /** 
     * Decides when the route should be stored
     * If the route should be stored, I believe the boolean is indicating to a controller whether or not to fire this.store
     * _When_ it is called though does not particularly matter, just know that this determines whether or not we store the route
     * An idea of what to do here: check the route.routeConfig.path to see if it is a path you would like to store
     * @param route This is, at least as I understand it, the route that the user is currently on, and we would like to know if we want to store it
     * @returns boolean indicating that we want to (true) or do not want to (false) store that route
     */
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        let detach: boolean = true;
        console.log("detaching", route, "return: ", detach);
        return detach;
    }

    /**
     * Constructs object of type `RouteStorageObject` to store, and then stores it for later attachment
     * @param route This is stored for later comparison to requested routes, see `this.shouldAttach`
     * @param handle Later to be retrieved by this.retrieve, and offered up to whatever controller is using this class
     */
    store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
        let storedRoute: RouteStorageObject = {
            snapshot: route,
            handle: handle
        };

        console.log("store:", storedRoute, "into: ", this.storedRoutes);
        // routes are stored by path - the key is the path name, and the handle is stored under it so that you can only ever have one object stored for a single path
        this.storedRoutes[route.routeConfig.path] = storedRoute;
    }

    /**
     * Determines whether or not there is a stored route and, if there is, whether or not it should be rendered in place of requested route
     * @param route The route the user requested
     * @returns boolean indicating whether or not to render the stored route
     */
    shouldAttach(route: ActivatedRouteSnapshot): boolean {

        // this will be true if the route has been stored before
        let canAttach: boolean = !!route.routeConfig && !!this.storedRoutes[route.routeConfig.path];

        // this decides whether the route already stored should be rendered in place of the requested route, and is the return value
        // at this point we already know that the paths match because the storedResults key is the route.routeConfig.path
        // so, if the route.params and route.queryParams also match, then we should reuse the component
        if (canAttach) {
            let willAttach: boolean = true;
            console.log("param comparison:");
            console.log(this.compareObjects(route.params, this.storedRoutes[route.routeConfig.path].snapshot.params));
            console.log("query param comparison");
            console.log(this.compareObjects(route.queryParams, this.storedRoutes[route.routeConfig.path].snapshot.queryParams));

            let paramsMatch: boolean = this.compareObjects(route.params, this.storedRoutes[route.routeConfig.path].snapshot.params);
            let queryParamsMatch: boolean = this.compareObjects(route.queryParams, this.storedRoutes[route.routeConfig.path].snapshot.queryParams);

            console.log("deciding to attach...", route, "does it match?", this.storedRoutes[route.routeConfig.path].snapshot, "return: ", paramsMatch && queryParamsMatch);
            return paramsMatch && queryParamsMatch;
        } else {
            return false;
        }
    }

    /** 
     * Finds the locally stored instance of the requested route, if it exists, and returns it
     * @param route New route the user has requested
     * @returns DetachedRouteHandle object which can be used to render the component
     */
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {

        // return null if the path does not have a routerConfig OR if there is no stored route for that routerConfig
        if (!route.routeConfig || !this.storedRoutes[route.routeConfig.path]) return null;
        console.log("retrieving", "return: ", this.storedRoutes[route.routeConfig.path]);

        /** returns handle when the route.routeConfig.path is already stored */
        return this.storedRoutes[route.routeConfig.path].handle;
    }

    /** 
     * Determines whether or not the current route should be reused
     * @param future The route the user is going to, as triggered by the router
     * @param curr The route the user is currently on
     * @returns boolean basically indicating true if the user intends to leave the current route
     */
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        console.log("deciding to reuse", "future", future.routeConfig, "current", curr.routeConfig, "return: ", future.routeConfig === curr.routeConfig);
        return future.routeConfig === curr.routeConfig;
    }

    /** 
     * This nasty bugger finds out whether the objects are _traditionally_ equal to each other, like you might assume someone else would have put this function in Vanilla JS already
     * One thing to note is that it uses coercive comparison (==) on properties which both objects have, not strict comparison (===)
     * @param base The base object which you would like to compare another object to
     * @param compare The object to compare to base
     * @returns boolean indicating whether or not the objects have all the same properties and those properties are ==
     */
    private compareObjects(base: any, compare: any): boolean {

        // loop through all properties in base object
        for (let baseProperty in base) {

            // determine if comparrison object has that property, if not: return false
            if (compare.hasOwnProperty(baseProperty)) {
                switch (typeof base[baseProperty]) {
                    // if one is object and other is not: return false
                    // if they are both objects, recursively call this comparison function
                    case 'object':
                        if (typeof compare[baseProperty] !== 'object' || !this.compareObjects(base[baseProperty], compare[baseProperty])) { return false; } break;
                    // if one is function and other is not: return false
                    // if both are functions, compare function.toString() results
                    case 'function':
                        if (typeof compare[baseProperty] !== 'function' || base[baseProperty].toString() !== compare[baseProperty].toString()) { return false; } break;
                    // otherwise, see if they are equal using coercive comparison
                    default:
                        if (base[baseProperty] != compare[baseProperty]) { return false; }
                }
            } else {
                return false;
            }
        }

        // returns true only after false HAS NOT BEEN returned through all loops
        return true;
    }
}

RouteReuseStrategy est-il prêt pour l'enfant paths? Ou existe-t-il un autre moyen d'utiliser RouteReuseStrategy avec des chemins contenant l'enfant paths

19
Tjaart van der Walt

Voici un moyen de générer des clés uniques pour les itinéraires dans la classe de stratégie ..__J'ai eu un problème similaire, mais une fois que j'ai commencé à générer des clés uniques, le problème a disparu:

private takeFullUrl(route: ActivatedRouteSnapshot) {
  let next = route;
  // Since navigation is usually relative
  // we go down to find out the child to be shown.
  while (next.firstChild) {
    next = next.firstChild;
  }
  const segments = [];
  // Then build a unique key-path by going to the root.
  while (next) {
    segments.Push(next.url.join('/'));
    next = next.parent;
  }
  return compact(segments.reverse()).join('/');
}

Plus d'informations à ce sujet https://github.com/angular/angular/issues/13869#issuecomment-344403045

2
barbatus

J'ai ajouté une solution de contournement pour ne jamais extraire d'itinéraires détachés sur un itinéraire avec loadChildren en modifiant ma fonction d'extraction dans la RouteReuseStrategy personnalisée.

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
       if (!route.routeConfig) return null;
       if(route.routeConfig.loadChildren) return null;
       return this.handlers[route.routeConfig.path];
    }

Je ne suis pas sûr que ce soit une solution parfaite pour tous les scénarios, mais dans mon cas, cela fonctionne.

2

Le routeur angulaire est inutilement complexe et les stratégies personnalisées poursuivent cette tendance.

Votre stratégie personnalisée utilise route.routerConfig.path en tant que clé pour les itinéraires stockés.

Il stocke (écrase) deux itinéraires différents pour le même chemin person/:id

  1. /person/%23123456789%23/edit
  2. /person/%23123456789%23/view

La première fois que l'itinéraire de vue a été stocké, la deuxième fois modifiée, lorsque vous ouvrez à nouveau la vue, le dernier itinéraire enregistré est modifié, mais la vue est attendue. 

Selon l’opinion du routeur, ces routes ne sont pas compatibles. Elle vérifie les nœuds de manière récursive et découvre que routerConfig pour ViewPersonComponent n’est pas identique à routerConfig pour EditPersonComponent, boom! 

Donc, soit routerConfig.path ne doit pas être utilisé comme clé, soit c'est un problème/une limitation de conception de routeur.

1
kemsky

J'ai rencontré un problème similaire et la modification de ma méthode de clé unique l'a résolu.

private routeToUrl(route: ActivatedRouteSnapshot): string {
    if (route.url) {
        if (route.url.length) {
            return route.url.join('/');
        } else {
            if (typeof route.component === 'function') {
                return `[${route.component.name}]`;
            } else if (typeof route.component === 'string') {
                return `[${route.component}]`;
            } else {
                return `[null]`;
            }
        }
    } else {
        return '(null)';
    }
}


private getChildRouteKeys(route:ActivatedRouteSnapshot): string {
    let  url = this.routeToUrl(route);
    return route.children.reduce((fin, cr) => fin += this.getChildRouteKeys(cr), url);
}

private getRouteKey(route: ActivatedRouteSnapshot) {
    let url = route.pathFromRoot.map(it => this.routeToUrl(it)).join('/') + '*';
    url += route.children.map(cr => this.getChildRouteKeys(cr));
    return url;
}

Auparavant, je ne construisais que le premier enfant, maintenant je construis récursivement ma clé sur tous les enfants. Je n'ai pas écrit la fonction routeToUrl, je l'ai obtenue d'un article que j'ai lu sur les stratégies de réutilisation personnalisées il y a quelque temps et qui n'a pas été modifié.

0
bryan60

Dans mon cas, je dois vérifier la route.routeConfig.children également dans la méthode de récupération

retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
   if (!route.routeConfig) return null;
   if (route.routeConfig.loadChildren || route.routeConfig.children ) return null;
   return this.handlers[route.routeConfig.path];
}
0
Husam Zidan