跳转到主要内容

Angular is a popular web framework used for developing complex web applications. It provides developers with a powerful set of features and tools, but can also be quite complex to work with. In this article, we will explore some of the lesser-known Angular tricks that can help you become an Angular pro.

Use trackBy for performance optimization

Angular’s ngFor directive is a powerful tool for displaying lists of items, but it can also be a performance bottleneck if not used correctly. When rendering lists of items, Angular will often destroy and recreate the entire list when changes are made, which can be slow for large lists.

To improve performance, you can use the trackBy function to tell Angular how to identify each item in the list. By providing a unique identifier for each item, Angular can more efficiently update the DOM when changes are made.

Here’s an example of how to use trackBy:

<ul>
  <li *ngFor="let item of items; trackBy: trackById">{{ item }}</li>
</ul>

...

trackById(index: number, item: any): number {
  return item.id;
}

n this example, we’re using the trackById function to identify each item in the list by its id property. This helps Angular to more efficiently update the DOM when changes are made.

Use ViewChild to access child components

Angular provides a powerful mechanism for accessing child components from their parent components using the ViewChild decorator. This can be especially useful when you need to manipulate a child component from the parent, or when you need to pass data from the child to the parent.

Here’s an example of how to use ViewChild:

import { Component, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-parent',
  template: `
    <app-child></app-child>
    <button (click)="logChildData()">Log Child Data</button>
  `,
})
export class ParentComponent {
  @ViewChild(ChildComponent) childComponent: ChildComponent;

  logChildData() {
    console.log(this.childComponent.data);
  }
}

In this example, we’re using ViewChild to access the ChildComponent instance from the ParentComponent. We can then call methods or access properties on the child component from the parent.

Use ng-container to conditionally render content

Angular’s ngIf directive is a powerful tool for conditionally rendering content, but it can also be overused and lead to bloated templates. To keep your templates clean and concise, you can use the ng-container directive to conditionally render content without creating an additional element in the DOM.

Here’s an example of how to use ng-container:

<ng-container *ngIf="showContent">
  <p>This content will only be displayed if showContent is true.</p>
</ng-container>

n this example, we’re using ng-container to conditionally render a <p> element if showContent is true. The <ng-container> tag itself won't be rendered in the DOM, which can help keep your templates clean.

Use @HostBinding to dynamically set host element attributes

Angular provides the @HostBinding decorator to dynamically set attributes on the host element of a component. This can be useful for styling components based on their state, or for dynamically setting attributes such as role or aria-label for accessibility.

Here’s an example of how to use @HostBinding:

import { Component, HostBinding } from '@angular/core';

@Component({
  selector: 'app-my-component',
  template: '<p>My Component</p>',
})
export class MyComponent {
  @HostBinding('class.active') isActive = true;
}

In this example, we’re using @HostBinding to add the active class to the host element of the MyComponent component. The isActive property can be dynamically set to true or false to add or remove the class.

Use @ContentChildren to access content projection

Angular provides the @ContentChildren decorator to access content that has been projected into a component's template using the <ng-content> tag. This can be useful for building reusable components that allow users to provide their own content.

Here’s an example of how to use @ContentChildren:

import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { MyDirective } from './my.directive';

@Component({
  selector: 'app-my-component',
  template: `
    <ng-content></ng-content>
  `,
})
export class MyComponent implements AfterContentInit {
  @ContentChildren(MyDirective) myDirectives: QueryList<MyDirective>;

  ngAfterContentInit() {
    this.myDirectives.forEach((directive) => {
      console.log(directive);
    });
  }
}

n this example, we’re using @ContentChildren to access all instances of the MyDirective directive that have been projected into the MyComponent component using the <ng-content> tag. We can then iterate over the QueryList to access each instance of the directive.

Use pure pipes for efficient rendering

When you use pipes to transform data in Angular, the framework will automatically re-run the pipe whenever there’s a change detection cycle. This can be a problem if you’re working with large datasets, as it can cause performance issues.

To avoid this problem, you can use pure pipes. Pure pipes are special types of pipes that only run when the input value changes. This means that the pipe won’t run unnecessarily and can significantly improve the performance of your application.

Here’s an example of how to create a pure pipe:

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

@Pipe({
  name: 'myPurePipe',
  pure: true,
})
export class MyPurePipe implements PipeTransform {
  transform(value: string): string {
    console.log('Running pure pipe');
    return value.toUpperCase();
  }
}

Here’s an example of how to use the MyPurePipe pipe:

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

@Component({
  selector: 'app-my-component',
  template: `
    <p>{{ myString | myPurePipe }}</p>
  `,
})
export class MyComponent {
  myString = 'Hello, World!';
}

In this example, we’re using the myPurePipe pipe to transform the myString value to uppercase. Because the MyPurePipe pipe is a pure pipe, it will only run when the input value changes.

By using pure pipes, you can significantly improve the performance of your application when working with large datasets.

Here’s a more complex example of using pure pipes to sort a list of objects:

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

@Pipe({
  name: 'sortBy',
  pure: true,
})
export class SortByPipe implements PipeTransform {
  transform(value: any[], propertyName: string, direction: 'asc' | 'desc' = 'asc'): any[] {
    if (!value || value.length === 0) {
      return value;
    }

    return value.sort((a, b) => {
      const aValue = a[propertyName];
      const bValue = b[propertyName];

      if (aValue > bValue) {
        return direction === 'asc' ? 1 : -1;
      } else if (aValue < bValue) {
        return direction === 'asc' ? -1 : 1;
      } else {
        return 0;
      }
    });
  }
}

In this example, we’re creating a SortByPipe pipe that takes an array of objects and a property name, and sorts the array based on the values of the property. The direction parameter can be used to specify whether the array should be sorted in ascending or descending order.

Here’s an example of how to use the SortByPipe pipe:

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

interface Person {
  name: string;
  age: number;
}

@Component({
  selector: 'app-my-component',
  template: `
    <ul>
      <li *ngFor="let person of people | sortBy:'name': 'asc'">{{ person.name }}</li>
    </ul>
  `,
})
export class MyComponent {
  people: Person[] = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 },
    { name: 'Charlie', age: 20 },
  ];
}

In this example, we’re using the SortByPipe to sort a list of people by their name property in ascending order.

Dynamic template rendering with ngTemplateOutlet

Sometimes, you may need to dynamically render templates in your Angular components. This can be achieved using the ngTemplateOutlet directive. You can pass a template reference to this directive and dynamically render it in the component.

Here’s an example:

<ng-container *ngIf="condition">
  <ng-container *ngTemplateOutlet="myTemplate"></ng-container>
</ng-container>

<ng-template #myTemplate>
  <div>This template will be dynamically rendered if the condition is true</div>
</ng-template>

In this example, if the condition is true, the myTemplate will be dynamically rendered in the component.

Custom validators for reactive forms

When working with reactive forms in Angular, you may need to create custom validators for your form controls. This can be achieved by creating a custom validator function and passing it to the Validators array.

Here’s an example:

import { AbstractControl, ValidatorFn } from '@angular/forms';

export function forbiddenValueValidator(forbiddenValue: string): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const forbidden = control.value === forbiddenValue;
    return forbidden ? { forbiddenValue: { value: control.value } } : null;
  };
}

In this example, we’re creating a custom validator function forbiddenValueValidator that takes a forbiddenValue argument. We're returning a validator function that takes a form control as an argument and returns a validation error object if the control value matches the forbiddenValue.

You can then use this custom validator in your reactive form like this:

this.myForm = this.fb.group({
  myControl: ['', forbiddenValueValidator('forbidden')],
});

In this example, we’re using the forbiddenValueValidator to create a custom validator for the myControl form control.

Multi-level reactive forms

In some cases, you may need to create multi-level reactive forms in Angular. This means that you have a form that contains sub-forms. This can be achieved by nesting reactive form groups within a parent form group.

Here’s an example:

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

@Component({
  selector: 'app-multi-level-form',
  template: `
    <form [formGroup]="myForm" (ngSubmit)="onSubmit()">
      <div formGroupName="personalInfo">
        <label>Name</label>
        <input type="text" formControlName="name" />

        <label>Email</label>
        <input type="email" formControlName="email" />
      </div>

      <div formGroupName="address">
        <label>Street</label>
        <input type="text" formControlName="street" />

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

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

        <label>Zip Code</label>
        <input type="text" formControlName="zipCode" />
      </div>

      <button type="submit">Submit</button>
    </form>
  `,
})
export class MultiLevelFormComponent {
  myForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.myForm = this.fb.group({
      personalInfo: this.fb.group({
        name: ['', Validators.required],
        email: ['', [Validators.required, Validators.email]],
      }),
      address: this.fb.group({
        street: ['', Validators.required],
        city: ['', Validators.required],
        state: ['', Validators.required],
        zipCode: ['', [Validators.required, Validators.pattern('[0-9]{5}')]],
      }),
    });
  }

  onSubmit() {
    console.log(this.myForm.value);
  }
}

In this example, we’re creating a multi-level reactive form that contains a personalInfo group and an address group. Each group contains its own set of form controls.

Custom form control components

Sometimes, you may need to create custom form controls in Angular. This can be achieved by creating a custom form control component and implementing the ControlValueAccessor interface.

Here’s an example:

import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-custom-input',
  template: `
    <input type="text" [(ngModel)]="value" />
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomInputComponent),
      multi: true,
    },
  ],
})
export class CustomInputComponent implements ControlValueAccessor {
  value: string;
  onChange: (value: any) => void;

  writeValue(value: any): void {
    this.value = value;
  }

  registerOnChange(fn: (value: any) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {}

  setDisabledState(isDisabled: boolean): void {}
}

In this example, we’re creating a custom input component that implements the ControlValueAccessor interface. We're also providing the NG_VALUE_ACCESSOR token to the providers array of the component so that Angular knows that this component can be used as a form control.

You can then use this custom input component in a reactive form like this:

<form [formGroup]="myForm">
  <app-custom-input formControlName="myInput"></app-custom-input>
</form>

you might want to create a custom form control that allows users to input a phone number in a specific format, such as (123) 456–7890.

Here is an example of a custom form control for phone numbers:

import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-phone-number',
  templateUrl: './phone-number.component.html',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => PhoneNumberComponent),
      multi: true
    }
  ]
})
export class PhoneNumberComponent implements ControlValueAccessor {
  @Input() placeholder = '(XXX) XXX-XXXX';

  private onChange: (value: string) => void;
  private onTouched: () => void;
  private value: string;

  writeValue(value: string) {
    this.value = value;
  }

  registerOnChange(onChange: (value: string) => void) {
    this.onChange = onChange;
  }

  registerOnTouched(onTouched: () => void) {
    this.onTouched = onTouched;
  }

  onInput(value: string) {
    this.value = value;
    if (this.onChange) {
      this.onChange(value);
    }
    if (this.onTouched) {
      this.onTouched();
    }
  }
}

In this example, the PhoneNumberComponent implements the ControlValueAccessor interface and provides methods for setting and getting the value of the control. The component also adds an onInput() method to handle user input and update the value of the control.

To use this custom form control in your application, you can simply add it to your template like this:

<app-phone-number [(ngModel)]="phoneNumber"></app-phone-number>

When implementing a ControlValueAccessor, you can access the parent form by injecting the NgControl token in your component's constructor.

Here’s an example implementation of a custom form control that accesses the parent form’s properties:

import { Component, forwardRef, Injector } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';

@Component({
  selector: 'app-custom-control',
  template: `
    <input type="text" [(ngModel)]="value" />
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CustomControlComponent),
      multi: true
    }
  ]
})
export class CustomControlComponent implements ControlValueAccessor {

  private innerValue: any;

  constructor(private injector: Injector) { }

  // ControlValueAccessor interface implementation
  writeValue(value: any): void {
    this.innerValue = value;
  }

  // ControlValueAccessor interface implementation
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  // ControlValueAccessor interface implementation
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  onChange(value: any) {
    this.innerValue = value;
  }

  onTouched() { }

  get value() {
    return this.innerValue;
  }

  set value(value: any) {
    this.innerValue = value;
    this.onChange(value);

    // Access parent form's properties
    const parentForm = this.injector.get(NgControl).control.parent;
    console.log(parentForm.value); // Logs the parent form's value
  }

}

In this example, we’re injecting the Injector service to access the NgControl token, which provides us with a reference to the parent form's AbstractControl instance. We can then use this reference to access any of the parent form's properties, such as its value.

NgControl is an abstract base class in Angular that provides some common properties and methods for form controls. It's the base class for FormControlFormGroup, and FormArray, and it's used to build custom form controls.

NgControl provides a value property that represents the current value of the control, and it also provides an updateValueAndValidity() method that is used to update the control's value and validation status.

Here’s an example of a custom form control that extends NgControl:

import { Component, Input, OnInit, Optional, Self } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';

@Component({
  selector: 'app-custom-input',
  template: `
    <input type="text" [value]="value" (input)="onInput($event.target.value)" />
  `,
  providers: [{ provide: NgControl, useExisting: CustomInputComponent }]
})
export class CustomInputComponent implements ControlValueAccessor, OnInit {
  @Input() defaultValue: string;
  value: string;

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
  }

  ngOnInit() {
    if (this.defaultValue) {
      this.writeValue(this.defaultValue);
    }
  }

  writeValue(value: any) {
    this.value = value;
  }

  registerOnChange(fn: any) {
    this.onChange = fn;
  }

  registerOnTouched() {}

  onChange(value: string) {
    this.value = value;
    this.updateValueAndValidity();
  }

  onInput(value: string) {
    this.onChange(value);
  }

  private updateValueAndValidity() {
    if (this.ngControl != null) {
      this.ngControl.control.updateValueAndValidity();
    }
  }
}

In this example, we’re extending NgControl by providing it as a provider and using it as a base class for our CustomInputComponent. We're also injecting NgControl into our component's constructor and setting this.ngControl.valueAccessor to the instance of our component.

We’re then implementing the ControlValueAccessor interface to handle the value updates and provide callbacks to the parent form, and we're using ngOnInit() to set the default value of the input.

We’re also using updateValueAndValidity() to update the control's value and validation status after any changes to the value.

Another example of NgControl usage is when we need to access the parent form's properties from within a custom form control. We can do this by injecting NgControl into our component and using this.ngControl.control.parent to get a reference to the parent form's AbstractControl instance. We can then use this reference to access any of the parent form's properties, such as its value or validation status.

Overall, NgControl provides a lot of flexibility and functionality when building custom form controls in Angular.

Advanced routing techniques

In Angular, routing is an essential part of building single-page applications. Here are a few advanced routing techniques that can help you build more complex applications:

  • Route guards: Route guards are used to protect routes from unauthorized access. They can be used to check if the user is authenticated or has the required permissions to access a particular route.
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) {}

  canActivate(): boolean {
    if (this.authService.isAuthenticated()) {
      return true;
    } else {
      this.router.navigate(['/login']);
      return false;
    }
  }

}
  • Lazy loading: Lazy loading is a technique that allows you to load different parts of your application on-demand. This can help reduce the initial load time of your application and improve the overall performance.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
  { path: 'about', loadChildren: () => import('./about/about.module').then(m => m.AboutModule) },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
  • Route resolvers: Route resolvers are used to pre-fetch data before a route is activated. This can help improve the user experience by reducing the time it takes to load data.
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';

@Injectable()
export class UserResolver implements Resolve<any> {

  constructor(private userService: UserService) {}

  resolve(route: ActivatedRouteSnapshot) {
    return this.userService.getUserById(route.params.id);
  }

}

Progressive Web Apps (PWA)

Progressive Web Apps (PWA) are a set of technologies that allow web applications to behave more like native applications. PWAs can be installed on a user’s device and provide features such as offline support, push notifications, and background sync.

Angular provides built-in support for building PWAs. You can use the @angular/service-worker package to add service worker support to your application and make it installable as a PWA.

import { Component, OnInit } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor(private swUpdate: SwUpdate) {}

  ngOnInit() {
    if (this.swUpdate.isEnabled) {
      this.swUpdate.available.subscribe(() => {
        if (confirm('New version available. Load New Version?')) {
          window.location.reload();
        }
      });
    }
  }

}

Server-side rendering

Server-side rendering (SSR) is a technique that allows your Angular application to be rendered on the server before it’s sent to the client’s browser. This can help improve the initial load time of your application and improve its overall performance.

Angular provides built-in support for SSR. You can use the @nguniversal/express-engine package to render your application on the server.

import 'zone.js/dist/zone-node';
import { enableProdMode } from '@angular/core';
import { renderModuleFactory } from '@angular/platform-server';
import { AppServerModuleNgFactory } from './app/app.server.module.ngfactory';
import * as express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';

enableProdMode();

const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');

const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

app.engine('html', (_, options, callback) => {
  renderModuleFactory(AppServerModuleNgFactory, {
    document: template,
    url: options.req.url,
    extraProviders: [
      {
        provide: 'REQUEST',
        useValue: options.req,
      },
      {
        provide: 'RESPONSE',
        useValue: options.res,
      },
    ],
  }).then(html => {
    callback(null, html);
  });
});

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

app.get('*.*', express.static(DIST_FOLDER, { maxAge: '1y' }));

app.get('*', (req, res) => {
  res.render('index', { req });
});

Custom Decorators in Angular

In Angular, decorators are used to add functionality to classes. You can create custom decorators to add even more functionality to your Angular components, services, and other classes.

Let’s say we want to create a custom decorator that logs every time a method is called. We can define the decorator like this:

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Result: ${JSON.stringify(result)}`);
    return result;
  }

  return descriptor;
}

In this example, the logMethod decorator takes three arguments: the target class, the name of the method being decorated, and a property descriptor that describes the method. The decorator then logs the name of the method and its arguments before calling the original method, logs the result of the method, and returns the result.

We can then use the logMethod decorator to decorate any method in our Angular components or services:

@Component({
  selector: 'app-my-component',
  template: '<p>{{message}}</p>'
})
export class MyComponent {
  message = 'Hello, world!';

  @logMethod
  onClick() {
    this.message = 'Button clicked!';
  }
}

In this example, we use the @logMethod decorator to decorate the onClick() method in our MyComponent class. Now, every time the onClick() method is called, it will log the method name, arguments, and result to the console.

By creating custom decorators, you can add functionality to your Angular classes in a reusable and modular way. This allows you to keep your code clean and maintainable, while still adding powerful features to your application.

In conclusion, Angular is a powerful framework that can help you build complex and scalable web applications. By mastering these advanced techniques, you can take your Angular skills to the next level and become a pro.

if(youLikeMyContent){
  pleaseConsiderFollowingMe(😊);
}

标签