https://github.com/Rachnerd/Angular-10

Component based

DI

Modular

Components

Component

  • TypeScript
  • HTML
  • (s)css

User2: ...

User1: ...

User1

User2

User3

Chat

Sidebar

Form

Messages

Channel

npm i -g @angular/cli
ng new chat-app --prefix=ov --style=scss
ng generate component chat
ng g c chat/chat-sidebar
ng g c chat/chat-channel
ng g c chat/chat-messages
ng g c chat/chat-form

Chat

Sidebar

Channel

Messages

Form

Demo

Component types

  • Retrieves data/state via services (DI)
  • Processes events
  • Calls services that process state/logic
  • Receives data from parent
  • Emits events
  • Contains only UI related state/logic

Smart

Dumb

UI

Smart

Dumb

Data

Event

Data flow

Chat

ChatForm

send($event)

User interaction

Button

click($event)

sendMessage(...)

Smart

Dumb

UI

Chat

Sidebar

Channel

Form

Messages

Smart

Dumb

messages

channel

event

Demo

Modules

Chat

Sidebar

Channel

Form

Messages

Smart

Dumb

AppModule

App

AppModule

Chat

ChatModule

...

Demo

@NgModule({
  declarations: [
    ChatSidebarComponent,
    ChatMessagesComponent,
    ChatFormComponent,
    ChatChannelComponent,
    ChatComponent,
  ],
  imports: [CommonModule],
  exports: [ChatComponent]
})
export class ChatModule {}

Module

declarations are (private) components, directives and pipes available within the module scope

imports are other modules that add more declarations 

to this module's scope and services to the root injector

exports are components, directives and pipes that will be available for other modules that import this module

Modules

  • App module (root)
  • Feature modules (Chat)
  • Shared module (UI)

Chat

Sidebar

Channel

Form

Messages

ChatModule

Input

Button

Smart

Dumb

UI

Demo

App

AppModule

Chat

ChatModule

...

Button

UiModule

Input

Sidebar

Services (DI)

AppModule

Chat

ChatModule

...

Button

UiModule

...

Injector (root)

Service

Demo

Injectable

@Injectable({
  providedIn: 'root'
})
export class ChatService {
  // ...
}

Chat

ChatModule

...

Injector (root)

ChatService

Demo

Testing

RxJS

"ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events."

Observable

  • Synchronous
  • Asynchronous
  • Completes automatically
import { of } from 'rxjs';

const number$ = of(123);

number$.subscribe(n => console.log(n));
// 123

"Invokable collection of future values or events"

import { of } from 'rxjs';
import { delay } from 'rxjs/operators';

const delayedNumber$ = of(123)
  .pipe(delay(100));

delayedNumber$.subscribe(n => console.log(n));
delayedNumber$.subscribe(n => console.log(n));
// (100ms pass)
// 123
// 123
delayedNumber$
  .subscribe(
    noop, // Next
    noop, // Error
    () => console.log('Completed')
  );

// (100ms pass)
// Completed

Subject

  • Emits values
  • Requires manual completion

"Equivalent to an EventEmitter, and the only way of multicasting a value or event to multiple Observer"

import { Subject } from 'rxjs';

const numberSubject = new Subject<number>();

numberSubject.asObservable()
  .subscribe(
    n => console.log(n),
    e => console.log('Error occurred!'),
    () => console.log('Completed')
  );

numberSubject.next(123);
// 123

numberSubject.next(456);
// 456
numberSubject.complete();
// Completed

numberSubject.next(456);
// (nothing)

Hot vs Cold

observables

Video (Cold)

  • Starts when you click play
  • Has a limited duration
  • Has started with or without you
  • Ends manually

Stream (Hot)

Cold

this.httpClient.get('assets/messages.json')
  .subscribe(
    n => console.log('Messages'),
    error => console.log('Error'),
    () => console.log('Completed')
  );

/**
 * If the call succeeds
 */ 
// Messages
// Completed

/**
 * If the call fails
 */ 
// Error

Hot

class Service {
  number$: Observable<number>;
  private numberSubject: Subject<number>;
  
  constructor() {
    this.numberSubject = new Subject<number>();
    this.number$ = this.numberSubject
      .asObservable();
  }
}

@Component({
  // ...
})
class Component implements OnInit, OnDestroy {
  n: number;
  private subscription: Subscription;
  
  constructor(private service: Service) {}
  
  ngOnInit() {
    this.subscription = this.service.number$
      .subscribe(n => this.n = n);
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
           

Component lifecycle

"async" Pipe

@Component({
  // ...
  template: `
    <p *ngIf="number$ | async as number">
      {{ number }}
    </p>
  `
})
class Component implements OnInit {
  number$: Observable<number>;
  
  constructor(private service: Service) {}
  
  ngOnInit() {
    this.number$ = this.service.number$;
  }
 
}

Demo

Routing

Routing example

const ROUTES: Route[] = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'chat',
    component: ChatComponent,
  }
];

@NgModule({
  declarations: [],
  imports: [
    RouterModule.forRoot(ROUTES),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

RouterOutlet

Chat

Home

/

/chat

App

AppRouting

"forChild" example

const CHAT_ROUTES: Route[] = [
  {
    path: 'chat',
    component: ChatComponent,
  }
];

@NgModule({
  // ...
  imports: [
    RouterModule.forChild(ROUTES),
    // ...
  ],
  // ...
})
export class ChatModule {}

RouterOutlet

Chat

Home

App

AppRouting

Lazy load example

RouterOutlet

Chat

Home

const ROUTES: Route[] = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'chat',
    loadChildren: () =>
      import('./chat/chat.module')
        .then((m) => m.ChatModule)
  }
];

@NgModule({
  declarations: [],
  imports: [
    RouterModule.forRoot(ROUTES),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Demo

App

AppRouting

Child routes example

RouterOutlet

Chat

Home

const CHAT_ROUTES: Route[] = [
  {
    path: '',
    component: ChatComponent,
    children: [
      {
        path: '',
        component: ChatChannelComponent,
      },
      {
        path: 'user',
        component: ChatUserComponent,
      },
    ],
  },
];

...

RouterOutlet

User

Channel

Demo

Authentication

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

User

// auth.model.ts
interface BaseUser {
  username: string;
}

export interface AnonymousUser extends BaseUser {
  type: 'Anonymous';
}

export interface LoggedInUser extends BaseUser {
  type: 'LoggedIn';
  token: string;
  image: string;
}

export type User = AnonymousUser | LoggedInUser;

AuthService

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  user$: Observable<User>;
  private userSubject: BehaviorSubject<User>;

  constructor(
    private httpClient: HttpClient
  ) {
    this.userSubject = 
      new BehaviorSubject<User>(ANONYMOUS_USER);
    this.user$ = this.userSubject.asObservable();
  }
  
  getUser(): User {
    return this.userSubject.getValue();
  }

  login(): void {
    this.httpClient.get('assets/user.json')
      .subscribe((user: LoggedInUser) => {
        this.userSubject.next(user);
      });
  }

  logout(): void {
    this.userSubject.next(ANONYMOUS_USER);
  }
}

HomeComponent

@Component({
  // ...
  template: `
    <p>Welcome {{ (user$ | async).username }}</p>
  `
})
export class HomeComponent implements OnInit {
  user$: Observable<User>;
  constructor(private authService: AuthService) {}

  ngOnInit(): void {
    this.user$ = this.authService.user$;
  }
}

Demo

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

RouterOutlet

Chat

Home

Logout

Login

Auth

App

AppRouting

AuthService

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  isLoggedIn$: Observable<boolean>;
  
  user$: Observable<User>;
  private userSubject: BehaviorSubject<User>;

  constructor(
    private httpClient: HttpClient
  ) {
    this.userSubject = 
      new BehaviorSubject<User>(ANONYMOUS_USER);
    this.user$ = this.userSubject.asObservable();
      
    this.isLoggedIn$ = this.user$.pipe(
      map(user => user.type !== 'Anonymous')
    );
  }
  
  // ...
}

LoginComponent

@Component({
  // ...
})
export class LoginComponent implements OnInit, OnDestroy {
  private subscription: Subscription;

  constructor(private authService: AuthService, private router: Router) {}

  ngOnInit(): void {
    this.subscription = this.authService.isLoggedIn$
      .pipe(filter((isLoggedIn) => isLoggedIn))
      .subscribe(() => this.router.navigate(['']));
  }

  login(): void {
    this.authService.login();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

LogoutComponent

@Component({
  // ...
})
export class LogoutComponent implements OnInit, OnDestroy {
  private subscription: Subscription;

  constructor(private authService: AuthService, private router: Router) {}

  ngOnInit(): void {
    this.subscription = this.authService.isLoggedIn$
      .pipe(filter((isLoggedIn) => !isLoggedIn))
      .subscribe(() => this.router.navigate(['']));

    this.authService.logout();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

Demo

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

AppComponent

@Component({
  // ...
  template: `
    ...
    <ov-navigation [menu]="navigationMenu$ | async"></ov-navigation>
    ...
  `
})
export class AppComponent implements OnInit {
  navigationMenu$: Observable<NavigationMenu[]>;

  constructor(private authService: AuthService) {}

  ngOnInit(): void {
    this.navigationMenu$ = this.authService.isLoggedIn$.pipe(
      map(isLoggedIn => (isLoggedIn ? LOGGED_IN_MENU : LOGGED_OUT_MENU))
    );
  }
}

Demo

LoggedInGuard

@Injectable({
  providedIn: 'root',
})
export class LoggedInGuard implements CanActivate {
  constructor(private router: Router, private authService: AuthService) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    return this.authService.isLoggedIn$.pipe(
      tap((isLoggedIn) => {
        if (!isLoggedIn) {
          this.router.navigate(['login']);
        }
      })
    );
  }
}

Demo

// chat.module.ts
const CHAT_ROUTES: Route[] = [
  {
    path: '',
    component: ChatComponent,
    canActivate: [LoggedInGuard],
    // ...
  },
];

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

Interceptor

@Injectable()
export class Interceptor implements HttpInterceptor {
  
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    /**
     * Process request
     */ 
    return next.handle(request)
      .pipe(
        map(response => {
          /**
            * Process response
            */ 
          return response;
        })
      );
  }
}

TokenInterceptor

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
  constructor(public authService: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
      
    const user = this.authService.getUser();
    if (user.type === 'LoggedIn') {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${user.token}`,
        },
      });
    }

    return next.handle(request);
  }
}

Demo

@NgModule({
  // ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true,
    },
  ],
})
export class AuthModule {}

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

AuthService

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  // ...
  constructor(
    private httpClient: HttpClient
  ) {
    const persistedUser = this.getPersistedUser();
    this.userSubject = 
      new BehaviorSubject<User>(persistedUser);
      
    // ...
    this.user$.subscribe((user) =>
      sessionStorage.setItem(
        USER_STORAGE_KEY, 
        JSON.stringify(user)
      )
    );
  }
  
  private getPersistedUser(): User {
    return JSON.parse(
      sessionStorage.getItem(USER_STORAGE_KEY)
    ) || ANONYMOUS_USER
  }
  
  // ...
}
  • Coupled to web
  • Not configurable

Chat

Storage

App

Injector (root)

InjectionToken

Demo

Requirements

  • Anonymous / logged in users
  • Log in and out
  • Chat is only accessible for logged in users
  • Requests to the server require a token
  • User is persisted in storage

Advanced routing

Router events

Guards

  • CanActivate
  • CanActivateChild
  • CanDeactivate
  • Resolve
  • CanLoad

Named outlets

  • CanActivate
  • CanActivateChild
  • CanDeactivate
  • Resolve
  • CanLoad

Angular 10

By rachnerd

Angular 10

  • 185