Notes on Angular.
list
<!-- simple -->
<div *ngFor="let item of items"><!-- do something with item --></div>
<!-- with index -->
<div *ngFor="let item of items; let i = index">
<!-- do something with item and index -->
</div>
if
<div *ngIf="condition">This is conditionally printed</div>
with else:
<div *ngIf="condition">This is printed when condition is true</div>
<div *ngIf="!condition">This is printed when condition is false</div>
Another way but quite ugly.
<div *ngIf="condition; else xxx">This is conditionally printed</div>
<ng-template #xxx>Content of xxx</ng-template>
ngSwitch
<div ngSwitch="value">
<p *ngSwitchCase="5"></p>
<p *ngSwitchCase="3"></p>
<p *ngSwitchCase="1"></p>
<p *ngSwitchDefault></p>
</div>
Data binding
Interpolation
{{ product.name }}
property binding
<img [src]="product.imageUrl" />
Event binding
<button (click)="doSomething()">click me!</button>
Two-way binding
<input [(ngModel)]="quantity" />
Be sure to add the FormsModule to the imports of your module.
Using event
honda A component can emit events
in the ts of component-a:
@Output() elementCreated = new EventEmitter<{eventDataType}>();
...
elementCreated.emit(...);
in the template of another component using this componement:
<app-component-a (elementCreated)="{function to handle the event}"></app-component-a>
View reference
Can be used to avoid two-way binding on inputs.
<div>
<input type="text" #myname />
<button (click)="onClick(myname)">Click!</button>
</div>
Element is passed as an HTML element.
ViewChild
same template as above and in the controller
@ViewChild('myname') input: ElementRef;
...
input.nativeElement // HTMLElement we can use to read
Only available after the onAfterViewInit event.
ngContent
Render the content passed in the body of the component's use:
<app-component-a> <p>This is ignored by default</p> </app-component-a>
The content is ignored by default.
Adding the <ng-content></ng-content> tag changes the default behavior to rebder the content where the tag is on component-a.
The content is interpreted in the template it is written, not in the target component (component-a).
If we need to access ref in component-a, use @ContentChild instead of @ViewChild.
Lifecycle
- onChanges -- when @Input value changes. Receives change of type SimpleChange
- onInit -- init is finished but maybe not displayed
- doCheck -- whenever something changes or UI event
- afterContentInit -- when view of parent is
- afterContentChecked
- afterViewInit -- when component has been displayed
- afterViewChecked
- onDestroy -- place to do cleanup
Directive
- attribute directives -- e.g. ngStyle, ngClass
- structural directives (modifies the current DOM element) -- e.g. ngIf, ngFor
There can be only one structural directive on an element.
Build a attribute directive
@Directive({
selector: '[appYellowText]'
})
export class YellowTextDirective implements onInit {
constructor(
private elemenRef: ElementRef,
private renderer: Renderer2
) {}
ngOnInit() {
// do not change the style directly, renderer2 helps for that (it solves issue of running this in a service worker
this.renderer.setStyle(this.elementRef.nativeElement, 'color', 'yellow' /*, flags*/);
}
}
HostListener
Binding to events
@Directive({
selector: '[appYellowText]'
})
export class YellowTextDirective implements onInit {
constructor(
private elemenRef: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mouseenter')
mouseover(data: Event) {
// ... react on event
}
ngOnInit() {
// do not change the style directly, renderer2 helps for that (it solves issue of running this in a service worker
this.renderer.setStyle(this.elementRef.nativeElement, 'color', 'yellow' /*, flags*/);
}
}
HostBinding
Binding to host properties. May remove the need for renderer and elementRef when changing attribute of host like style.
@Directive({
selector: '[appYellowText]'
})
export class YellowTextDirective implements onInit {
@HostBinding('style.color') color: string = 'transparent';
@HostListener('mouseenter')
mouseover(data: Event) {
// ... react on event
this.color = 'blue';
}
}
Classes is handled with boolean in HostBinding.
@HostBinding('class.expand') isExpanded: boolean = false;
Custom properties binding
@Directive({
selector: '[appYellowText]'
})
export class YellowTextDirective implements onInit {
@Input('color') deaultColor: string = 'green';
@HostBinding('style.color') color: string = 'transparent';
@hostListener('mouseenter')
mouseover(data: Event) {
// ... react on event
this.color = this.defaultColor;
}
}
Build a structural directive
@Directive({
selector: '[appIf]'
})
export class IfDirective {
@Input()
set appIf(condition: boolean) {
if (condition) {
this.vcRef.createEmbeddedView(this.templateRef);
} else {
this.vcRef.clear();
}
}
constructor(
private templateRef: TemplateRef<any>,
private vcRef: ViewContainerRef
) {}
}
Services
Create a class.
Add it to the constructor parameters of a component.
Add it to the providers section in the @Component annotation.
Beware of the hierarchy of injection. Only the component and its childs will get the same instance of the service.
The declaration of the service can be done in the component using the service, in one of its parent upto the app or at the module level.
To inject a service in a service, the service must be declared to receive injections with the annotation @Injectable.
cross component communication
Define an EventEmitter on a service and one user emits and the other subscribes.
This construct should probably be replaced by Observable:
In the service, a Subject is defined and the publishers and subscribers can get the handle on the Subject and register on the .next method.
Routing
Routes of a feature module should be extracted in its own routing module and being added to the exports configuration of the feature module.
Setup
In modules configuration file:
import { Routes, RouterModule } from "@angular/router";
// ...
const routes: Routes = [
{ path: '', component: HomeComonent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id', component: UserComponent },
{ path: 'products', component: ProductsComponent },
// redirect
{ path: 'old/path', redirectTo: '' }
// 404
{ path: '**', component: ErrorComponent, data: { type: 'not-found' } }
]
// ...
@NgModule({
...
imports: [
...,
RouterModule.forRoot(routes)
],
...
})
When a path is expecting parameters, the component must subscribe to the params on the ActivatedRoute in the constructor:
export class UserComponent implements OnInit {
id: number;
params$: Subscription;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.params$ = this.route.params.subscribe((params) => {
this.id = params['id'];
});
}
ngOnDestroy() {
// not mandatory in this particular case
this.params$.unsubscribe();
}
}
Where to render the page component ?, in the main app component, add <router-outlet></router-outlet>
Path ** is the wildcard path, it must always be the last.
Specific case for redirection, as paths are resolved in prefix mode meaning that the path "" matches all paths in the case of redirection:
{ path: '', redirectTo: '/another-path', patchMatch: 'full'}
Else it ends in redirection loop.
Links navigation
In template:
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
<a routerLink="/">Home</a>
</li>
<li routerLinkActive="active"><a routerLink="/users">Users</a></li>
<li routerLinkActive="active"><a routerLink="/products">Products</a></li>
Exact is required as '' is contained in all the other paths.
Programatical navigation
import { Router } from "@angular/router";
@Component(...)
export class XxxComponent implements OnInit {
constructor(private router: Router) {}
onEvent(userId) {
// ...
this.router.navigate(['/users', userId])
}
}
.navigate does not know the current path so the url is always root.
To navigate to a relative url, ActivatedRoute must be injected as follows:
import { Router, ActivatedRoute } from "@angular/router";
@Component(...)
export class XxxComponent implements OnInit {
constructor(private router: Router, private route: ActivatedRoute) {}
onEvent(userId) {
// ...
this.router.navigate(['edit', userId], {relativeTo: this.route})
}
}
Now the path is relative to the current route.
Query string and anchor
To add query parameters to links in the template:
<a
[routerLink]=['/users', id, 'edit']
[queryParams]="{scope: 'full', test: true}"
[fragment]="loading"
>Edit!</a>
Programatically:
this.router.navigate(['/users', id, 'edit'], {
queryParams: {...},
fragment: 'loading'
});
To retrieve these:
export class UsersComponent {
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// this.route.snapshot.queryParams
this.route.queryParams.subscribe(...)
this.route.fragment.subscribe(...)
}
}
Nested router
In the route definitions, use the children optional parameter like so:
const routes: Routes = [
{
path: '/users',
component: UsersComponent,
children: [
{ path: ':id', component: UserComponent },
{ path: ':id/edit', component: UserEditComponent }
]
}
];
Then in the template of the containing component (here UsersComponent), add an outlet:
<!-- -->
<router-outlet></router-outlet>
<!-- -->
where you want the children component to be inserted.
Route pre-guard canActivate
Code executed to protect a route: canActivate
Define a new service xxx-guard.service.ts:
@Injectable()
export class XxxxGuard implement CanActivate {
constructor(private router: Router, private xxxService: XxxService) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.xxxService.check().then(
(can) => {
if (!can) {
this.router.navigate(['not-authorized']);
return false;
}
return can;
}
)
}
}
(This new service must be added to the module's providers section)
The route to "protect" must be configured as follows:
{ path: 'protected', canActivate: [XxxxGuard], ... }
For nested routes, the service must implement CanActivateChild:
//...
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
// ...
}
//...
And the route is configured like this:
{
path: 'protected',
canActivateChild: [XxxxGuard],
children: [ ... ]
}
Both, canActivate and canActivateChild can be used together.
Route post-guard canDeactivate
As before, we define a service but it must implement the CanDeactivate interface. It must also define an interface in order to link the component that is navigated from.
export interface IsReadyToLeave {
isReady: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class ReadyToLeaveGuard implements CanDeactivate<IsReadyToLeave> {
canDeactivate(
component: IsReadyToLeave,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouteStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return componement.isReady();
}
}
on the route configuration:
{ path: 'check-ready', canDeactivate: [ReadyToLeaveGuard], ... }
And obviously, the component must implement the corresponding interface (here IsReadyToLeave)
Adding dynamic data to a route with a resolver
export class DataResolver implement Resolve<DataModel> {
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot) : Observable<DataModel> | Promise<DataModel> | <DataModel> {
// fetch data from service and return it based on route.params
}
}
In the route definition:
{ path: 'data', resolve: {dataName: DataResolver}, ... }
In the target component:
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.data.subscribe(
(data: Data) => {
this.data = data.dataName;
}
)
}
Template driven form
In the template, define a <form> that needs to be intrumented as in the example below. An input element msut have a name and the ngModel attribute.
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<input name="test" ngModel type="email" />
<button type="submit">Submit!</button>
</form>
Default values are defined by property binding the form element to a value: [ngModel]="defaultValue"
The corresponding component declares a onSubmit method:
// ...
// if we need a reference to the form before submission
@ViewChild('f') myForm: NgForm;
onSubmit(form: NgForm) {
form.values.test // contains the entered value
myForm.values.test // same object
}
// ...
NgForm is an internal representation of the form.
Radio button are setup like this:
<label>
<input type="radio" name="myRadioButton" ngModel [value]="theValue" />
{{ theValue }}
</label>
User input validation
When a validator is added and it fails, the form.valid is set to false.
Example
requiredattributeemailattribute -- angular directive specific email validator
Dynamically, angular sets classes to the form element: ng-valid and ng-invalid. To style it:
input.ng-invalid.ng-touched {
border: red 1px solid;
}
To disable the button based on the validation in the form, add the following to the submit button:
<button type="submit" [disabled]="!f.valid">Submit !</button>
To enable error messages related to a field, a reference can be added to the form element and that reference can be used in a ngIf directive.
Grouping elements
Add a <div> with ngModelGroup="groupName". You can the have all the advantage of element but agregated, like valid attribute.
Programatically defining form values
To define all values of the form element at once, use the NgForm.setValue method.
To define one specfic value, use the NgForm.form.patchValue({ ... }).
Reseting form is done by calling NgForm.reset().
Reactive Form
<form [formGroup]="formName" (ngSubmit)="onSubmit">
<input type="email" formControlName="email" />
<button type="submit">Submit!</button>
</form>
@Component({...})
export class MyReactiveForm() {
formName = new FormGroup({
email: new FormControl(null, [Validators.required, Validators.email], this.emailAlreadyUsed)
})
// async method
emailAlreadyUser(control: FormControl): Observable<any> | Promise<any> {
if (control.value) {
return this.emailService.checkIfUsed(control.value).then((isUsed) => {
return ifUsed ? {
'emailAreadyUsed': true
} : null;
})
}
}
onSubmit() {
// do something with formName
}
}
Subscribing to form updates
NgForm.valueChanges.subscribe((value) => { ... }). The subscriber method is called on ever actions on a form component (e.g. every keystroke).
This observable is also available on each FormControl.
NgForm.statusChanges.subscribe((status) => { ... }). status is the state of the form: INVALID, PENDING in case of async validators and VALID.
Array of form elements
<!-- ... -->
<div formControlArray="nameOfArray">
<div *ngFor="let itemCtrl of nameOfForm.get('items').controls; let i = index">
<div>
<input type="text" formControlName="nameOfFormControl" />
</div>
</div>
<button type="button" (addItem)="">"addItem()">add</button>
</div>
<!-- ... -->
// ...
addItem() {
(<FormArray>this.nameOfForm.get('items')).push({
nameOfFormControl: new FormControl()
})
}
onNgInit() {
this.nameOfForm = new FormGroup({
// ...
items: new FormArray([
new FormGroup({
nameOfFormControl: new FormControl(defaultValue)
}),
new FormGroup({
nameOfFormControl: new FormControl(defaultValue)
})
])
// ...
});
}
// ...
Pipes
Transforming output in the template.
<span>{{ value | uppercase }}</span>
References: documentation
Pipes can be chained.
Pipe configuration
Example with date pipe
<span>{{ startDate | date:'fullDate' }}</span>
Parameters are separated with :
Custom pipe
In a file named xxx.pipe.ts:
@Pipe({
name: 'name-in-the-template'
})
export class XxxPipe implements PipeTransform {
transform(value: any, firstParam: Type, secondParam: Type) {
// do something with value and param
return result;
}
}
Then use it:
<span>{{ value | name-in-the-template:'a':'b' }}</span>
Note the quote around the pipe parameters.
The pipe must be declared in the declaration array in the module.
Pipes can be used on lists (e.g. in a ngFor directive) to filter data. Beware, if the content of the list changes the pipe is not updated. This behavior can be changed by adding the pure: false value to the @Pipe decorator. This might hurt performance.
Async Pipe
Enables the usages of async Promise or Observable in the template directly.
HttpClient
Interceptor on request
To implement generic mutation on requests, an Interceptor can be implemented:
@Injectable()
export class MyRequestInterceptor implements HttpInterceptor {
constructor(private service: MyService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const newReq = req.clone({headers: req.headers.set('x', service.getValue()})
return next.handle(newReq);
}
}
And add it:
@NgModule({
// ...
providers: [
// ...
{ provide: HTTP_INTERCEPTORS, useClass: MyRequestInterceptor, multi: true }
]
})
export class CoreModule {}
Interceptor on response
To implement generic mutation on response, an Interceptor can be implemented:
@Injectable()
export class MyResponseInterceptor implements HttpInterceptor {
constructor(private service: MyService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
tap((event) => {
// do something with the event
})
);
}
}
And add it:
@NgModule({
// ...
providers: [
// ...
{
provide: HTTP_INTERCEPTORS,
useClass: MyResponseInterceptor,
multi: true
}
]
})
export class CoreModule {}
Modules
A way to structure an application or extract reusable parts.
There are 4 types of modules:
- Feature module -- structuring the app
- Shared module -- containing elements that are shared across other modules
- Core module -- containing all Directives, Components and Services that are at application layer (e.g. Header, Footer)
- the app
A module is a file that exports a NgModule:
@NgModule({
declarations: [ExportedComponentOne, ExportedComponentTwo],
imports: [CommonModule]
})
export class MyModule {}
A Component, Pipe,... cannot be declared in 2 different modules.
If components in the module are using routing, the child routing must be declared in the module imports section of the module like: RouterModule.forChild(routes)? (as opposed to the forRoot method that must be called only in the application module). Or create a new routing module and set the same configuration but in the exports section.
Shared modules
Extracting parts that are used in several modules.
Lazy loading
On a working application:
Remove module to lazy-load from imports in the application modules configuration.
In the app route modules (if any) add the modules root path again with the following configuration:
const routes = [
// ...
{ path: 'lazy-module-path-root', loadChildren: './path/to/module-without-extension#ClassNameOfModule'
// ...
]
in the module route configuration:
const route = [
{
path: '', // was: 'lazy-module-path-root',
component: LazyComponent,
children: [{
path: '': component: SubComponent
}]
}
]
Preloading can be configured as follows, in the route definition:
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PrealoadAllModules })
]
exports: [RouterModule]
)
By default, there is no preloading. One can define the Strategy to load modules based on other criteria.
Using Guard with lazy loading
canActivate becomes canLoad and the Guard should implement the CanLoad interface.
Modules and Services
If the lazy loaded feature module defines a Service in its providers configuration array, the injected service will be another instance than the same services that would be present in the app's providers configuration.
Don't add providers on Shared module, it will inject in a counter intuitive way on lazy loaded modules using the shared provider.
Core module
Should contain all components, Directives and Services not in another modules (except the app). Services can just be moved to the providers section of the CoreModule.
The CoreModules must export all Component that are used in the app's template (e.g. routingModule, components)
Compilation
Templates parsing and compilation from "text" to javascript.
just-in-time (JiT) vs ahead-of-time (AoT).
AoT
- is faster in browser.
- templates are checked at compile of time
- smaller file size !
Enable it by running ng build --prod --aot.
Deployment
Be sure to define the base href of the app by adding the --base-href /path/to/my/app.
NgRX
Adding its dependency: npm i @ngrx/store.
Add StoreModule in the imports array of the app module.
@NgModule({
// ...
imports: [
// ...
StoreModule.forRoot({ stuff: stuffReducer }) // see below for implementation
]
})
export class
reducers
In a file named stuff.reducers.ts
export interface State {
items: Stuff[];
}
const initialState: State = {
stuff: []
};
export function stuffReducer(state = initialState, action: StuffActions) {
switch (action.type) {
case ADD_STUFF:
// do stuff
return {
...state,
list: ...
/*...*/
};
case REMOVE_STUFF:
// do stuff
return {
...state,
list: ...
/*...*/
};
}
return state;
}
actions
In a file named stuff.actions.ts
export const ADD_STUFF = 'ADD_STUFF';
export const REMOVE_STUFF = 'REMOVE_STUFF';
export class addStuff implements Action {
readonly type = ADD_STUFF;
stuff: Stuff; // payload data model
}
export class removeStuff implements Action {
readonly type = REMOVE_STUFF;
stuff: Stuff; // payload data model
}
export type StuffActions = addStuff | removeStuff;
Fetching data from the store
In the component,
export class Component {
constructor(private store: Store<AppState>) {}
ngInit() {
this.store.select('stuff').subscribe((stuff) => {
// do something with stuff
});
}
}
Or simply create a reference to the observable: stuffInState: Observable<State> = this.store.select('stuff') and use it in the template like:
<ul>
<li *ngFor="let item in (stuffInState | async)">...</li>
</ul>
Reducers agregation
Create an app reducer file combining reducers and states:
import * as fromStuff from '../path/to/stuff.reducer';
export interface AppState {
stuff: fromStuff.State;
auth: fromAuth.State;
}
export const reducers: ActionReducerMap<AppState> = {
stuff: fromStuff.stuffReducer,
auth: fromAuth.authReducer
};
And use that in the app.module StoreModule.forRoot(reducers).
Reducers of lazy-loaded modules
Stores of lazy loaded modules must be added slightly differently. In the module:
@NgModule({
// ...
imports: [
// ...
StoreModule.forFeature('featureName', featureReducer)
]
})
The reducers should declare a Feature interface as State:
export interface FeatureState extends AppState {
stuff: State;
}
The point in this is to "isolate" the module from the rest of the application.
To use the store in the component:
@Component({...})
export class MyComponent {
stuffState: Observable<State>;
constructor(private store: Store<FeatureState>) {}
ngOnInit() {
this.stuffState = this.store.select('stuff')
}
}
Animation
On a component
@Component({
yyy: [
trigger('nameOfTrigger', [
state('in', style({
transform: 'transitionX(0px)',
opacity: 1
})),
transition('void => *', [
style({
opacity: 0,
transform: 'transitionx(-100px)
}),
animate(300)
]),
transition('* => void', [
animate(300, style({
transform: 'translateX(100px)',
opacity: 0
}))
])
])
]
})
export class MyComponent {}
In template, apply the trigger: <div [@nameOfTrigger]></div>
offline
Check angular-pwa that can be added with the cli.
Tools
- Augury -- Chrome extension to inspect angular applications