Project - Groupbuy App Part IV

prev

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 directive ng-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() and setInterval()
    • 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);
}

   Reprint policy


《Project - Groupbuy App Part IV》 by Tong Shi is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Three Equal Parts Three Equal Parts
LeetCode Q 927 - Three Equal PartsGiven an array A of 0s and 1s, divide the array into 3 non-empty parts such that all
2019-06-27 Tong Shi
Next 
Valid Boomerang Valid Boomerang
LeetCode Q 1037 - Valid BoomerangA boomerang is a set of 3 points that are all distinct and not in a straight line.Give
2019-06-26 Tong Shi
  TOC