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 usersLog 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 usersLog in and outChat 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 usersLog in and outChat is only accessible for logged in usersRequests 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 usersLog in and outChat is only accessible for logged in usersRequests to the server require a tokenUser 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