note
Updated 2021-09-01

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

  • required attribute
  • email attribute -- 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

About me

Stuff I do with my brain, my fingers and an editor:

(Front-end) development - typescript, Vue, React, svelte(kit) web architectures web performance API design pragmatic SEO security Accessibility (A11y) front-end dev recruitment and probably more...

Feel free to , check out my open source projects, or just read the things I write on this site.