สรุปฟีเจอร์ตั้งแต่ Angular 13 ถึง Angular 16

5/11/2566

thirteen-to-sixteen
Photo by Benjamin Brunner on Unsplash

ก่อนที่ Angular 17 จะออกมาในไม่กี่วันนี้ เรามาทบทวนกันดีกว่าว่าตั้งแต่ Angular 13 จนถึง Angular 16 มีฟีเจอร์อะไรที่น่าสนใจบ้าง


สารบัญ


Angular 13

thirteen
Photo by Kind and Curious on Unsplash

เอา View Engine ออก

ตั้งแต่ Angular 13 จะไม่มี View Engine อีกแล้วเพื่อหลีกทางให้ Ivy based ฟีเจอร์ในอนาคต และเพื่อการคอมไพล์ที่เร็วขึ้น เมื่อไม่มี View Engine แล้วก็จะลดการพึ่งพา ngcc (Angular Compatible Compiler) ได้ด้วย

เปลี่ยนไปใช้ Angular Package Format (APF)

ตั้งแต่ Angular 13 จะทำการเอา output format แบบเก่าซึ่งรวมถึง View Engine metadata ออก ไลบรารี่ที่สร้างด้วย APF จะไม่จำเป็นต้องใช้ ngcc อีกต่อไป ส่งผลให้ package มีขนาดเล็กลงและ execute ได้เร็วขึ้น

อัพเดท Component API

API ในการสร้าง Dynamic Component ได้ถูกทำให้ง่ายขึ้นคือ ComponentFactoryResolver ไม่จำเป็นต้องถูก inject ใน Constructor อีกแล้วตามตัวอย่างด้านล่างนี้ครับ

// ก่อน Angular 13
@Directive({ … })
export class MyDirective {
    constructor(private viewContainerRef: ViewContainerRef,
                private componentFactoryResolver:
                        ComponentFactoryResolver) {}
    createMyComponent() {
        const componentFactory = this.componentFactoryResolver.
                             resolveComponentFactory(MyComponent);

        this.viewContainerRef.createComponent(componentFactory);
    }
}

// Angular 13
@Directive({ … })
export class MyDirective {
    constructor(private viewContainerRef: ViewContainerRef) {}
    createMyComponent() {
        this.viewContainerRef.createComponent(MyComponent);
    }
}

จบการซัพพอร์ต IE11

ด้วยการที่ไม่ต้องซัพพอร์ต IE11 อีกต่อไปจะทำให้สามารถใช้ฟีเจอร์ใหม่ๆ ของ modern browser ได้เช่น CSS variable หรือ web animation นอกจากนี้แอพจะมีขนาดเล็กลงและโหลดได้เร็วขึ้นเพราะสามารถเอา polyfill ของ IE11 ออกได้แล้ว

เพิ่มความสามารถของ Angular CLI

project ใหม่ที่สร้างด้วย Angular 13 จะถูก enable Persistent build cache เป็น default ซึ่งจะช่วยให้ build เร็วขึ้นถึง 68% เลยทีเดียว

อัพเดท dependency ของ framework

อัพเดท RxJS เป็นเวอร์ชั่น 7.4 และอัพเดท TypeScript เวอร์ชั่นเป็น 4.4

ปรับปรุง Angular Tests

TestBed ได้ถูกปรับปรุงให้สามารถ clean up ทุกครั้งหลัง test ทำให้กิน memory น้อยลงและรัน test ได้เร็วขึ้น

Angular Material Component

Angular Material Component ถูกปรับปรุงให้รองรับเรื่อง Accessibility และรองรับมาตรฐาน a11y ได้ดีขึ้น

Community contributions

ในส่วนของ Community ที่ช่วยกัน contribute มีสิ่งที่น่าสนใจดังนี้

  • Dynamically enable/disable validators
  • Restore history after canceled navigation
  • Making the SwUpdate API a little more ergonomic
  • Language Service config to enable auto-apply optional chaining on nullable symbol
  • Router emit activate/deactivate events when an outlet gets attached/detached

Angular 14

fourteen
Photo by Jeff Cooper on Unsplash

Standalone Component

เพื่อที่จะลดการใช้งาน NgModules ใน Angular 14 ได้เพิ่ม Standalone Component เข้ามาใน developer preview ซึ่งมีหน้าตาประมาณนี้

import { Component } from '@angular/core'
import { CommonModule } from '@angular/common' // มี NgIf และ TitleCasePipe ใน CommonModule
import { bootstrapApplication } from '@angular/platform-browser'

import { MatCardModule } from '@angular/material/card'
import { ImageComponent } from './app/image.component'
import { HighlightDirective } from './app/highlight.directive'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    ImageComponent,
    HighlightDirective, // import standalone Components, Directives และ Pipes
    CommonModule,
    MatCardModule, // และ NgModules
  ],
  template: `
    <mat-card *ngIf="url">
      <app-image-component [url]="url"></app-image-component>
      <h2 app-highlight>{{ name | titlecase }}</h2>
    </mat-card>
  `,
})
export class ExampleStandaloneComponent {
  name = 'emma'
  url = 'www.emma.org/image'
}

// Bootstrap Angular แอพโดยใช้ `ExampleStandaloneComponent` เป็น root component.
bootstrapApplication(ExampleStandaloneComponent)

เราสามารถสร้าง standalone component ด้วยการเซ็ต flag standalone: true ทำให้เราสามารถ import Component, Directive, Pipe หรือ Module เข้ามาใน Component โดยไม่ต้องพึ่ง @NgModule()

Typed Angular Form

Angular Github Top issue (ในสมัยนั้น) ก็คืออยากให้ Angular Form เป็น Fully Typed ซึ่งใน Angular 14 ก็ได้ทำการปิด issue นี้เป็นที่เรียบร้อย ทำให้ Angular Form ซัพพอร์ต strict typing ซักที โดยเฉพาะเคสที่เป็น nested

const cat = new FormGroup({
  name: new FormGroup({
    first: new FormControl('Barb'),
    last: new FormControl('Smith'),
  }),
  lives: new FormControl(9),
})

// เช็ค type ของ form value
// TS Error: Property 'substring' does not exist on type 'number'.
let remainingLives = cat.value.lives.substring(1)

// Optional และ required control ถูกเช็ค
// TS Error: No overload matches this call.
cat.removeControl('lives')

// FormGroups รู้ถึง child control
// name.middle is never on cat
let catMiddleName = cat.get('name.middle')

ถ้าเราทำการ ng update มาจากเวอร์ชั่นเก่า Form จะถูกแทนที่ด้วย Untyped เวอร์ชั่นดังนี้

// form ใน v13
const cat = new FormGroup({
   name: new FormGroup(
      first: new FormControl('Barb'),
      last: new FormControl('Smith'),
   ),
   lives: new FormControl(9)
});

// form ใน v14 หลังจากรัน `ng update`
const cat = new UntypedFormGroup({
   name: new UntypedFormGroup(
      first: new UntypedFormControl('Barb'),
      last: new UntypedFormControl('Smith'),
   ),
   lives: new UntypedFormControl(9)
});

ซึ่งก็จะเป็นหน้าที่ของเราในการอัพเดท type ให้ถูกต้องแล้วค่อยเปลี่ยนไปใช้ Typed Form แบบปกติครับ

Page Title Accessibility

ใน Angular 14 เราสามารถใส่ page title ลงไปตอนสร้าง route ได้แล้ว และยังสามารถ handle เคสที่ซับซ้อนได้โดยการ custom TitleStrategy ครับ

const routes: Routes = [{
  path: 'home',
  component: HomeComponent
}, {
  path: 'about',
  component: AboutComponent,
  title: 'About Me'  // <-- Page title
}];

@Injectable()
export class TemplatePageTitleStrategy extends TitleStrategy {
  override updateTitle(routerState: RouterStateSnapshot) {
    const title = this.buildTitle(routerState);
    if (title !== undefined) {
      document.title = `My App - ${title}`;
    } else {
      document.title = `My App - Home`;
  };
};

@NgModule({

  providers: [{provide: TitleStrategy,  useClass: TemplatePageTitleStrategy}]
})
class MainModule {}

Angular CLI ปรับปรุงใหม่

ng completion

Angular CLI เวอร์ชั่น 14 จะเป็น real-time type-ahead autocompletion ดูตัวอย่างจากในรูปได้เลย

ng completion ใน Angular 14

ng analytics

command ng analytics นี้จะช่วยเซ็ต analytic setting และโชว์ analytic information

ng analytics ใน Angular 14

ng cache

command ng cache จะใช้จัดการ cache และโชว์ cache information

ng cache ใน Angular 14

อัพเดทอื่นๆ

  • Extended developer diagnostics
  • Tree-shakeable error messages
  • TypeScript 4.7
  • Bind to protected component members
  • Optional injectors in Embedded Views
  • NgModel OnPush
  • CDK Menu และ Modal stable แล้ว
  • hasHarness and getHarnessOrNull in Component Test Harnesses
  • Angular DevTools ใช้แบบ offline ได้แล้วมีบน Firefox แล้ว
  • Experimental ESM Application Builds

Angular 15

fifteen
Photo by Waldemar on Unsplash

Standalone API ออกจาก developer preview แล้ว

Standalone Component ได้ถูกเปิดให้ใช้ใน developer preview ตั้งแต่ Angular 14 ซึ่งใน Angular 15 ตัว Standalone API ก็เข้าสู่สถานะ stable แล้วครับ เราสามารถสร้าง Standalone Component ได้ตามตัวอย่างด้านล่างนี้ได้เลย

import { bootstrapApplication } from '@angular/platform-browser'
import { ImageGridComponent } from './image-grid'

@Component({
  standalone: true,
  selector: 'photo-gallery',
  imports: [ImageGridComponent],
  template: ` … <image-grid [images]="imageList"></image-grid> `,
})
export class PhotoGalleryComponent {
  // component logic
}

bootstrapApplication(PhotoGalleryComponent)

Router เป็น tree-shakable standalone APIs

เราสามารถสร้าง multi-route แอพด้วย router standalone api แบบใหม่ โดยประกาศ root route แบบนี้

export const appRoutes: Routes = [
  {
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.routes').then((routes) => routes.lazyRoutes),
  },
]

หลังจากนั้นก็สร้าง lazy route แบบนี้

import { Routes } from '@angular/router'

import { LazyComponent } from './lazy.component'

export const lazyRoutes: Routes = [{ path: '', component: LazyComponent }]

สุดท้ายก็ register root route ใน bootstrapApplication

bootstrapApplication(AppComponent, {
  providers: [provideRouter(appRoutes)],
})

ซึ่งตัว provideRouter เป็น tree-shakable หมายความว่าฟีเจอร์ไหนที่ไม่ได้ใช้จะถูกเอาออกตอนขั้นตอนการ build นั่นเอง ส่งผลให้ bundle มีขนาดเล็กลงด้วยครับ

Directive composition API

Directive composition API ทำให้เราเพิ่มความสามารถของ host element ด้วย directive ซึ่งเป็นการ reuse code แบบนึง แต่ว่าตัว Directive composition API จะใช้ได้กับ standalone component เท่านั้นนะครับ

ตัวอย่างการใช้ Directive composition API

@Component({
  selector: 'mat-menu',
  hostDirectives: [
    HasColor,
    {
      directive: CdkMenu,
      inputs: ['cdkMenuDisabled: disabled'],
      outputs: ['cdkMenuClosed: closed'],
    },
  ],
})
class MatMenu {}

Image directive เข้าสู่สถานะ stable แล้ว

Image directive ถูกประกาศเป็น developer preview ตั้งแต่ Angular 14.2 และถูกเปลี่ยนเป็นสถานะ stable ใน Angular 15 พร้อมเพิ่มฟีเจอร์ใหม่คือ

  • Automatic srcset generation สร้าง srcset attribute ให้โดยอัตโนมัติ เพื่อช่วยลดเวลาการ download
  • Fill mode [experimental] สามารถกำหนดให้รูปภาพ fill parent container ซึ่งช่วยให้เราไม่ต้องกำหนด size ของรูป

เราสามารถใช้ NgOptimizedImage ได้ดังนี้

import { NgOptimizedImage } from '@angular/common';

// เพิ่มใน NgModule
@NgModule({
  imports: [NgOptimizedImage],
})
class AppModule {}

// หรือเพิ่มไปใน standalone Component
@Component({
  standalone: true
  imports: [NgOptimizedImage],
})
class MyStandaloneComponent {}

วิธีใช้ก็คือเปลี่ยน attribute src เป็น ngSrc และกำหนด attribute priority สำหรับรูปที่เป็น LCP

Functional router guards

เราสามารถเขียน router guard แบบเป็น function ได้แล้วแบบนี้

// ก่อน Angular 15
@Injectable({ providedIn: 'root' })
export class MyGuardWithDependency implements CanActivate {
  constructor(private loginService: LoginService) {}

  canActivate() {
    return this.loginService.isLoggedIn()
  }
}

const route = {
  path: 'somePath',
  canActivate: [MyGuardWithDependency],
}

// Angular 15
const route = {
  path: 'admin',
  canActivate: [() => inject(LoginService).isLoggedIn()],
}

Router default imports

ยิ่งไปกว่านั้นเรายังเขียน lazy load ใน router ได้ง่ายขึ้นแบบนี้

// ก่อน Angular 15
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file').then(m => m.LazyComponent),
}

// Angular 15
{
  path: 'lazy',
  loadComponent: () => import('./lazy-file'),
}

Stack trace ดีขึ้น

ทางทีม Angular ได้ร่วมมือกับทีม Chrome DevTool เพื่อพัฒนาให้ Stack trace อ่านง่ายขึ้นแบบนี้

// ก่อน Angular 15
ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:25:1)
    at _ZoneDelegate.invoke (zone.js:372:26)
    at Object.onInvoke (core.mjs:26378:33)
    at _ZoneDelegate.invoke (zone.js:371:52)
    at Zone.run (zone.js:134:43)
    at zone.js:1275:36
    at _ZoneDelegate.invokeTask (zone.js:406:31)
    at resolvePromise (zone.js:1211:31)
    at zone.js:1118:17
    at zone.js:1134:33

// Angular 15
ERROR Error: Uncaught (in promise): Error
Error
    at app.component.ts:18:11
    at fetch (async)
    at (anonymous) (app.component.ts:4)
    at request (app.component.ts:4)
    at (anonymous) (app.component.ts:17)
    at submit (app.component.ts:15)
    at AppComponent_click_3_listener (app.component.html:4)

จะเห็นได้ว่าเมื่ออ่าน Stack trace แบบใหม่เราจะรู้ได้ทันทีว่า error มาจาก AppComponent

อัพเดทอื่นๆ

  • refactor Angular Material Component ให้ based on Material Design Component for Web สำเร็จแล้ว
  • range selection support in the slider
  • CDK Listbox
  • Improvements in the experimental esbuild support
  • Automatic imports in language service
  • สร้าง standalone component ด้วยคำสั่ง ng g component --standalone
  • deprecate providedIn: 'any' และ providedIn: NgModule
  • หยุด publish new release ของ @angular/flex-layout

อัพเดทจาก Community Contribution

  • Provide an ability to configure default options for DatePipe
  • Add <link> preload tag for priority images during SSR

Angular 16

sixteen
Photo by Julien Photo on Unsplash

ทบทวนเรื่อง Reactivity

ใน Angular 16 ทางทีมได้ออก reactivity model แบบใหม่มาใน developer preview ซึ่งตัวใหม่นี้จะ backward compatible และใช้งานได้กับ code เก่า ซึ่งจะช่วยให้

  • มี runtime performance ดีขึ้นและลดการคำนวณตอนที่มีการ change detection และน่าจะข่วยให้มี INP หนึ่งใน Core Web Vitals ที่ดีขึ้นอีกด้วย
  • ทำให้ reactivity model ง่ายขึ้น เข้าใจ dependency และ data flow ได้ดีขึ้น
  • สามารถปรับจูน reactivity ได้ดีขึ้นซึ่งจะทำให้สามารถเช็คการเปลี่ยนแปลงเฉพาะ component ที่มีการเปลี่ยนแปลงจริงๆเท่านั้น
  • ทำให้ Zone.js เป็น optional
  • สามารถใช้ computed property โดยที่ไม่ต้องทำการคำนวณใหม่ในทุกรอบ change detection
  • ใช้งานแบบไร้รอยต่อได้กับ RxJS

Angular 16 จะเพิ่ม package signals เข้ามาใน @angular/core และเพิ่ม @angular/core/rxjs-interop เข้ามา

Angular Signals

เราสามารถใช้ Angular Signals ได้แบบนี้

@Component({
  selector: 'my-app',
  standalone: true,
  template: ` {{ fullName() }} <button (click)="setName('John')">Click</button> `,
})
export class App {
  firstName = signal('Jane')
  lastName = signal('Doe')
  fullName = computed(() => `${this.firstName()} ${this.lastName()}`)

  constructor() {
    effect(() => console.log('Name changed:', this.fullName()))
  }

  setName(newName: string) {
    this.firstName.set(newName)
  }
}

Code ด้านบนจะทำการสร้าง computed value ที่ชื่อว่า fullName ซึ่งมี dependency คือ firstName และ lastName และเรายังทำการสร้าง effect ที่จะรันทุกรอบเมื่อ dependency มีการเปลี่ยนแปลง ซึ่งในที่นี้ก็คือ fullName (ซึ่งจะขึ้นกับ firstName และ lastName อีกที)

RxJS interoperability

เราสามารถแปลง signal เป็น observable ได้แบบนี้

import { toObservable } from '@angular/core/rxjs-interop';

@Component({...})
export class App {
  count = signal(0);
  count$ = toObservable(this.count);

  ngOnInit() {
    this.count$.subscribe(() => ...);
  }
}

และเรายังสามารถแปลง observable เป็น signal ได้แบบนี้

import { toSignal } from '@angular/core/rxjs-interop'

@Component({
  template: ` <li *ngFor="let row of data()">{{ row }}</li> `,
})
export class App {
  dataService = inject(DataService)
  data = toSignal(this.dataService.data$, [])
}

Server-side rendering และ hydration

จากการทำการสำรวจพบว่า Server-side rendering เป็นหนึ่งในส่วนที่อยากให้มีการปรับปรุงมากที่สุด ทางทีมได้ร่วมกับทีม Chrome Aurora ได้ทำการพัฒนาและได้ออก developer preview full app non-destructive hydration ออกมาครับ

ด้วยการมาของ full app non-destructive hydration จะทำให้ Angular ไม่จำเป็นต้อง rerender ใหม่หมดทุกรอบ แต่จะทำการมองหา node ที่มีอยู่แล้วและทำการ attach event ไปที่ node นั้นแทน ซึ่งจะช่วยให้

  • ไม่มี content flickering ให้ user เห็น
  • มี Web Core Vitals ดีขึ้น
  • นำไปสู่การทำ progressive lazy route hydration ในอนาคต
  • integrate กับ app ที่มีอยู่แล้วได้ง่าย
  • ค่อยๆ adopt hydration ด้วย ngSkipHydration attribute ใน template เพื่อทำ manual DOM manipulation

จากการทดสอบเบื้องต้นพบว่า Largest Contentful Paint ดีขึ้นถึง 45% เลยทีเดียว

สามารถเริ่มใช้งานง่ายๆ แบบนี้

import {
  bootstrapApplication,
  provideClientHydration,
} from '@angular/platform-browser';

...

bootstrapApplication(RootCmp, {
  providers: [provideClientHydration()]
});

นอกจากนี้ยังมีการ ng add schematics สำหรับเพิ่ม server-side rendering ไปยัง project ที่ใช้ standalone API และยังมีซัพพอร์ต Content Security Policy สำหรับ inline style

ปรับปรุง Tooling สำหรับ standalone components directives และ pipes

ทางทีมได้เพิ่ม schematics สำหรับ migrate ไปเป็น standalone API โดยใช้คำสั่งแบบนี้

ng generate @angular/core:standalone

นอกจากนั้นเรายังสามารถสร้าง project ให้เป็น standalone ตั้งแต่ต้นได้ดังนี้

ng new --standalone

นอกจากนี้เรายังสามารถ config Zone.js กับ standalone API ได้ด้วยแบบนี้

bootstrapApplication(App, {
  providers: [provideZoneChangeDetection({ eventCoalescing: true })],
})

พัฒนา developer tooling ให้ทันสมัย

Developer preview สำหรับ esbuild-based build system

ใน Angular 16 ทางทีมได้ออก developer preview สำหรับ esbuild based build system ซึ่งเบื้องต้นพบว่ามีผลที่ดีขึ้นถึง 72% สำหรับ cold production build

ng serve จะใช้ Vite สำหรับ development server และ esbuild จะอยู่เบื้องหลังทั้ง development และ production build

เราสามารถทดลองใช้ Vite และ esbuild ได้ด้วยเพิ่ม config ใน angular.json แบบนี้

...
"architect": {
  "build": {                     /* Add the esbuild suffix  */
    "builder": "@angular-devkit/build-angular:browser-esbuild",
...

อัพเดทอื่นๆ ของ tooling

  • unit test ดีขึ้นด้วย Jest และ Web Test Runner
  • Autocomplete imports ใน templates
  • TypeScript 5.0
  • เพิ่ม service worker และ app shell ซัพพอร์ตสำหรับ standalone app

ปรับปรุง Developer Experience

Required Input

เราสามารถกำหนด input ให้เป็น required ได้แล้วแบบนี้

@Component(...)
export class App {
  @Input({ required: true }) title: string = '';
}

ส่ง router data เป็น component inputs

เราสามารถ bind router data ให้เป็น component input ได้แล้วแบบนี้

const routes = [
  {
    path: 'about',
    loadComponent: import('./about'),
    resolve: { contact: () => getContact() }
  }
];

@Component(...)
export class About {
  // The value of "contact" is passed to the contact input
  @Input() contact?: string;
}

ngOnDestroy ที่ flexible ขึ้น

เราสามารถรันสิ่งที่ต้องการให้รันใน ngOnDestroy นอก component ได้แล้วแบบนี้

import { Injectable, DestroyRef } from '@angular/core';

@Injectable(...)
export class AppService {
  destroyRef = inject(DestroyRef);

  destroy() {
    this.destroyRef.onDestroy(() => /* cleanup */ );
  }
}

Self closing tag

เราสามารถใช้ Self closing tag (แบบ React) ได้แล้วแบบนี้

<!-- ก่อน Angular 16 -->
<super-duper-long-component-name [prop]="someVar"></super-duper-long-component-name>

<!-- ตั้งแต่ Angular 16 -->
<super-duper-long-component-name [prop]="someVar" />

อัพเดทจาก Community Contribution

  • The extended diagnostics for proper use of ngSkipHydration
  • The introduction of provideServiceWorker to enable usage of the Angular service worker without NgModules

สรุป

จะเห็นได้ว่าตั้งแต่ Angular 13 จนมาถึง Angular 16 มีการพัฒนาและปรับปรุงให้ดีขึ้นเรื่อยๆ ไม่ว่าจะเป็นการนำ View Engine ออกเพื่อไปใช้ Ivy อย่างเต็มตัว, Standalone API ที่ทำให้สร้าง component ได้ง่ายขึ้นและลดการใช้ NgModules, Image Directive ที่ช่วยให้โหลดรูปได้ดีขึ้น, ปรับปรุง router หลายๆด้านไม่ว่าจะทำให้เป็น tree shakable, ทำให้เขียนได้ง่ายขึ้นและสั้นลง, ส่ง data จาก route ไปเป็น input ใน Component ได้ นอกจากนี้ยังมีการยกเครื่อง Server-side rendering ใหม่ให้มีประสิทธิภาพดีขึ้นและยังนำไปสู่ฟีเจอร์ใหม่ๆอีกหลายอย่างในอนาคต และท้ายที่สุดการมาของ Signal ที่จะช่วยให้ Angular เข้าใจง่ายขึ้น ใช้งานง่ายขึ้น และประสิทธิภาพดีขึ้นครับ


References

Angular v13 is now Available We’re back with the brand new release of Angular v13 to share with all of you!

Angular v14 is now available! We are excited to announce the release of Angular v14! From typed forms and standalone components to new primitives in the Angular CDK (component dev kit), we’re excited to share how each feature makes Angular more powerful.

Angular v15 is now available! Over the past year we removed Angular’s legacy compiler and rendering pipeline which enabled the development of a series of developer experience improvements in the past couple of months.

Angular v16 is here! Six months ago, we reached a significant milestone in Angular’s simplicity and developer experience by graduating the standalone APIs from developer preview.