angulartypescript

2 components for the same path


My Angular project has this folder structure:

.
├── private
│   ├── components
│   └── private-routing.module.ts
├── public
│   ├── components
│   ├── public-routing.module.ts
├── shared
│   ├── components
│   ├── shared-routing.module.ts

The private directory relies to users who are signed in, the public directory for users who are not signed in, and the shared directory contains components which can be used for both type of users.

My goal is to have 2 different components but both should point to the path ''. So I have a HomeComponent in the private directory but also a HomeComponent in the public directory. I.e. if the user is signed in, HomeComponent from the private directory should be activated, otherwise HomeComponent from public directory.

My problem is that I have 2 routes with the path ''.

private-routing.module.ts:

const routes: Routes = [
  { path: '', component: HomeComponent , canActivate: [LoginGuard], data: { isPrivate: true }, title: "Home"}
]

public-routing.module.ts:

const routes: Routes = [
  {path: '', component: HomeComponent, data: {isPrivate: false}, title: "Home"}
]

This is my guard:

@Injectable()
export class PermissionService {
  constructor(private _routerService: RouterService, private loginService: LoginService, private router: Router) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    return this.loginService.isUserLoggedIn().pipe(
      map((isLoggedIn: boolean): boolean => {
        if (next.routeConfig?.path === '') {
          if (isLoggedIn) {
            console.log('User is logged in');
          }
          else {
            console.log('Unknown user');
          }
        }

        if (next.routeConfig?.path === 'login' || next.routeConfig?.path === 'register') {
          if (isLoggedIn) {
            // If the user is already logged in and trying to access /login or /register, redirect to /
            this.router.navigate(['/']).then(() => window.scrollTo(0, 0));
            return false; 
          }
          // Allow access to /login and /register for unauthenticated users
          return true;
        } else if (!isLoggedIn) {
          // User is not logged in, navigate to /login if trying to access protected routes
          const publicPages: string[] = ['/register', '/forgot-password'];
          if (!publicPages.includes(this.router.url) && this._routerService.isPrivate()) {
            this.router.navigate(['/login']).then(() => window.scrollTo(0, 0));
          }
          return false; 
        } else {
          return true;
        }
      })
    );
  }
}
export const LoginGuard: CanActivateFn = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> => {
  return inject(PermissionService).canActivate(next, state);
}

If the user is signed in, I get the log User is logged in and the HomeComponent from private directory can be displayed. However, if the user is not signed in, I get the log Unknown user but the HomeComponent from public directory will not be shown.


Solution

  • As mentioned in the comments, here is how you might implement it, we can create a function that returns true or false, based on permissions.

    export const canMatchPrivateFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn;
    };
    
    export const canMatchFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn.pipe(map((x: boolean) => !x));
    };
    

    These two functions are configured to their respective routing parts, using canMatch

    export const routes: Routes = [
      {
        // here users route component will get lazy loaded, by using a callback with import statement!
        path: '',
        canMatch: [canMatchPrivateFn],
        loadChildren: () =>
          import('./app/private/private.module').then((mod) => mod.PrivateModule),
      },
      {
        // here users route component will get lazy loaded, by using a callback with import statement!
        path: '',
        canMatch: [canMatchFn],
        loadChildren: () =>
          import('./app/public/public.module').then((mod) => mod.PublicModule),
      },
    ];
    

    After this, when we toggle the flag on the service, we can see the routes being changed based on an asynchronous call (simulates an API)

    import { Injectable } from '@angular/core';
    import { delay } from 'rxjs/operators';
    import { of } from 'rxjs';
    @Injectable({
      providedIn: 'root',
    })
    export class AuthService {
      isLoggedIn = of(true).pipe(delay(1000));
      constructor() {}
    }
    

    Full Code:

    import { Component, inject } from '@angular/core';
    import { provideRouter, Routes, RouterModule } from '@angular/router';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { AuthService } from './auth.service';
    import { map } from 'rxjs';
    
    export const canMatchPrivateFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn;
    };
    
    export const canMatchFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn.pipe(map((x: boolean) => !x));
    };
    
    export const routes: Routes = [
      {
        // here users route component will get lazy loaded, by using a callback with import statement!
        path: '',
        canMatch: [canMatchPrivateFn],
        loadChildren: () =>
          import('./app/private/private.module').then((mod) => mod.PrivateModule),
      },
      {
        // here users route component will get lazy loaded, by using a callback with import statement!
        path: '',
        canMatch: [canMatchFn],
        loadChildren: () =>
          import('./app/public/public.module').then((mod) => mod.PublicModule),
      },
    ];
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [RouterModule],
      template: `
        <router-outlet/>
      `,
    })
    export class App {
      name = 'Angular';
    }
    
    bootstrapApplication(App, {
      providers: [provideRouter(routes)],
    });
    

    Stackblitz Demo