Skip to content

A demonstration of modern Angular routing patterns that prioritize user experience over traditional blocking guards.

Notifications You must be signed in to change notification settings

cisstech/angular-routing-ux

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Angular routing guards demo

A demonstration of modern Angular routing patterns that prioritize user experience over traditional blocking guards. This project showcases two UX-friendly alternatives to conventional guards that block navigation until authentication is resolved.

Live demo available at : https://cisstech.github.io/angular-routing-ux/

Project structure

This demo follows modern Angular practices with a flat file structure and no suffix naming convention:

src/app/
├── components/
│   ├── index.ts         # barrel export
│   └── skeleton.ts      # reusable skeleton component
├── directives/
│   ├── index.ts         # barrel export  
│   └── auth-if.ts       # structural directive for auth control
├── guards/
│   └── auth-guard.ts    # guard implementations
├── pages/
│   ├── demo-auth-if.ts  # authIf directive demonstration
│   ├── demo-blocking.ts # traditional blocking guard demo
│   ├── demo-skeleton.ts # skeleton guard demonstration
│   ├── home.ts          # application home page
│   └── login.ts         # login simulation page
├── services/
│   ├── index.ts         # barrel export
│   ├── auth-guard.ts    # guard state management service
│   ├── auth.ts          # authentication service
│   └── user-api.ts      # user data service
├── app.ts               # root component
├── app.config.ts        # application configuration
└── app.routes.ts        # routing configuration

Modern Angular conventions

This project demonstrates current Angular best practices:

  • No suffix naming: Files are named auth.ts, skeleton.ts instead of auth.service.ts, skeleton.component.ts
  • Barrel exports: Each directory has an index.ts for clean imports
  • Standalone components: No NgModules, all components are standalone
  • Signals: State management uses signals instead of RxJS subjects

Code simplification for demo purpose

  • Inline templates and styles: Simplified code with no external CSS/HTML files
  • Global CSS sharing: Common styles defined once and reused across components

Traditional guards limitations

Angular's traditional route guards (CanActivate) have significant UX drawbacks:

  • Blocking navigation: Users see nothing while authentication resolves
  • Poor performance metrics: First Contentful Paint is delayed
  • No visual feedback: Applications appear frozen during guard execution
  • Lighthouse score impact: Core Web Vitals suffer from delayed rendering
// Traditional blocking guard - blocks until resolved
export const blockingAuthGuard: CanActivateFn = () => {
  const authService = inject(Auth);
  const router = inject(Router);
  
  return authService.loginStatusChange.pipe(
    map(isLoggedIn => {
      if (!isLoggedIn) {
        router.navigate(['/login']);
        return false;
      }
      return true;
    })
  );
};

Solution 1: Skeleton guard

The skeleton guard allows immediate page rendering with placeholder content while authentication resolves in the background.

Implementation

export const skeletonAuthGuard: CanActivateFn = (route) => {
  const authService = inject(Auth);
  const userService = inject(UserApi);
  const router = inject(Router);
  const guardService = inject(AuthGuard);
  
  const skeleton = route.data['skeleton'];
  const skeletonDelay = route.data['skeletonDelay'] || 300;
  
  // Show skeleton after delay if guard is still resolving
  const skeletonTimer = timer(skeletonDelay).pipe(
    takeUntil(authService.loginStatusChange),
    tap(() => guardService.setSkeleton(skeleton))
  );
  
  return authService.loginStatusChange.pipe(
    tap(() => skeletonTimer.subscribe()),
    mergeMap(isLoggedIn => {
      if (!isLoggedIn) {
        router.navigate(['/login']);
        return of(false);
      }
      
      return userService.loadCurrentUser().pipe(
        map(() => true),
        catchError(() => {
          router.navigate(['/login']);
          return of(false);
        })
      );
    }),
    finalize(() => guardService.setSkeleton(null))
  );
};

Configuration

Guards are configured in route data:

{
  path: 'protected',
  component: ProtectedPage,
  canActivate: [skeletonAuthGuard],
  data: {
    skeleton: SkeletonOverlay,
    skeletonDelay: 300  // milliseconds
  }
}

Use cases

  • Dashboard pages: Show layout skeleton while loading user data
  • Profile sections: Display structure while fetching user information
  • Data-heavy pages: Provide visual feedback during authentication and data loading
  • Mobile applications: Improve perceived performance on slower connections

Limitations

  • Layout matching: Skeleton must exactly match final page layout to avoid layout shifts
  • Maintenance overhead: Skeleton components need updates when page layout changes
  • Content complexity: Not suitable for highly dynamic content structures
  • Animation considerations: Skeleton transitions should be smooth to avoid jarring effects

Skeleton techniques

The skeleton system uses shared CSS between host and overlay components:

@Component({
  selector: 'app-skeleton',
  template: `
    <div
      class="skeleton"
      [class]="'skeleton--' + variant()"
      [style.width]="width() || undefined"
      [style.height]="height() || undefined"
    ></div>
  `,
  styles: [`
    .skeleton {
      background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
      background-size: 200% 100%;
      animation: skeleton-shimmer 1.5s ease-in-out infinite;
    }
  `]
})
export class Skeleton {
  variant = input<'line' | 'text' | 'card' | 'button'>('line');
  width = input<string>();     // explicit width prevents layout shifts
  height = input<string>();    // explicit height maintains space
}

skeletonDelay property: Controls when skeleton appears (default: 300ms). This prevents skeleton flash on fast connections while providing feedback on slower ones.

Solution 2: AuthIf directive

A structural directive that provides granular authentication control without navigation blocking.

Implementation

@Directive({
  selector: '[authIf]',
  standalone: true
})
export class AuthIf {}

Usage

<div *authIf="let authState = state; let user = user; redirectOnReject: false; onAccept: onLoadUser.bind(this)">
  @switch (authState) {
    @case ('pending') {
      <app-skeleton variant="card"></app-skeleton>
    }
    @case ('accepted') {
      <h1>Welcome {{ user?.name }}</h1>
    }
    @case ('denied') {
      <p>Please log in to continue</p>
    }
  }
</div>
class MyComponent {

  onLoadUser(user) {
    // replace ngOnInit
  }
}

Properties

  • authIfOnAccept: Callback to intercept user resolving
  • authIfOnReject: Callback to intercept errors
  • authIfRedirectOnReject: Automatically redirects to login when authentication fails (default: true)
  • state: Provides current authentication state (pending, accepted, denied)
  • user: Exposes current user data when authenticated

This directive can be upgraded to accept roles and check more options on user.

Use cases

  • Mixed content pages: Combine public and protected content on the same page
  • Progressive disclosure: Show different content based on user permissions
  • Granular control: Apply authentication logic to specific page sections
  • Complex layouts: Handle multiple authentication zones independently

Comparison table

Approach Navigation blocking First paint UX impact Complexity Use case
Traditional guard Yes Delayed Poor Low Simple auth check
Skeleton guard No Immediate Good Medium Full page protection
AuthIf directive No Immediate Excellent Low Granular control

Running the demo

npm install
npm run start

Navigate to http://localhost:4200 to explore the different approaches.

Technical considerations

  • Performance: Skeleton and AuthIf approaches improve Core Web Vitals scores
  • Accessibility: Skeleton animations should respect prefers-reduced-motion
  • SEO: Immediate page rendering improves search engine indexing
  • Analytics: Better user engagement metrics due to faster perceived loading

This demo intentionally keeps code simple with inline templates and styles to focus on the core concepts rather than project structure complexity.

About

A demonstration of modern Angular routing patterns that prioritize user experience over traditional blocking guards.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published