Angular Notes

angular is a JavaScript Framework which allows you to create reactive Single-Page-Applications(SPAs).

Angular Fundamentals

  1. Component & Templates
  2. Forms
  3. Observables & RxJS
  4. NgModules
  5. Service & Dependency Injection
  6. HttpClient
  7. Routing & Navigation

 

Plan the App

Angular CLI

npm install -g @angular/cli
// then go to the folder we want to create the project
ng new my-dream-app
cd my-dream-app
ng serve

Use Bootstrap

// use bootstrap 2
npm install --save bootstrap@3

// in angular.json, styles
// add at front
"node_modules/bootstrap/dist/css/bootstrap.min.css",

Use Angular material

ng add @angular/material

How an angular app gets loaded and started

index.html file is served by server

<app-root> </app-root>

replace with the template of the component. the template of this component is app.component.html

cli created the root component of the application under "app folder"

in app.component.ts

selector: 'app-root'

in main.ts, bootstrap start the application with AppModule

in app.module

app.module.ts: declaration: register the app component with angular

bootstrap: [AppComponent]

list all the components should be known to angular at the point of time it analyse index.html file.

 

Components

angular thinks in "Components"

component is a typescript class.

Ideally, a component's job is to enable the user experience and nothing more. A component should present properties and methods for data binding, in order to mediate between the view (rendered by the template) and the application logic (which often includes some notion of a model).

other components, their selectors will be added to the app component from component decorator.

the other component stored in another folder.  component declarations in app.module

creating components with the CLI

ng generate component name
// or
ng g c name
// inside a component folder
ng g c folder/name

nesting components: inside one component call another component using their selectors multiple times

component selector is like CSS selector

select by elements

by attributes

by class

 

Data Binding

Simply put, data binding is a fundamental concept in Angular that allows developers to make communication between a component and its view or more precisly the DOM. This way you don't need to manually push data from your component to the DOM and back.

Angular provides four types of data binding and they are essentically different in the way data flows i.e from the component to the DOM, from the DOM to the component or both ways:

  • String Interpolation: Data flows from the component to the DOM - It's used to display the value of a component member variable in the associated template, e.g. ``. We use curly braces for interpolation {{ }}.
  • Property binding: Data flows from the component to a property of an element in the DOM. It's used to bind a component member variable to an attribute of a DOM such as the value attribue of an <input> tag (For example: <input type="text" [value]="foobar">). We use brackets for property binding.
  • Event binding: Data flows from the DOM to the component. When a DOM event, such as a click, is triggered, the bound method from the component is called. For example: <button (click)="sayHi()">Hi</button> - The sayHi() method will be called so it needs to be defined in the component class. We use parentheses for event binding. passing data, $event is the reserved name, access to event emitted data. when using data, event is in type Event
  • Two-way data binding: Data flows both ways. For example: <input type="text" [(ngModel)]="foobar"> (The foobar variable needs to be defined in the component). The input element and foobar will have the same value and when one changes, the other one changes to the same value accordingly. We use the banana in the box syntax which combines brackets and parentheses for two-way data binding. ngModel is a special directive that binds to the value attribute of the <input> and <textarea> elements but you can constrcut two-way data binding for any property in the DOM or component. Two-way data binding = property binding + event binding. Note need to import FormsModule in app.module.ts.

Component communication  (with 上下級關係)

binding to custom property

use decorator

// expose this property to the component
// bind the property from outside
@Input() element

// then the host component (through the selector) is able to bind the element
// in app.component.html
[element] = "serverElement"

// assigning alias to custom properties
@Input('srvElement) element

[srvElement] = "serverElement"

binding to custom events

// define the type the eventemitter is going to emit
@Output() serverCreated = new EventEmitter<{serverName: string, serverContent: string}>();

// emit the new event of this type
this.serverCreated.emit({serverName: val, serverContent: val})

// can subscribe to the event
// Registers handlers for events emitted by this instance.
.subscribe(()=>{

})

// in app.component.html
// onServerAdded is in app.component.ts
// serverCreated is from cockpit.component.ts
<app-cockpit (serverCreated) = "onServerAdded($event)"></app-cockpit>

// assign alias similar to property binding

 

View encapsulation (angular feature)

View encapsulation defines whether the template and styles defined within the component can affect the whole application or vice versa. Angular provides three encapsulation strategies:

  • Emulated (default) - styles from main HTML propagate to the component. Styles defined in this component's @Component decorator are scoped to this component only.

@Component({
// ...
encapsulation: ViewEncapsulation.None,
styles: [
  // ...
]
})
export class HelloComponent {
// ...
}

local references

Instead of two-way binding, we can easily fetch a value of any input through local references in Angular.

can apply to any html element

// will hold the reference to the element (all the properties of the element)
<input type = "text" class="form-control" #name>

// can use anywhere in the template (in html file, not in ts)
<button class="btn btn-primary" (click) = "onAddServer(name)">Add Server </button>

Access to template and DOM with local reference pass through @viewchild

// in component.ts, serverContentInput is of type ElementRef
@ViewChild('name',{static: true}) serverContentInput: ElementRef;

// in component.html, local references
<input type = "text" class="form-control" #name>

// to get access to the element through nativeElement
this.serverContentInput.nativeElement.value;

Project content into components with ng-content

html pass into the component from outside

// anything between the opening and closing tag of the own component is lost
<app-server-element>

</app-server-element>

// use ng-content
// in server-element.component.html
// as a hook to mark the place for the content inbetween the tags
<ng-content> </ng-content>

get access to ng-content with @ContentChild

// in server-element.component.ts
// we want to access to the local reference  defined in app.component.html through ng-content
@ContentChild('name' {static:true}) varname;

// in server-element.component.html
<ng-content> </ng-content>

// in app.component.html, local references
<app-server-element>
<input type = "text" class="form-control" #name>
</app-server-element>

 

Component lifecycle

Every Angular component and Angular directive have a lifecycle

Angular creates it, renders it, creates and renders it’s children, checks it when it’s data-bound properties change, and destroys it before removing it from the DOM.

 

 

Directives

  • directives are instructions in the DOM

structural directives

*ngIf="posts.length > 0"

enchance ngif with else condition

local reference:

<p *ngIf="serverCreated; else noServer"> Server was created, server name is {{serverName}} </p>
<ng-template #noServer>
    <p>No server was created!</p>
</ng-template>

*ngFor="let post of posts"

<app-server *ngFor="let server of servers"></app-server>

get the index from ngFor

<app-server *ngFor="let server of servers; let i = index"></app-server>

ngSwitch

<div [ngSwitch]="value">
    <p *ngSwitchCase="5"> Value is 5 </p>
    <p *ngSwitchDefault="0"> Value is 0 </p>
</div>

building a structrual directive

@Directive({
    selector: "[appUnless]"
})
export class UnlessDirecrive implement OnInit{
    @Input() set appUnless(condition: boolean){
        if(!condition){
            this.vcRef.createEmbeddedView(this.remplateRef);
        }else{
            this.vcRef.clear();
        }
    }
    constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) {}
}

use *appUnless as directive

attribute directives

don't add or remove elements. they only change the element they were placed on.

use property binding with this directive.

ngStyle dynamically assign a CSS style.

<p [ngStyle] = "{backgroundColor: getColor()}"> </p>

ngClass allows dynamically remove CSS classes

<!-- attach class to CSS if the condition is true -->
<p [ngClass] ="{classname: serverStatus === 'online'}"> </p>

custom attribute directives

@Directive({
    selector:'[appBasic]'
})
export class BasicDirective{

}

//in app.module.ts add in declarations:

// in app.component.html
<p appBasic>  <p>

create directive

ng g d directivename

1. Use renderer (not directly change the dom element)

import{ Directive, OnInit, Renderer2 } from

@Directive({
    selector: "[]"
})
export class DirecriveName implement OnInit{
    constructor(private elRef: ElementRef, private renderer: Renderer2) {}
    ngOnInit(){
        this.renderer.setStyle(this.elRef.nativeElement, 'background-color','blue');

    }
}

2. use hostbinding to bind to host properties

another method instead of renderer

@HostBinding('property of hosting element we want to bind') property:type

hostlistener to listen to host events

@HostListener('eventname') method(eventData: Event){

}

 

binding to directive properties

use custom property binding

 

Forms

Both reactive and template-driven forms share underlying building blocks.

  • FormControl tracks the value and validation status of an individual form control.
  • FormGroup tracks the same values and status for a collection of form controls.
  • FormArray tracks the same values and status for an array of form controls.
  • ControlValueAccessor creates a bridge between Angular FormControl instances and native DOM elements.

 The source of truth provides the value and status of the form element at a given point in time. In reactive forms, the form model is the source of truth. In the example above, the form model is the FormControl instance. With reactive forms, the form model is explicitly defined in the component class. The reactive form directive (in this case, FormControlDirective) then links the existing FormControl instance to a specific form element in the view using a value accessor (ControlValueAccessor instance).

In template-driven forms, the source of truth is the template. The abstraction of the form model promotes simplicity over structure. The template-driven form directive NgModel is responsible for creating and managing the FormControl instance for a given form element. It's less explicit, but you no longer have direct control over the form model. 

  • template-driven:
    • 模板驅動的表單是我們實例化好一個類的數據之後,在html中使用 NgForm 指令後將數據和表單進行綁定,使用[(ngModel)]來將表單的數據和和視圖進行雙向綁定,NgForm 指令爲 form 增補了一些額外特性。 它會控制那些帶有 ngModel 指令和 name 屬性的元素,監聽他們的屬性。
  • reactive:
    • are more robust: they're more scalable, reusable, and testable.
    • 我們會在數據源裏面進行各種操作,像添加校驗等,在html文件中使用 formGroup,formGroupName,formControlName等將數據和視圖進行綁定(需要引入ReactiveFormsModule)。

Data flow in forms

Updates from the view to the model and from the model to the view are synchronous and aren't dependent on the UI rendered.

 

The steps below outline the data flow from view to model.

  1. The user types a value into the input element, in this case the favorite color Blue.
  2. The form input element emits an "input" event with the latest value.
  3. The control value accessor listening for events on the form input element immediately relays the new value to the FormControl instance.
  4. The FormControl instance emits the new value through the valueChanges observable.
  5. Any subscribers to the valueChanges observable receive the new value.

The steps below outline the data flow from model to view.

  1. The user calls the favoriteColorControl.setValue() method, which updates the FormControl value.
  2. The FormControl instance emits the new value through the valueChanges observable.
  3. Any subscribers to the valueChanges observable receive the new value.
  4. The control value accessor on the form input element updates the element with the new value.

  1. The user types Blue into the input element.
  2. The input element emits an "input" event with the value Blue.
  3. The control value accessor attached to the input triggers the setValue() method on the FormControl instance.
  4. The FormControl instance emits the new value through the valueChanges observable.
  5. Any subscribers to the valueChanges observable receive the new value.
  6. The control value accessor also calls the NgModel.viewToModelUpdate() method which emits an ngModelChange event.
  7. Because the component template uses two-way data binding for the favoriteColor property, the favoriteColor property in the component is updated to the value emitted by the ngModelChange event (Blue).

 

The steps below outline the data flow from model to view when the favoriteColor changes from Blue to Red.

  1. The favoriteColor value is updated in the component.
  2. Change detection begins.
  3. During change detection, the ngOnChanges lifecycle hook is called on the NgModel directive instance because the value of one of its inputs has changed.
  4. The ngOnChanges() method queues an async task to set the value for the internal FormControl instance.
  5. Change detection completes.
  6. On the next tick, the task to set the FormControl instance value is executed.
  7. The FormControl instance emits the latest value through the valueChanges observable.
  8. Any subscribers to the valueChanges observable receive the new value.
  9. The control value accessor updates the form input element in the view with the latest favoriteColor value.

 

Form validation

Validation is an integral part of managing any set of forms. Whether you're checking for required fields or querying an external API for an existing username, Angular provides a set of built-in validators as well as the ability to create custom validators.

  • Reactive forms define custom validators as functions that receive a control to validate.
  • Template-driven forms are tied to template directives, and must provide custom validator directives that wrap validation functions.

 

1.Template-Driven

in app.module.ts

import { FormsModule } from '@angular/forms';

imports: [
    FormsModule,
]

create forms and register the controls

<!-- no binding, tell Angular input is the control -->
<form>
    <!-- register the control -->
    <input 
        type = "text"
        if="username"
        ngModel
        <!-- name of the control -->
        name="username">
</form>

<!-- set control with default values with ngModel Property Binding -->
[ngModel] = ""

<!-- use ngModel with two-way binding, instantly do with the value -->
[(ngModel)] = ""

submit and use the form

<!-- Access the JS object (form) created by angular using the reference -->
<form (ngSubmit)="onSubmit(f)" #f="ngForm">

<button type="submit"> </button>

</form>
// use the element from the component
onSubmit(form: ngForm) {

}

use form with viewChild

<!-- Access the JS object (form) created by angular using the reference -->
<form (ngSubmit)="onSubmit()" #f="ngForm">
@ViewChild('f') signupForm:NgForm;

onSubmit( {
    console.log(this.signupForm);
}

 

Validators

angular dynamically add some CSS class, give information about the individual controls.

https://angular.io/api/forms/Validators

 

Form State

<form #f="ngForm">
    <button
    class=
    type=
    [disabled]="f.valid"> </button>
</form>
<!-- state managed by Angular -->
<!-- get access to the control created -->
<input #email="ngModel">

<span *ngIf="email.valid">

Group form controls

<div ngModelGroup = "userData" #userData="ngModelGroup">

<!-- use ngModelGroup, userData field is added to the NgForm object -->

Set and Patch Form values

@ViewChild('f') signupForm:NgForm;

// set the whole form
this.signupForm.setValue()

// to overwrite parts of the form
this.signupForm.form.patchValue()

Use Form Data

this.signupForm.value.userData.xxx

Reset From

this.signupForm.reset();

// or reset with specific values

 

2.Reactive-driven

The FormControl class is the basic building block when using reactive forms.

not configure the form in the template

import { ReactiveFormModule } from '@angular/forms';


imports:[
    ReactiveFormsModule

]

 

Displaying a form control value

  • Through the valueChanges observable where you can listen for changes in the form's value in the template using AsyncPipe or in the component class using the subscribe() method.
  • With the value property, which gives you a snapshot of the current value.

 

Replacing a form control value

A form control instance provides a setValue() method that updates the value of the form control and validates the structure of the value provided against the control's structure.

 

Partial model updates(patch)

  • Use the setValue() method to set a new value for an individual control. The setValue() method strictly adheres to the structure of the form group and replaces the entire value for the control.

  • Use the patchValue() method to replace any properties defined in the object that have changed in the form model. PatchValue() only updates properties that the form model defines.

updateProfile() {
  this.profileForm.patchValue({
    firstName: 'Nancy',
    address: {
      street: '123 Drew Street'
    }
  });
}
<p>
  <button (click)="updateProfile()">Update Profile</button>
</p>

 

Grouping form controls

a form group instance tracks the form state of a group of form control instances (for example, a form). Each control in a form group instance is tracked by name when creating the form group.

import {FromControl,FormGroup} from '@angular/forms';

@Component()
export class AppComponent implements OnInit {
    // declare type
    signupForm: FormGroup;

    ngOnInit(){
        this.signupFrom = new FormGroup({
            'username':new FromControl(null),
            'email': new FormControl(null),
            'gender': new FormControl('male')
        });
    }
}

The individual form controls are now collected within a group. A FormGroup instance provides its model value as an object reduced from the values of each control in the group. A form group instance has the same properties (such as value and untouched) and methods (such as setValue()) as a form control instance.

 

Associating the FormGroup model and view

A form group tracks the status and changes for each of its controls, so if one of the controls changes, the parent control also emits a new status or value change.

using formControl binding provided by FormControl Directive in  Reactive Forms Module

<form [formGroup]="signupForm">

    <input formControlName = "username">

    <input formControlName = "email">


</form>

 

submit the form

The FormGroup directive listens for the submit event emitted by the form element and emits an ngSubmit event that you can bind to a callback function.

<form [formGroup]="signupForm" (ngSubmit) = "onSubmit()">

    <input formControlName = "username">

</form>

 

Get access to controls

signupForm.get('username')

<!-- overall form -->
signupForm

 

Nested groups

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    address: new FormGroup({
      street: new FormControl(''),
      city: new FormControl(''),
      state: new FormControl(''),
      zip: new FormControl('')
    })
  });
}
<div formGroupName="address">
  <h3>Address</h3>

  <label>
    Street:
    <input type="text" formControlName="street">
  </label>

  <label>
    City:
    <input type="text" formControlName="city">
  </label>
  
  <label>
    State:
    <input type="text" formControlName="state">
  </label>

  <label>
    Zip Code:
    <input type="text" formControlName="zip">
  </label>
</div>
signupForm.get('userData.username')

 

Generating form controls with FormBuilder (service)

import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
  profileForm = this.fb.group({
    firstName: [''],
    lastName: [''],
    address: this.fb.group({
      street: [''],
      city: [''],
      state: [''],
      zip: ['']
    }),
  });

  constructor(private fb: FormBuilder) { }
}

 

Dynamic controls using form arrays (FormArray)

FormArray is an alternative to FormGroup for managing any number of unnamed controls. As with form group instances, you can dynamically insert and remove controls from form array instances, and the form array instance value and validation status is calculated from its child controls. However, you don't need to define a key for each control by name, so this is a great option if you don't know the number of child values in advance.

Use the FormBuilder.array() method to define the array, and the FormBuilder.control() method to populate the array with an initial control.

import { FormArray } from '@angular/forms';

profileForm = this.fb.group({
  firstName: ['', Validators.required],
  lastName: [''],
  address: this.fb.group({
    street: [''],
    city: [''],
    state: [''],
    zip: ['']
  }),
  aliases: this.fb.array([
    this.fb.control('')
  ])
});

Accessing the FormArray control

A getter provides easy access to the aliases in the form array instance compared to repeating the profileForm.get() method to get each instance. The form array instance represents an undefined number of controls in an array. It's convenient to access a control through a getter, and this approach is easy to repeat for additional controls.

get aliases() {
  return this.profileForm.get('aliases') as FormArray;
}

 Define a method to dynamically insert an alias control into the alias's form array. The FormArray.push() method inserts the control as a new item in the array.

addAlias() {
  this.aliases.push(this.fb.control(''));
}

Displaying the form array in the template

<div formArrayName="aliases">
  <h3>Aliases</h3> <button (click)="addAlias()">Add Alias</button>

  <div *ngFor="let alias of aliases.controls; let i=index">
    <!-- The repeated alias template -->
    <label>
      Alias:
      <input type="text" [formControlName]="i">
    </label>
  </div>
</div>

 

Another example

'hobbies': new FormArray([]);

onAddHobby(){
    const control = new FormControl(null,Validators.required);
    //expilicit cast
    (<FormArray>this.signupForm.get('hobbies')).push(control);
}

getControls(){
    return (<FormArray>this.signupForm.get('hobbies')).controls;
}
<div formArrayName="hobbies">
    <!-- access the element in the form array -->
    getControls()

</div>

 

Add validation

in ts code

import {FromControl,FormGroup, Validators} from '@angular/forms';

@Component()
export class AppComponent implements OnInit {
    // declare type
    signupForm: FormGroup;

    ngOnInit(){
        this.signupFrom = new FormGroup({
            'username':new FromControl(null, Validators.required),
            'email': new FormControl(null, [Validators.required, Validators.email]),
            'gender': new FormControl('male')
        });
    }
}

 

create custom validators example

//key value pair
forbiddenNames(control: FormControl): {[s: string]:boolean} {
    if(this.forbiddenUsernames.indexOf(control.value) !== -1){
        return {'nameIsForbidden':true};
    }
    // if validation is successful, have to return null
    return null;
}

when use as validator

this.forbiddenNames.bind(this)

Async validators

//key value pair
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise<any>((resolve,reject) => {
        if
            resolve({'emailIsForbidden': true});
        else
            resolve(null);
    }
    return promise;
}

React to value changes and status changes

two observables

this.signupForm.valueChanges.subscribe(
    (value) =>

);

this.signupForm.statusChanges.subscribe(
    (status) =>

);

 

 

RxJS

RxJS is a library for composing asynchronous and event-based programs by using observable sequences.

  • Observable: represents the idea of an invokable collection of future values or events.
  • Observer: is a collection of callbacks that knows how to listen to values delivered by the Observable.
  • Subscription: represents the execution of an Observable, is primarily useful for cancelling the execution.
  • Operators: are pure functions that enable a functional programming style of dealing with collections with operations like map, filter, concat, reduce, etc.
  • Subject: is the equivalent to an EventEmitter, and the only way of multicasting a value or event to multiple Observers.
  • Schedulers: are centralized dispatchers to control concurrency, allowing us to coordinate when computation happens on e.g. setTimeout or requestAnimationFrame or others.
  • Subscriber: Implements the Observer interface and extends the Subscription class. While the Observer is the public API for consuming the values of an Observable, all Observers get converted to a Subscriber, in order to provide Subscription-like capabilities such as unsubscribe. Subscriber is a common type in RxJS, and crucial for implementing operators, but it is rarely used as a public API.

Observables

  • Observables provide support for passing messages between publishers and subscribers in your application.
  • Observables are declarative—that is, you define a function for publishing values, but it is not executed until a consumer subscribes to it.
  • The subscribed consumer then receives notifications until the function completes, or until they unsubscribe.

Usage

As a publisher, you create an Observable instance that defines a subscriber function. This is the function that is executed when a consumer calls the subscribe() method. The subscriber function defines how to obtain or generate values or messages to be published.

To execute the observable you have created and begin receiving notifications, you call its subscribe() method, passing an observer (This is a JavaScript object that defines the handlers for the notifications you receive). The subscribe() call returns a Subscription object that has an unsubscribe() method, which you call to stop receiving notifications.

 

observable.subscribe(observer)

 

                    Invoke

next(), <-------------------------|

error(),   <-----------------------|

complete()  <-------------------|

observer -----------------> observable    ----------------------> Http Request

                 Subscription                          Wraps Callback

three notification types called on the observer side: next(),error(),complete()

  • observer is what we basically pass into subscribe. tell observer the new data, error, completion. (a listener)
  • observable is a wrap around the stream of values, use subscribe to be informed about change in data. observable emitting data and listening to that data in different palces of our application. we can subscribe to certain updates, changes and push these changes from a different place. observable is like "passive" subject can actively trigger.

angular obervables like(params) do not need to unsubscribe.

 

Defining observers

A handler for receiving observable notifications implements the Observer interface. It is an object that defines callback methods to handle the three types of notifications that an observable can send:

next Required. A handler for each delivered value. Called zero or more times after execution starts.
error Optional. A handler for an error notification. An error halts execution of the observable instance.
complete Optional. A handler for the execution-complete notification. Delayed values can continue to be delivered to the next handler after execution is complete.

An observer object can define any combination of these handlers. If you don't supply a handler for a notification type, the observer ignores notifications of that type.

 

Custom observable

import { Subscription, Observable } from 'rxjs';

@Component()

export class HomeComponent implements OnInit {
    private firstObsSubscription: Subscription;
    
    constructor() {}

    ngOnInit(){

        //observer
        //next, error, complete
        const customIntervalObservable = Observable.create((observer)=>{
            let count = 0;
            setInterval(handler:()=>{    
                // call next to emit a new value
                observer.next(count);
                if(count == 2)
                    observer.complete();    
                if(count>3){
                    observer.error(new Error());
                }
                count++;
            }, timeout:1000);
        });

        // start using observerables        
        this.firstObsSubscription = customIntervalObservable.subscribe(data => {
            console.log(data);
        },error => {
            console.log(error);
        },()=>{
            console.log('completed');
        });
    }    
    ngOnDestroy(){
        this.firstObsSubscription.unsubscribe();
    }
}

Operators

 

map()

 

chain of operators

import { map } from 'rxjs/operators';

customIntervalObservable.pipe(filter(),map(data => {
    return ;
)).subscribe();

Subject

like an observable, but more actively need to be triggered ( like event emitter). note observable, next() can only be called inside when created.

in service

import {Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({providedIn: 'root'})
export class UserService {
    activatedEmitter = new Subject<boolean>();
}

To emit

this.userService.activatedEmitter.next(true);

when use, use as an observable.

  • In this case subject is a suitable replacement for angular event emitter. (use as  cross component event emitters, when maually call next(emit), through services).
  • when using @output, not suitable for using Subject

 

 

Service and dependency injection

Service is a broad category encompassing any value, function, or feature that an app needs. A service is typically a class with a narrow, well-defined purpose. It should do something specific and do it well.

 

A component can delegate certain tasks to services, such as fetching data from the server, validating user input, or logging directly to the console. By defining such processing tasks in an injectable service class, you make those tasks available to any component. You can also make your app more adaptable by injecting different providers of the same kind of service, as appropriate in different circumstances.

dependency injection

Components consume services; that is, you can inject a service into a component, giving the component access to that service class. To define a class as a service in Angular, use the @Injectable() decorator to provide the metadata that allows Angular to inject it into a component or other class (such as another service, a pipe, or an NgModule) as a dependency.

  • The injector is the main mechanism. Angular creates an application-wide injector for you during the bootstrap process, and additional injectors as needed. You don't have to create injectors.

  • An injector creates dependencies, and maintains a container of dependency instances that it reuses if possible.

  • A provider is an object that tells an injector how to obtain or create a dependency.

For any dependency that you need in your app, you must register a provider with the app's injector, so that the injector can use the provider to create new instances. For a service, the provider is typically the service class itself.

When Angular creates a new instance of a component class, it determines which services or other dependencies that component needs by looking at the constructor parameter types. For example, the constructor of HeroListComponent needs HeroService.

constructor(private service: HeroService) { }

When Angular discovers that a component depends on a service, it first checks if the injector has any existing instances of that service. If a requested service instance doesn't yet exist, the injector makes one using the registered provider, and adds it to the injector before returning the service to Angular.

When all requested services have been resolved and returned, Angular can call the component's constructor with those services as arguments.

 

Providing services

You must register at least one provider of any service you are going to use. The provider can be part of the service's own metadata, making that service available everywhere, or you can register providers with specific modules or components. You register providers in the metadata of the service (in the @Injectable() decorator), or in the @NgModule() or @Component() metadata

  • By default, the Angular CLI command ng generate service registers a provider with the root injector for your service by including provider metadata in the @Injectable() decorator. The tutorial uses this method to register the provider of HeroService class definition. (something can be injected in there)

@Injectable({
 providedIn: 'root',
})

When you provide the service at the root level, Angular creates a single, shared instance of HeroService and injects it into any class that asks for it. Registering the provider in the @Injectable() metadata also allows Angular to optimize an app by removing the service from the compiled app if it isn't used.

  • When you register a provider with a specific NgModule, the same instance of a service is available to all components in that NgModule. To register at this level, use the providers property of the @NgModule() decorator (in app.module.ts)

@NgModule({
  providers: [
  BackendService,
  Logger
 ],
 ...
})
  • When you register a provider at the component level, you get a new instance of the service with each new instance of that component. At the component level, register a service provider in the providers property of the @Component() metadata.

@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

Inject services into services

by providing services at app.module level

 

Routing

in app-routing.module.ts

import {Routes,RouterModule} from "@angular/router";

// starting page
// localhost:4200/users, then component(action)
const appRoutes: Routes = [
    { path: '', component:HomeComponent }
    { path: 'users', component:UsersComponent }
    { path: 'servers', component:ServersComponent }
];

// register routes in NgModule
imports:[
    RouterModule.forRoot(appRoutes)
],
exports:[RouterModule]

 in app.module.ts

imports:[
    AppRoutingModule
]

in app.component.html

<!-- special directive -->
<!-- angular router load the component of currently selected route -->
<router-outlet> </router-outlet>

The Angular Router enables navigation from one view to the next as users perform application tasks.

client side routing: readiung urls and rerendering parts of  the page.

server side routing: handling incoming requests and sending back sth different.

nested routes

const appRoutes: Routes = [
    { path: '', component:HomeComponent }
    { path: 'users', component:UsersComponent }
    { path: 'servers', component:ServersComponent, children: [
        {path: ':id', component:ServerComponent}
        {path: ':id/edit', component: EditServersComponent}
    ] }
];
<!-- use this instead of <app-xxx> -->
<router-outlet> </router-outlet>
<!-- directive -->
routerLink = '/servers'

<!-- or -->
[routerLink] = "['/users']"

<!-- /users/:id/edit -->
[routerLink] = "['/users',5,'edit']"
<!-- navigation path -->
<!-- absolute path -->
/server
<!-- relative path -->
server
./server  <!-- same as above -->
../server <!-- go up one directory -->
<!-- dynamically set the CSS class, here "active" -->
routerLinkActive="active"
<!-- only the full path is matched -->
[routerLinkActiveOptions] = "{exact: true}"

The route path and parameters are available through an injected router service called the ActivatedRoute.

import { Router, ActivatedRoute } from '@angular/router';

// ActivatedRoute: currently active route
constructor(private router: Router, private route: ActivatedRoute) {}

this.router.navigate(['/servers']);

// give relative path
this.router.navigate(['servers'], {relativeTo: this.route});

pass parameters to routes and fetch

'users/:id'
constructor(private route: ActivatedRoute) {}

this.route.snapshot.params['id']

fetch route parameters reactively

// params is an observable, work with async task
// this is useful when params changes
this.route.params
.subscribe(
    (params: Params)=>{
        this.user.id = +params['id'];
        this.user.name = params['name'];
    }
)

Query params and fragments

// query params
?mode=editing&active=true

//fragments
#loading
<!-- pass queryParams together with routerLink -->
[queryParams] = "{mode: 'editing', active: 'true'}"

[fragment] = "'loading'"
<!-- OR -->
fragment = "loading"
// pass in programmatic way
this.router.navigate(['/servers',id,'edit'],{queryParams: {allowEdit:'1'}, fragment: 'loading'});

retrieve

import { ActivatedRoute } from '@angular/router';

// ActivatedRoute: currently active route
constructor(private route: ActivatedRoute) {}

this.route.queryParams.subscribe(
    (params: Params)=>{
        
    }   
);
this.route.fragment.subscribe();


// or use the snapshot

handling query parameters

// preserve or merge(overwrite)
this.router.navigate(['servers'], {relativeTo: this.route, queryParamsHandling: 'preserve'});

Redirect

By default, Angular matches paths by prefix.

To fix this behavior, you need to change the matching strategy to "full" :

{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' } 
// wildcard, catch all]
// put at last
const appRoutes: Routes = [
    { path: '**', redirectTo:'/not-found' }
];

Guards(canActivate,canActivateChild, canDeactivete)

run before the component loaded

CanActivate,canActivateChild

auth-guard.service.ts

import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

export class AuthGuard implements CanActivate {

    constructor(private authService: AuthService){}

    // can run either sync or async
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean{
        return this.authService.isAuthenticated()
        .then(
        (authenticated: boolean) => {
            if(authenticated) {
                return true;
            }else{
                this.router.navigate(['/']);
                return false;
            }
        )

    }
    canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean){

        return this.canActivate(route,state);
    }
}

in app-routing.module

{path: '', canActivate: [AuthGuard]}

// protect the child route
{path: '', canActivateChild: [AuthGuard]}

canDeactivete

control navigation

can-deactivate-guard.service.

// force the class to provide some logic
export interface CanComponentDeactivate {
    canDeactivate: ()  => Observable<boolean> | Promise<boolean> | boolean;

}

export class CanDeactivatedGuard implements CanDeactivate<CanComponentDeactivate> {
    canDeactivate(component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot,
nextState? : RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean{
    
    return component.canDeactivate():
}

in app-routing.module

{path: '', component:   , canDeactivate: [CanDeactivateGuard]}

in component

// implement the actual logic

canDeactivate()  => Observable<boolean> | Promise<boolean> | boolean{


}

pass static data to route

{path: '', component:  , data: {message: ''}}

use

constructor(private route: ActivatedRoute){}

ngOnInit(){
    //this.errorMessage = this.route.snapshot.data['message'];
    //possibly change when we are still on the page
    this.route.data.subscribe(
        (data: Data) ={
            this.errorMessage = data['message'];
    }
}

dynamic data (Resolver)

resolver: allow run some code before the router is rendered. does not decide the router should be rendered or not. whether the component should be loaded or not. it always render the component in the end but do some pre-loading.

interface Server {
    id: number;
    name: string;
    status: string;
}

export class ServerResolver implements Resolve<Server>
    constructor(private serversService: ){}

    resolve(route: AcrivatedRouteSnapshot, state: RouterStateSnapshot): Obervable<Server> | Promise<Server> | Server {
    return 
}

in app-routing.module

{path: '', component: , resolve: { server: NameResolver}},
this.route.data.subscribe((data: Data) => {
    this.server = data['server'];
}

 

Pipes

transform values

parametrizing pipes

<!-- use : -->  
xxx | date: 'fullDate' 

<!-- multiple :s for multiple parameters -->

chain pipes

|  |  |

create a custom pipe

can use ng generate pipe name

create a new file xxx.pipe.ts

update arrays or objects doesn't trigger the pipe

import { Pipe, PipeTransform } from "@angular/core";

@Pipe({
    name: 'shorten',
    // pipe recalculate when updated. default set to true.
    pure: false
})
export class ShortenPipe implements PipeTransform{
    transform(value: any, limit: number){
        return value.substr(0,limit);
    }
}

in app.module

declarations: [
    ShortenPipe
]

in html

xxx | shorten

//pass parameter 
xxx | shorten: 5

Async pipe

recognize promise, observable and output when async task finish

(XXX | async)

 

HTTP

in app.module.ts

import { HttpClientModule } from '@angular/common/http';

imports[HttpClientModule]

 in app.component.ts

import {HttpClient} from '@angular/common/http';

constructor(private http: HttpClient) {}

Send a POST Request

// http client take JS object and convert to JSON
// post returns an observable
this.http.post('url', postData).subscribe(responseData => {
    console.log(responseData);
});

GET data

this.http.get('url').subscribe(posts => {

});

DELETE data

// observable, can return it
return this.http.delete('url');

 

transform response data

use rxjs operator

import {map} from "rxjs/operators";
this.http
.get('url')
.pipe(map(responseData => {
    

    return posts
}))
.subscribe(posts => {

});

use types with httpclient

// store response body type
// avaliable on all requests
.get<{[key:string]: Post}>('url')

where Post is the model defined in post.model.ts

loading indicator

create a variable isLoading and set to true when start fetching,  and set to false when done (in subscribe funtion)

 

Use service for http Requests

  • If the component doesn't care about the response whether the request is done or not, there is no reason to subscribe in the component, just subscribe in the service.
  • if it does care about the response and the response status as it does for fetching posts, then in the service, return the observable, subscribe in the component.

 

Handling errors

//in subscribe
.subscribe(data=>{}, error => {});

use Subject for error handling

import { Subject } from 'rxjs';


error = new Subject<string>();


.subscribe(xxxxx, error => {
    this.error.next(error.message);

});

user catchError Operator

.catchError(errorRes => {
    return throwError(errorRes);
}

 

Set Headers, add query params

import {HttpHeaders, HttpParams} from '@angular/common/http';

.get('url',
{
    headers: new HttpHeaders({key: value}),
    params: new HttpParams().set('property','value')

})

// or multiple params
let serachParams = new HttpParams();
serachParams = searchParams.append('print','pretty');
searchParams = searchParams.append('custom','key');

 

Observe different types of responses

full response(body,headers,...)

.post<{name: string}>(
    'url',
    postData,
    {
        observe: 'response'
    }

)

 control the request status

import { tap } from 'rxjs/operators';

// execute some code without ordering the response
return this.http.delete( 'url', 
    {
        observe: 'events'
    }
).pipe(tap(event => {
    event.type === HttpEventType.Sent
    event.type === HttpEventType.Response
}));

Change the response body type

.post<{name: string}>(
    'url',
    postData,
    {
        responseType: 'json'
    }

)

 

Interceptors

simplify the  requests ( process the requests like middleware in nodejs)

create interceptors: e.g auth-interceptor-service.ts

import { HttpInterceptor, HttpRequest } from '@angular/common/http';

export class AuthInterceptorService implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler) {

        //manipulate request object
        const modifiedRequest = req.clone({headers: req.headers.append('xx','yy')});
        
        // return an observable
        // manipulate the response
        // let the request continue
        return next.handle(modifiedRequest).pipe();
    }
}

in app.module.ts

providers: [{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true}]

 

Authentication

 

Dynamic Components

Use *ngIf whenever possible.

 

Modules

 modules generally don't communicate with each other, unless export it to another module.

 

 

 

Lazy Loading

initially only load the root route, then only when visit the other modules, we load the module and the components belong to that module.

 

NgRx

state management in bigger Angular applications (replacement for services and subjects)

application state

So basically, any data, any information that controls what should be visible on the screen, that is state.

 

 

 

npm install --save @ngrx/store

An example of using NgRx to Add Ingredient to the shoppinglist.

define the action and reducer

in store folder

shopping-list.reducer.ts

Now that reducer as you learned here is just a function and now the important thing about that reducer function is that NgRx will automatically pass in the current state and an action it received, so it will execute this function whenever a new action is received and then this function executes and gets both the current state and the action that was received. In the reducer, I then use a switch case statement and this is a typical setup you see in the Redux and NgRx world, where we check the type of action we got because action is actually a Javascript object with a type property and depending on the type which we have, we return a new state and that is how a reducer function always works, data in data out, no asynchronous code, it's all synchronous, so we only have synchronous code in here and we always return a new object which will be used to replace the old state for this part of the application, so for the shopping list here and this return state is what NgRx will in the end register for the shopping list slice of the overall AppState, of the App store it manages here.

import { Ingredient } from '../../shared/ingredient.model';
import * as ShoppingListActions from './shopping-list.actions';

const initialState = {
  ingredients: [
    new Ingredient('Apple', 5),
    new Ingredient('kiwi', 10)
  ]
};

// set default value to function argument
export function shoppingListReducer(
  state = initialState,
  action: ShoppingListActions.AddIngredient
) {
  switch (action.type) {
    case ShoppingListActions.ADD_INGREDIENT:
      return {
        ...state,
        ingredients: [...state.ingredients, action.payload]
      };
    // for the initial state
    default:
      return state;
  }
}

shopping-list.actions.ts

we also added an actions file where we for one defined unique identifiers for all our actions, these are simply strings that identify each action and then the action itself is not just this identifier but it's a complete object based on classes we define in here. Each action needs to have a type property where we do store the string identifier for the action but in addition, we might also have a payload, so a property which can be set to attach data to that action and we needed

import {Action} from '@ngrx/store';
import { Ingredient } from '../../shared/ingredient.model';


export const ADD_INGREDIENT = 'ADD_INGREDIENT';

export class AddIngredient implements Action {
  readonly type = ADD_INGREDIENT;
  payload: Ingredient;
}


// typescript feature
// export the type
// multiple actions
export type ShoppingListActions = AddIngredient | AddIngredients;

in app.module

So we added NgRx to our application by including the store module and calling for root. For root then needs a map, so basically an object that tells NgRx which reducers we have in our application because all these reducers and the state they generate make up the store of this application, make up the NgRx store. Now we add a feature for that with an identifier of our choice and then the reducer for that feature.

imports: [
// structure of the store
StoreModule.forRoot({shoppingList: shoppingListReducer}),

]

use in shopping-list.component (to select state)

@Component({
})
export class ShoppingListComponent implements OnInit, OnDestroy {
  ingredients: Observable<{ingredients: Ingredient[]}>;
  private subscription: Subscription;

  
  // injectable
  // key name should match the one defined in app.module
  // the type of data stored in shoppinglist is what the reducer function yields
  constructor(
    private slService: ShoppingListService,
    private store: Store<{ shoppingList: { ingredients: Ingredient[] } }>
  ) { }

  // the purpose is to get access to the ingredient stored in the store
  ngOnInit() {
    // return an observable
    // select  the shopping list part of the global store
    this.ingredients = this.store.select('shoppingList');
  }
  onEditItem(index: number) {
    this.slService.startedEditing.next(index);
  }
  ngOnDestroy() {
  }
}

 in shopping-edit component

onSubmit(form: NgForm) {
  const value = form.value;
  const newIngredient = new Ingredient(value.name, value.amount);
  if (this.editMode) {
    this.slService.updateIngredient(this.editedItemIndex, newIngredient);
  } else {
    // this.slService.addIngredient(newIngredient);
    // dispatch the new actions to our store
    this.store.dispatch(new ShoppingListActions.AddIngredient(newIngredient));
  }
  this.editMode = false;
  form.reset();
}

The flow of this add ingredient process

if we add an ingredient, we dispatch this action which is defined in the actions file to that store, to the @ngrx/store, we only have one such store in the entire application, setup in the app-module and that store is aware of the shoppingListReducer because we have to create such a store with all the reducers that matter. So then the action automatically reaches all the reducers that our store knows, so in this case this one reducer here and in that reducer, the action is therefore passed in as a second argument and now we can check the different types of actions and react appropriately.

 

Effects

npm install --save @ngrx/effects

aysnc code and side effects

 

Router Store

npm install --save @ngrx/router-store

in app.module

StoreRouterConnectingModule.forRoot(),

give you a quick and easy way of reacting to different routing events and different data attached to these events.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章