สรุปฟีเจอร์ตั้งแต่ Angular 13 ถึง Angular 16
5/11/2566
ก่อนที่ Angular 17 จะออกมาในไม่กี่วันนี้ เรามาทบทวนกันดีกว่าว่าตั้งแต่ Angular 13 จนถึง Angular 16 มีฟีเจอร์อะไรที่น่าสนใจบ้าง
สารบัญ
Angular 13
เอา 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
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 analytics
command ng analytics นี้จะช่วยเซ็ต analytic setting และโชว์ analytic information
ng cache
command ng cache จะใช้จัดการ cache และโชว์ cache information
อัพเดทอื่นๆ
- 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
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
ทบทวนเรื่อง 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 เข้าใจง่ายขึ้น ใช้งานง่ายขึ้น และประสิทธิภาพดีขึ้นครับ