Skip to content

Load a component in a route depending on an asynchronous condition #12088

Closed

Description

@alexjoverm

I'm submitting a ... (check one with "x")

Current behavior
Doesn't seem possible to do it in an easy way without not use framework core stuff

Expected behavior
To be able to determine by an asynchronous condition (a condition that will be resolved later) which component to load in a route.

What is the motivation / use case for changing the behavior?
I'd say the following is a very common behavior expected on any medium size app:

Let's say you have a dashboard (just after you login). You wanna keep 2 different components UserDashboard and AdminDashboard (for not filling your template code with *ngIf), but that condition cannot be resolved immediately because the role comes from an API, so then you cannot "right away" conditionally pass one component or another to a specific route.

This doesn't seem to have an answer that implies not to compile and load a component by yourself.
Please, check out http://stackoverflow.com/questions/39688929/angular-2-how-to-conditionally-load-a-component-in-a-route-asynchronously

  • Angular version: 2.0.1

Activity

  1. DzmitryShylovich commented on Oct 5, 2016

    @DzmitryShylovich
    Contributor

    Use CanActivate guard and redirect to an appropriate page based on user role.

  2. alexjoverm commented on Oct 5, 2016

    @alexjoverm
    Author

    But, doing that means you're redirecting to another route (let's say like "user-dashobard" or "admin-dashboard", while I wanted to keep them under "/dashboard".

    Maybe it's just not the way it should be done? In that case, what are the drawbacks or the approach?

  3. pkozlowski-opensource commented on Oct 5, 2016

    @pkozlowski-opensource
    Member

    Maybe it's just not the way it should be done? In that case, what are the drawbacks or the approach?

    IMO it would be super-confusing to have 2 completely different pages under the same URL. Imagine that someone who is an admin copies the /dashboard URL and sends it over to someone else that isn't. The other person would be able to access the page but would see completely different content from the intended one!

  4. DzmitryShylovich commented on Oct 5, 2016

    @DzmitryShylovich
    Contributor

    But, doing that means you're redirecting to another route (let's say like "user-dashobard" or "admin-dashboard", while I wanted to keep them under "/dashboard".

    I know it's a limitation but there're a lot of real bugs in the router and I don't think requested feature will be implemented in the nearest future.

  5. alexjoverm commented on Oct 5, 2016

    @alexjoverm
    Author

    @pkozlowski-opensource didn't see it in that way, but it makes sense.

  6. amartone commented on Oct 7, 2016

    @amartone

    I've wanted this feature for some time, as well!

  7. bencameron commented on Oct 11, 2016

    @bencameron

    I'm in need of this feature too.

    For a few different applications we've written in Angular 1.x, we have a route that takes in an itemId as a parameter. The item is then looked up by Id, and then the template/controller is selected based on the itemType. We accomplished this in 1.x using ui-router's templateProvider and controllerProvider.

    We will soon be upgrading these applications and need to figure out how to accomplish the equivalent in Angular 2. Is there a better way to do this type of thing?

  8. HorusGoul commented on May 14, 2017

    @HorusGoul

    Any update about this?

  9. prog110 commented on Jun 20, 2017

    @prog110

    @vsavkin Is there any chance of this being implemented sometime soon?

  10. Misiu commented on Sep 20, 2017

    @Misiu

    @bencameron I have exact same use case. Did You solve this?
    Is this feature planned? Guards return true/false, maybe something similar that could return component?

  11. bencameron commented on Sep 20, 2017

    @bencameron

    @Misiu we ended up just handling the dynamic selection of the component within another component that we route to. For example, instead of routing to FooComponent or BarComponent based on some condition that we determine from the route params, we're routing to a component that we use as a container for the other components -- let's call it ContainerComponent.

    Then, within the template for ContainerComponent, we're using a div with an ngSwitch directive on it to choose FooComponent or BarComponent based on the condition.

  12. Misiu commented on Sep 21, 2017

    @Misiu

    @bencameron than's for reply. For now I'll stick with same solution.
    @DzmitryShylovich any chances this could be added in near future?

  13. 51 remaining items

  14. rohittidke commented on Jul 1, 2021

    @rohittidke

    facing a similiar scenario, other than "not pretty" workarounds I don't see any other solution to the problems.
    my scenario -> based on the selection company -> the routing tree differs slightly, and the same url can re-direct to two different components.

    It would be really cool if the Angular team comes up with a feature to solve such use-cases.

  15. rohittidke commented on Jul 8, 2021

    @rohittidke

    I found an intermediate solution if it helps anyone and it works very well and is performant.

    This is how the route looks like

    { path: 'account', data: { breadcrumb: 'My Account' }, children: [ { path: 'my-account-status', component: InterfaceIdBasedRouting, data: { breadcrumb: 'My Account Status', keys: { 60: MyAccountStatusComponent, 64: OtherMyAccountStatusComponent } } },

    The InterfaceIdBasedRouting looks like this

    `@Component({
    selector: 'app-conditional-router',
    templateUrl: './linkid-router.component.html'
    })
    export class InterfaceIdBasedRouting implements AfterViewInit, OnInit {

    @ViewChild(PlaceholderDirective) placeholder: PlaceholderDirective;

    componentInitialized$ = new BehaviorSubject(false);

    constructor(private ComponentFactoryResolver: ComponentFactoryResolver, private someAsyncServiceYouDependUpon: SomeAsyncServiceYouDependUpon, private routerEventService: RouterEventService, private activatedRoute: ActivatedRoute) { }

    ngAfterViewInit(): void {

    combineLatest([this.routerEventService.currentUrl$, this.someAsyncServiceYouDependUpon.someAsyncResult$, this.componentInitialized$])
      .pipe(
        map(([a, b, c]) => ({ url: a, someAsyncResult: b, initialized: c })),
        switchMap(e => {
          if (e.initialized) {
            const returnNumberInHashTable= e.someAsyncResult.returnNumberInHashTable;
            const routeData = this.activatedRoute.snapshot.data;
            if (returnNumberInHashTable) {
              if (returnNumberInHashTable in routeData.keys) {
                return of(returnNumberInHashTable); // this structure is required to run this component only once (later you see takeWhile)
              }
            }
          }
          return of(undefined);
        }),
        takeWhile(linkId => linkId !== undefined)
      ).subscribe(linkId => this.createComponent(linkId));
    

    }

    ngOnInit(): void {
    this.componentInitialized$.next(true);
    }

    createComponent(linkId: number) {
    this.placeholder.ViewContainerRef.clear();
    const routeData = this.activatedRoute.snapshot.data;
    const componentFactory = this.ComponentFactoryResolver.resolveComponentFactory(routeData.keys[linkId]);
    const componentRef = this.placeholder.ViewContainerRef.createComponent(componentFactory);
    }
    }`

    The placeholder directive

    `import { Directive, ViewContainerRef } from '@angular/core';

    @directive({
    selector: '[appPlaceholder]'
    })
    export class PlaceholderDirective {
    constructor(public ViewContainerRef: ViewContainerRef) {

    }
    }`

  16. ArtemGrachov commented on Aug 29, 2021

    @ArtemGrachov

    After hours of search, I have found this article: https://www.linkedin.com/pulse/how-make-route-load-different-components-based-async-veloso-alves/ with very simple and clear solution.

    UPD: the example above is good, but it does not work when you need update routes dynamically. So here is my solution.

    The logic of this module is following: for authorized users the subscriptions page must be shown, for guests the news page must be shown.
    For this, we need possibility to check some condition on each route visit. The condition must be based on some dynamically changable value - in other words, we need to somehow inject a service.

    First goal might be achieved with matcher, second - with useFactory for providers.

    import { NgModule } from '@angular/core';
    import { RouterModule, ROUTES, Routes, UrlMatchResult, UrlSegment } from '@angular/router';
    
    import { AuthService } from 'src/app/services/auth.service';
    import { AuthObserverService } from './service/auth-observer.service';
    
    function routesFactory(authService: AuthService): Routes {
        return [
            {
                loadChildren: () => import('../news/news.module').then(m => m.NewsModule),
                matcher: (url: UrlSegment[]): UrlMatchResult | null => {
                    if (url.length !== 0) {
                        return null;
                    }
    
                    if (authService.isAuthorized) {
                        return null;
                    }
    
                    return { consumed: url };
                }
            },
            {
                loadChildren: () => import('../subscriptions/subscriptions.module').then(m => m.SubscriptionsModule),
                matcher: (url: UrlSegment[]): UrlMatchResult | null => {
                    if (url.length !== 0) {
                        return null;
                    }
    
                    if (authService.isAuthorized) {
                        return { consumed: url };
                    }
    
                    return null;
                }
            }
        ]
    }
    
    @NgModule({
        exports: [RouterModule],
        providers: [
            {
                provide: ROUTES,
                useFactory: routesFactory,
                multi: true,
                deps: [AuthService, AuthObserverService]
            },
            AuthObserverService
        ]
    })
    export class HomeRoutingModule { }
    
    

    Using provider factory allows to inject AuthService needed for conditionally selecting the component we need. With custom url matcher it is possible to re-check isAuthorized flag each time when the route is opened - unlike checking in the mentioned article, which is executed only once.

    However, on logging out the route will stay the same. This problem is solved with AuthObserverService.

    import { Injectable } from '@angular/core';
    import { Router } from '@angular/router';
    import { Subscription } from 'rxjs';
    import { skip } from 'rxjs/operators';
    
    import { AuthService } from 'src/app/services/auth.service';
    
    @Injectable()
    export class AuthObserverService {
        constructor(
            private authService: AuthService,
            private router: Router
        ) {}
    
        private authChangeSubscription: Subscription = this
            .authService
            .isAuthorized$
            .pipe(skip(1))
            .subscribe(() => {
                if (this.router.url !== '/') {
                    return;
                }
    
                this.router.navigated = false;
                this.router.navigate([this.router.url]);
            });
    
        public ngOnDestroy(): void {
            this.authChangeSubscription.unsubscribe();
        }
    }
    

    The AuthObserverService subscribes to isAuthorized$ observable (skip needed as in my app the isAuthorized$ is a BehaviorSubject). When the authorization state is updated, the route is reloaded too.
    if (this.router.url !== '/') { needed to not to trigger page reloading when any other route is active.

  17. added a commit that references this issue on Jun 10, 2022
    a99727f
  18. angular-automatic-lock-bot commented on Jul 14, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  19. locked and limited conversation to collaborators on Jul 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: routerfeatureLabel used to distinguish feature request from other issuesfeature: under considerationFeature request for which voting has completed and the request is now under considerationfreq2: medium

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions