lordchancellor
lordchancellor

Reputation: 4105

Conditional rendering of components in Angular child router outlet

I am trying to find a way to conditionally render components in the router-outlet of a child route, based on the user's role.

For example, I have a DashboardComponent, which contains a router-outlet. I want the component that is rendered in child router-outlet to be different dependent on the user role, which is passed in by the token, without having to specify additional routes.

I am hoping to try and write my Routes somewhere close to this:

{ 
    path: 'dashboard', 
    component: DashboardComponent,
    children: [
        { path: '', component: DataViewsComponent }, // For standard user role
        { path: '', component: AdminViewsComponent } // For the admin user role
    ]
}

This exact pattern doesn't work of course, because the router just sees two identical route paths.

I have managed to write a service that uses the canActivate property to check the user's role and forward on to a different route dependant on the role:

@Injectable()
export class RoleRouteRedirectService implements CanActivate {

    roleRoutingPermissions: any = {
        // Will contain redirects for every route in the app
        // For this example, only contains reroutes for the dashboard route
        ['dashboard']: [
            { roles: [ 'user' ], redirect: '/dashboard/admin' },
            { roles: [ 'admin' ], redirect: '/dashboard/admin' }

        ]
    };

    constructor(private router: Router) {}

    canActivate(route: ActivatedRouteSnapshot): boolean {
        // In the larger app, the roles are taken out of the token
        // Here I've mocked them from localStorage for ease
        const userRoles: string[] = localStorage.getItem('user-role').split(',');
        const url: string = route.data.url;

        // Find the object property matching the desired origin URL
        if (url in this.roleRoutingPermissions) {
            // Iterate over the array of route permissions in this property
            for (const item of roleRoutingPermissions[url]) {
                // If we find the entry for the role, apply the redirect
                if (item.roles.some(role => userRoles.includes(role))) {
                    this.router.navigate([ item.redirect ]);

                    return false;
                }
            }
        }

        // Otherwise, fail
        console.log('Could not find matching route permissions');
        return false;

    }

}

This would go with a different Routes pattern:

{ 
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [ RoleRouteRedirectService ],
    data: {
        url: 'dashboard'
    },
    // Conditional Routes
    children: [
        {
            path: 'user',
            component: DataViewsComponent,
        },
        {
            path: 'admin',
            component: AdminHomeComponent,
        }
    ]
}

Now, this works really well. However I have the following issues with this pattern:

  1. I wanted to avoid having additional routes - i.e. I don't want dashboard/user, I want just dashboard that will render a different child for different users
  2. I can see that this will quickly lead to my routes, and the RoleRouteRedirectService becoming incredibly convoluted as the app grows
  3. This doesn't feel like a correct way to be using a route guard (correct me if I am wrong!)
  4. I feel that there must be a way to handle this from within the router that I am missing

If anyone has any thoughts, I would love to hear them. I appreciate that this is a relatively convoluted problem - if I have missed out any information that would be useful, please comment and I will see what can be added in.

Upvotes: 4

Views: 7976

Answers (2)

dARKaRK1002
dARKaRK1002

Reputation: 36

Angular router uses first-match-wins strategy for defined routes which combined with canActivate is IMO a viable strategy for your use case. I don't think there's anything wrong with that approach. Here is how I implemented my routing:

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'logout', component: HomeComponent  },
  { path: '', component: AuthorizedComponent, canActivate: [AuthGuard], children: [
    { path: 'onboard', component: OnboardComponent, canActivate: [AuthGuard]  },
    { path: 'onboard_requests', component: OnboardRequestsComponent, canActivate: [AuthGuard]  },
    { path: 'employee_list', component: EmployeeListComponent, canActivate: [AuthGuard]  },
    { path: 'employee_detail', component: EmployeeDetailComponent, canActivate: [AuthGuard]  }
  ]
}]

Although your use case seems slightly different than mine but as you can see I used two components HomeComponent and AuthorizedComponent based on canActivate for root path

Upvotes: 0

Daniel Beckmann
Daniel Beckmann

Reputation: 286

An alternative would be to create a parent component which holds both, the DataViewsComponent and the AdminHomeComponent. Based on the user´s role, the relevant component can be chosen via an ngIf. Then you can move the logic from your RoleRouteRedirectService in this parent component:

<DataViewsComponent *ngIf="user.hasRole('user')"></DataViewsComponent>
<AdminHomeComponent *ngIf="user.hasRole('admin')"></DataViewsComponent>

Upvotes: 5

Related Questions