Skip to content
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from '../button.component';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-dashboard',
imports: [RouterLink, ButtonComponent],
imports: [MainLayoutComponent],
template: `
<p>dashboard for Admin works!</p>
<button app-button routerLink="/">Logout</button>
<app-main>dashboard for Admin works!</app-main>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-client',
imports: [CommonModule, MainLayoutComponent],
template: `
<app-main>client dashboard works!</app-main>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClientDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-everyone',
imports: [CommonModule, MainLayoutComponent],
template: `
<app-main>everyone dashboard works!</app-main>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EveryoneDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-dashboard',
imports: [],
selector: 'app-manager',
imports: [MainLayoutComponent],
template: `
<p>dashboard for Manager works!</p>
<button app-button routerLink="/">Logout</button>
<app-main>dashboard for Manager works!</app-main>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-reader-writer',
imports: [CommonModule, MainLayoutComponent],
template: `
<app-main>reader writer dashboard works!</app-main>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReaderWriterDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-reader',
imports: [CommonModule, MainLayoutComponent],
template: `
<app-main>reader dashboard works!</app-main>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReaderDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MainLayoutComponent } from '../layout/main.component';

@Component({
selector: 'app-writer',
imports: [CommonModule, MainLayoutComponent],
template: `
<app-main>writer dashboard works!</app-main>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WriterDashboardComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PermissionDirective } from './permission.directive';

describe('PermissionDirective', () => {
it('should create an instance', () => {
const directive = new PermissionDirective();
expect(directive).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Directive,
EmbeddedViewRef,
inject,
input,
OnInit,
TemplateRef,
ViewContainerRef,
ViewRef,
} from '@angular/core';
import { Role, User } from '../user.model';
import { UserStore } from '../user.store';

@Directive({
selector: '[appHasRole],[appHasRoleSuperAdmin]',
standalone: true,
})
export class PermissionDirective implements OnInit {
private readonly templateRef = inject(TemplateRef);
private readonly viewContainerRef = inject(ViewContainerRef);
private readonly userStore = inject(UserStore);

appHasRole = input<string | string[] | null>(null);
appHasRoleSuperAdmin = input<boolean | string>(true);

private currentEmbeddedView: EmbeddedViewRef<ViewRef> | null = null;

ngOnInit() {
this.manageSubscriptions();
}

private manageSubscriptions(): void {
this.userStore.user$.subscribe({
next: (user) => {
if (!user) {
this.viewContainerRef.clear();
return;
}
const renderView = this.canSectionBeVisible(user);
if (renderView) {
this.createView();
} else {
this.clearView();
}
},
error: () => {
// addition error handling goes here
this.clearView();
},
});
}

private createView(): void {
this.currentEmbeddedView ??= this.viewContainerRef.createEmbeddedView(
this.templateRef,
);
}

private clearView(): void {
this.viewContainerRef.clear();
this.currentEmbeddedView = null;
}

private canSectionBeVisible(activeUser: User): boolean {
const { roles } = activeUser;
const hasRole = this.appHasRole();
const isSuperAdmin = this.appHasRoleSuperAdmin();

if (Array.isArray(hasRole) && hasRole.length > 0) {
return hasRole.some((rolName: string) => roles.includes(rolName as Role));
}

if (typeof hasRole === 'string') {
return roles.includes(hasRole as Role);
}

return activeUser.isAdmin === !!isSuperAdmin;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';

import { authenticatedGuard } from './authenticated.guard';

describe('authenticatedGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authenticatedGuard(...guardParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { UserStore } from '../user.store';

export const authenticatedGuard: CanActivateFn = (route, state) => {
const userStore = inject(UserStore);
const router = inject(Router);

if (userStore.currentUser) return true;
router.navigate(['/']).then();
return false;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';

import { canCanGuard } from './can-can.guard';

describe('canCanGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => canCanGuard(...guardParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { Role } from '../user.model';
import { UserStore } from '../user.store';

export const canCanGuard: CanActivateFn = (route, state) => {
const userStore = inject(UserStore);

const activeUser = userStore.currentUser;
const routeData = route.data;
const roles = routeData?.['roles'] as string[] | undefined;

if (activeUser?.isAdmin && routeData?.['isAdmin']) {
return true;
}

if ([roles ?? []]?.length <= 0) return false;

const hasRole = roles?.some((roleName) =>
activeUser?.roles.includes(roleName as Role),
);
return !!hasRole;
};
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { UserStore } from './user.store';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PermissionDirective } from './directives/permission.directive';

@Component({
selector: 'app-information',
template: `
<h2 class="mt-10 text-xl">Information Panel</h2>
<!-- admin can see everything -->
<div>visible only for super admin</div>
<div>visible if manager</div>
<div>visible if manager and/or reader</div>
<div>visible if manager and/or writer</div>
<div>visible if client</div>
<div *appHasRoleSuperAdmin>visible only for super admin</div>
<div *appHasRole="'MANAGER'">visible if manager</div>
<div *appHasRole="['MANAGER', 'READER']">
visible if manager and/or reader
</div>
<div *appHasRole="['MANAGER', 'WRITER']">
visible if manager and/or writer
</div>
<div *appHasRole="['CLIENT']">visible if client</div>
<div>visible for everyone</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [PermissionDirective],
})
export class InformationComponent {
private readonly userStore = inject(UserStore);

user$ = this.userStore.user$;
}
export class InformationComponent {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { ButtonComponent } from '../button.component';
import { UserStore } from '../user.store';

@Component({
selector: 'app-main',
imports: [CommonModule, ButtonComponent],
template: `
<p>
<ng-content></ng-content>
</p>
<button app-button (click)="onHandleLogout()">Logout</button>
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MainLayoutComponent {
router = inject(Router);
userStore = inject(UserStore);

onHandleLogout() {
this.userStore.add(undefined);
this.router.navigate(['/']).then();
}
}
12 changes: 10 additions & 2 deletions apps/angular/6-structural-directive/src/app/login.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { ButtonComponent } from './button.component';
import { InformationComponent } from './information.component';
Expand Down Expand Up @@ -30,33 +30,41 @@ import { UserStore } from './user.store';

<app-information />

<button app-button class="mt-10" routerLink="enter">
<button app-button class="mt-10" [routerLink]="path()">
Enter application
</button>
`,
})
export class LoginComponent {
private readonly userStore = inject(UserStore);
path = signal<string>('enter');

admin() {
this.userStore.add(admin);
this.path.update(() => 'enter');
}
manager() {
this.userStore.add(manager);
this.path.update(() => 'manager');
}
reader() {
this.userStore.add(reader);
this.path.update(() => 'reader');
}
writer() {
this.userStore.add(writer);
this.path.update(() => 'writer');
}
readerWriter() {
this.userStore.add(readerAndWriter);
this.path.update(() => 'reader-writer');
}
client() {
this.userStore.add(client);
this.path.update(() => 'client');
}
everyone() {
this.userStore.add(everyone);
this.path.update(() => 'everyone');
}
}
Loading