Router named outlets that are activated once
Asked Answered
C

4

10

Is it possible to have router named outlets that are activated once and then never destroyed, no matter what route is navigated in primary outlet?

The intention is to have components that persist on page (for instance, sidebar) but get the benefits of routing on their initial load - such as guards (resolvers) and lazy loading.

The requirement is that named outlets shouldn't affect UX in any negative way, for example by introducing garbage suffixes to SPA URL, e.g. (outletName:routeName), they also shouldn't be accidentally deactivated. If there's a way to detach them from router after initial activation, it would be appropriate.

skipLocationChange option cannot be used for this purpose. In this example /login(popup:compose) URL appears when Contact and Login routes are sequentially navigated.

Caravansary answered 16/2, 2018 at 23:17 Comment(6)
you can create secondary outletsDashtilut
@pixelbits Yes, that's the idea. But how exactly they should be defined? I believe the question is specific enough to not be considered 'too broad' and has limited amount of possible quality answers. I didn't provide any code because it would be generic in this case. In this example from the guide it can be seen that secondary popup outlet persists when primary outlets are navigated, but it pollutes URL with (popup:compose).Caravansary
When you navigate, specify skipLocationChange: trueDashtilut
@pixelbits But this will work only for single navigation on secondary outlet itself. See stackblitz.com/edit/angular-kfuscp . When Contact link is clicked, url isn't changed, but (popup:compose) appears when Login is clicked. Covered here #43644280Caravansary
I gave it a try, hoping that the component could be moved to another container after being activated (see this stackblitz). It looked as if it may work at first, but it doesn't: the bindings are lost in the process. Once the feature requested in issue 20824 is in place, that could be a solution for you.Successive
@ConnorsFan Thanks for your research, I wasn't aware of this issue. Consider providing this as an answer if you wish. Of course, I'd like to have this problem solved, but if it's not currently feasible, a negative answer is an answer too.Caravansary
B
4

Router needs information about named outlets, so there are good chances that implementing your own UrlSerializer will help.

Idea is simple, deserialization process should be aware of routes which have static named outlets and produce UrlTree which contains named outlets i.e. for /login url should produce the same UrlTree as default serializer will produce for url /login(popup:compose). During serialization static named outlet parameters should not be included into resulting url.

Baku answered 28/5, 2018 at 15:42 Comment(1)
That's an excellent idea, thanks. I'm not sure if there's a better way to reconstruct url tree than I did, but it worked for me.Caravansary
L
4

We encountered the same (crucial) UX requirement in our project and came up with a semi-clean but so far fully functional solution.

Implement a custom LocationStrategy, we simply extend the default PathLocationStrategy class and preprocess the URL (that will be presented to the user / browser):

@Injectable()
export class OnlyPrimaryLocationStrategy extends PathLocationStrategy implements LocationStrategy {
  static readonly AUX_ROUTE_SEPERATOR = '//';

  replaceState(state: any, title: string, url: string, queryParams: string): void {
    super.replaceState(state, title, this.preprocessUrl(url), queryParams);
  }

  pushState(state: any, title: string, url: string, queryParams: string): void {
    super.pushState(state, title, this.preprocessUrl(url), queryParams);
  }

  preprocessUrl(url: string): string {
    if (url.includes(OnlyPrimaryLocationStrategy.AUX_ROUTE_SEPERATOR)) {
      if (url.split(OnlyPrimaryLocationStrategy.AUX_ROUTE_SEPERATOR).length > 2) {
        throw new Error(
          'Usage for more than one auxiliary route on the same level detected - please recheck imlementation'
        );
      }
      return url.split(OnlyPrimaryLocationStrategy.AUX_ROUTE_SEPERATOR)[0].replace('(', '');
    } else {
      return url;
    }
  }
}

Do not forget to provide it in your module:

providers: [
    {
     // ...
      provide: LocationStrategy,
      useClass: OnlyPrimaryLocationStrategy,
    },
  ],

String processing obviously is not a 100% clean, but it gets the job done for us - maybe it helps you. Be aware that your URL is now not fully capable of reconstructing your router state (obviously).

Lovett answered 18/11, 2019 at 13:23 Comment(0)
S
3

It appears that we cannot use a secondary route that targets a named outlet without having the secondary route appended to the URL. As suggested in your question, one possible solution would be to detach the component from the router outlet once it has been activated. My attempt to implement that solution is shown in this stackblitz:

<router-outlet name="popup" #popupRouterOutlet (activate)="processActivate($event)"></router-outlet>
<ng-container #popupContainer></ng-container>
export class AppComponent {

  @ViewChild("popupRouterOutlet", { read: ViewContainerRef }) private popupRouterOutlet: ViewContainerRef;
  @ViewChild("popupContainer", { read: ViewContainerRef }) private popupContainer: ViewContainerRef;

  constructor(public router: Router) {
  }

  processActivate(e) {
      let viewRef = this.popupRouterOutlet.detach(0);
      this.popupContainer.insert(viewRef);
      this.router.navigate([".", { outlets: { popup: null } }]);
  }
}

In the activate event handler, the component is detached from the router outlet, it is inserted in an ng-container, and the router outlet is cleared. The component can then stay in the DOM, without using the secondary route anymore.

The static content of the component is tranferred successfully but, unfortunately, the bindings are not. That problem has already been reported. A request has been made on Angular Github in issue 20824, to allow moving a component from one container to another. Until that feature request is implemented, that kind of transfer doesn't seem to be possible.

Successive answered 28/5, 2018 at 1:58 Comment(0)
C
2

skipLocationChange navigation option works only for a router it was supplied for, then named outlet appears in the url, like /login(foo:bar).

It's possible to have permanent foo router outlet by overriding UrlSerializer, as was suggested by @kemsky:

import {
  UrlSerializer, DefaultUrlSerializer, UrlSegmentGroup, UrlTree
} from '@angular/router';

export class FooUrlSerializer extends DefaultUrlSerializer {
  serialize(tree) {
    const { foo, ...noFooChildren } = tree.root.children;
    const root = new UrlSegmentGroup(tree.root.segments, noFooChildren);
    const noFooTree = Object.assign(new UrlTree(), tree, { root });

    return super.serialize(noFooTree);
  }
}

...
providers: [{ provide: UrlSerializer, useClass: FooUrlSerializer }, ...]
...
Caravansary answered 28/5, 2018 at 17:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.