Angular Basics II - Content Projection & Router & Pipe & Validation
Component nesting is inevitable. For example, previously we added ImageSlider
(<app-image-slider>
,ScrollableTab
(<app-scrollable-tab>
) components in App
component (app.component.html
). We can use @Input()
and @Output
to realize the data transfer between nesting components or between components and outside world. However, over nesting can lead to complexity and redundancy in our codes. To avoid the redundant data and events brought by component nesting, we can use content projetion, router, directive and service.
1. Content Projection
Using ng-content
to realize content projection.
- what: dynamic content
- syntax:
<ng-content select="style classes / HTML tags / directives"></ng-content>
- possible scenarios: display dynamic contents / as a component container
For example:
<ng-content select="span"></ng-content>
<ng-content select=".special"></ng-content> <!-- special is class name -->
<ng-content select="[appGridItem]"></ng-content>
2. Router
The most important benefit Router brings to us is that it helps decouple independent components. Its definition includes:
- path
- component
- child router
And to use it, we need to import RouterModule
both forRoot
and forChild
. For example,
Next, we will see how to use Router in detail.
Practice 1: Router to Home Page
- First, create a file
app-routing.module.ts
. Use directiveng-router-appmodule
generating a router template. Then add a route.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeContainerComponent } from './home';
const routes: Routes = [
{ path: '', component: HomeContainerComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
In app.component.html
add a tag <router-outlet></router-outlet>
. Then, don’t forget to import AppRoutingModule
and HomeModule
in app.module.ts
.
Practice 2: Child Router in Home Page
We create a child router in the home page as follows. In file home-routing.module.ts
,
const routes: Routes = [
{
path: 'home',
component: HomeContainerComponent,
children: [
{
path: '',
redirectTo: 'hot',
pathMatch: 'full'
},
{
path: ':tabLink',
component: HomeDetailComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
Practice 3: Params in Router URL
- Configuration
{ path: ':tabLink', component: HomeDetailComponent }
- Activation: Two types
<!-- params --> <a [routerLink="['/home', tab.link]"]>...</a> <a [routerLink="['/home', tab.link, {name: 'val1'}]"]></a> <!-- query params --> <a [routerLink="['/home', tab.link, [queryParams]={name: 'val1'}]"]></a> <!-- add style for activated router link --> <!-- class name: active --> <a [routerLink]="[ 'grand' ]" routerLinkActive="active" [queryParams]="{name: 'tonny', gender: 'girl'}" >...</a> this.router.navigate(['home', tab.link]); this.router.navigate(['home', tab.link, {queryParams: {name: 'val1'}}]);
- Corresponding URL is like
http://localhost:4200/home/sports
http://localhost:4200/home/sports;name=val1
http://localhost:4200/home/sports?name=val1
- Read Params in URL
this.route.paramsMap.subscribe(param => {...}); this.route.queryParamsMap.subscribe(param => {...});
3. Pipe
Pipe can help transform values in the View. For example, 1234
-> $ 1234
. The following image shows pipes provided by Angular.
How to use it ? Using |
, e.g.
<p> {\{ obj | json }} </p>
<p> {\{ date | date: 'MM-dd' }} </p>
<p> {\{ date | date: 'yyyy-MM-dd' }} </p>
<p> {\{ price | currency }} </p>
<p> {\{ price | currency: 'CNY' }} </p>
<p> {\{ price | currency: 'CNY': 'symbol': '4.0-2' }} </p>
<!-- data = [1, 2, 3, 4, 5] -->
<!-- output is [2, 3] -->
<!-- slice: left: right (left close, right open) -->
<p> {\{ data | slice: 1:3 }} </p>
Practice: customized pipe
We can also create our customized pipe. In the following example, we create an ago-pipe, which returns ... units ago
. First, use ng-pipe
generating the pipe template.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'appAgo'})
export class AgoPipe implements PipeTransform {
transform(value: any): any {
if (value) {
const seconds = Math.floor((+new Date - +new Date(value)) / 1000);
if (seconds < 30) {
return 'A few seconds ago';
}
const intervals = {
years: 3600 * 24 * 365,
months: 3600 * 24 * 30,
weeks: 3600 * 24 * 7,
days: 3600 * 24,
hours: 3600,
minutes: 60,
secondes: 1
};
let counter = 0;
for (const unitName in intervals) {
if (intervals.hasOwnProperty(unitName)) {
const unitValue = intervals[unitName];
counter = Math.floor(seconds / unitValue);
if (counter > 0) {
return `${counter} ${unitName} ago`;
}
}
}
}
return value;
}
}
Then we can use the created ago-pipe
.
<!-- html file -->
<p> {\{ date | agoPipe }} </p>
<!-- according ts file -->
export class HomeGrandComponent implements OnInit {
date: Date;
constructor() { }
ngOnInit() {
this.date = this.minusDays(new Date(), 60);
}
minusDays(date: Date, days: number) {
const result = new Date(date);
result.setDate(result.getDate() - days);
return result;
}
}
Then we can see 2 months ago in the view.
4. Dependency Injection
The procedure of how to use DI is shown as follows.
The instance created through DI is singleton. Say if component A and component B both inject a service, they are acturally using the same instance.
In angular, if we want to make some class injectable, we just need to add decorator @Injectable()
above that class. For example,
@Injectable
class Product {
constructor(private name: string, private color: string) {}
// the above line combine the field declaration and constructor together.
}
@Injectable
class PurchaseOrder {
private amount: number;
constructor(private product: Product) {}
}
Then declare the injectable class / factory using Injector.create();
as follows.
export class HomeGrandComponent implements OnInit {
// some codes
ngOninit() {
const injector = Injector.create({
providers: [
{
provider: Product,
//useClass: Product // inject a class
// or we can directly inject a specific object using useFactory
useFactory: () => {
return new Product("molten edge", "golden")
},
deps: [] // the dependencies of this provider
},
{
provider: PurchaseOrder,
useCass: PurchaseOrder,
deps: [Product]
}
]
});
console.log(injector.get(Product));
console.log(injector.get(PurchaseOrder));
}
}
Then we can get the following results in the console.
Practice 1: home service
It’s better to create serives for each module to do the data transfer job. Say we want to decouple data and components. DI provides a great way to do this. Next we will create a home service and see how to use DI to decouple data and our components.
First, use ng-service
to generate a service template. In this service, we put our data.
import { Injectable } from '@angular/core';
import { TopMenu, ImageSlider, Channel } from 'src/app/shared/components';
@Injectable()
export class HomeService {
menus: TopMenu[] = [...];
imageSliders: ImageSlider[] = [...];
getTabs() { return this.menus; }
getBanners() { return this.imageSliders; }
getChannels() { return this.channels; }
}
Future, we will get these data throug HTTP.
To use this service, don’t forget to declare this service in .module.ts
.
@NgModule({
declarations: [...],
providers: [
HomeService
],
imports: [...]
})
Now we can employ DI to get the data.
topMenus: TopMenu[]: [];
constructor(private service: HomeService) {}
ngOnInit() { this.topMenus = this.service.getTabs(); }
Practice 2: transfer a string value
We can also just transfer a value using DI.
ngOnInit() {
const injector = new Injector.create({
providers: [{
{
provider: 'baseUrl',
useValue: 'http://localhost'
}
}]
})
}
We can use a string 'baseUrl'
to be the indicator of a injectable value, however in large project this may cause conflict. So Angular wants us to use Token as the string type indicator.
ngOnInit() {
const token = new InjectionToken<string>('baseUrl');
const injector = new Injector.create({
providers: [{
{
provide: token,
useValue: 'http://localhost'
}
}]
})
}
How to transfer a string to other components?
- First export it and also do the declaration in
.module.ts
.export const token = new InjectionToken<string>('baseUrl');
providers: [HomeService, { provide: token, useValue: 'http://localhost' }]
- Then we can use it as follows.
constructor(
@Inject(token) private baseUrl: string
) { }
5. Validation
- What? view is refreshed when the data is changed.
- When? when validation is triggered?
- browser events (e.g. click, mouseover, keyup,… )
setTimeout()
andsetInterval()
HTTP
request
- How? Compare current status and new status.
The validation procedure is synchronous. And it is done twice by Angular framework.
As the above pic shows validation procedure is done before AfterViewChecked
and AfterViewInit
hook, so we should change property values in functions ngAfterViewChecked()
and ngAfterViewInit()
. For example, the following code will cause Error.
<span [textContent]="time | HH:mm:ss:SSS"></span>
<!-- or <span></span> -->
public get time(): number {
return this.\_time;
}
ngAfterViewChecked(): void {
this.\_title = 'hello';
}
How to solve this problem? We can use NgZone, set the change to be outside Angular.
\_time;
public get time(): number {
return this.\_time;
}
constructor(private ngZone: NgZone) {}
ngAfterViewChecked(): void {
this.ngZone.runOutsideAngular(() => {
setInterval( () => {
this.\_time = Date.now();
}, 200);
});
}
Then add a button to trigger Validation.
The above code still has limitation. We can not read time continuously. If we want to view the change in every millisecond, we’d better not use binding. Instead, operating DOM element is an effecient solution.
<span #timeRef></span>
@ViewChild('timeRef') timeRef: ElementRef;
ngAfterViewChecked(): void {
setInterval( () => {
this.timeRef.nativeElement.innerText = Date.now();
} , 200);
}
Operating DOM element directly is not recommended. Instead, we can use Renderer2
.
constructor(private rd2: Renderer2) {}
@ViewChild('timeRef') timeRef: ElementRef;
ngAfterViewChecked(): void {
setInterval( () => {
this.rd2.setProperty(this.timeRef.nativeElement,
'innerText',
Date.now());
} , 200);
}
We can also pipes in class to format Date.
constructor(private rd2: Renderer2) {}
@ViewChild('timeRef') timeRef: ElementRef;
ngAfterViewChecked(): void {
setInterval( () => {
this.rd2.setProperty(this.timeRef.nativeElement,
'innerText',
formatDate(Date.now(), HH:mm:ss:SSS), 'zh-Hans');
} , 200);
}