iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 41

Day 41 - 使用 AfterRenderEffect 生命週期鉤子進行反應式 DOM 讀寫

  • 分享至 

  • xImage
  •  

Introduction

在 Angular 19 中,實驗性生命週期鉤子 afterRenderEffect 允許開發人員更新 DOM。

afterNextRenderafterRender 一樣,afterRenderEffect 也有四個階段:earlyReadwritemixedReadWriteread

在此示範中,我將使用 afterRenderEffect 附加新的圖表數據和變更圖表顏色。 afterRenderEffect 註冊 effect 並在渲染所有元件後運行它。

AfterRenderEffect 的四個階段

  • earlyRead:在 write 階段之前從 DOM 讀取的階段。如果讀取可以等到寫入階段之後,則優先選擇read階段。。在此階段切勿讀取 DOM。
  • write:在此階段,開發人員會讀取訊號值並寫入 DOM。在此階段切勿讀取 DOM。
  • mixedReadWrite:在此階段,開發人員可以讀取和寫入 DOM。請避免此階段。
  • read:在此階段,開發人員可以從 DOM 讀取訊號。此階段切勿寫入 DOM。

在這篇文章中,我描述如何使用 afterNextRender 在畫布上新增圖表,以及如何使用 afterRenderEffect 重繪圖表。

afterNextRender和afterRenderEffect的使用場景

在此示範中,我想使用第三方圖表庫 Chart.js 在畫布元素上渲染長條圖。因此,我實作了 afterNextRender 鉤子來將圖表插入到畫布中。然後,我使用 RxJS timer運算子和 toSignal 產生新的圖表數據,並每秒將其附加到圖表數組中。

為了使示範具有互動性,顏色下拉清單會變更條形的顏色。我將實作兩個 afterRenderEffect 鉤子;第一個鉤子將新的圖表資料附加到圖表資料數組中,第二個鉤子修改圖表顏色。

@Component({
 selector: 'app-root',
 imports: [FormsModule],
 template: `
   <h1>Hello from AfterRenderEffect!</h1>
   <div style="width: 400px;">
     <div>
       <label>
         Bar Color:
         <select [(ngModel)]="barColor">
           @for (c of barColors(); track c.id) {
             <option [value]="c.id">{{ c.color }}</option>
           }
         </select>
       </label>
     </div>
     <canvas #canvas></canvas>
   </div>
 `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App implements OnDestroy {
 barColors = signal([
   { id: 'red', color: 'Red' },
   { id: 'pink', color: 'Pink' },
 ]);

 data = signal([
   { year: 2022, count: 30 },
   { year: 2023, count: 4 },
 ]);

 barColor = signal('red');
 numBars = signal(0);

 chart: Chart | null = null;
  
 constructor() {
   // Implement afterNextRender and afterRenderEffect here
   afterNextRender({});
   
   afterRenderEffect({});
   afterRenderEffect({});
 }
 
 ngOnDestroy(): void {
   this.chart?.destroy();
 }
}

Install dependency

npm i --save-exact chart.js

實作 afterNextRender 生命週期鉤子將圖表附加到畫布

afterNextRender 生命週期鉤子在下次變更偵測後執行一次。因此,write 階段是將新圖表插入 DOM 的理想入口點。

canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas')
nativeElement = computed(() => this.canvas().nativeElement);

首先,我使用 viewChild 函數來取得畫布的 ElementRefnativeElement 是一個存取 HTMLCanvasElement 實例的計算訊號。 在建構函式中, afterNextRender 生命週期鉤子用這些訊號初始化圖表。

type ChartDataType = {
 year: number;
 count: number;
};

function initializeChart(canvas: HTMLCanvasElement, data: ChartDataType[], backgroundColor: string) {
 return new Chart(canvas,
   {
     type: 'bar',
     data: {
       labels: data.map(row => row.year),
       datasets: [
         {
           label: 'Acquisitions by year',
           data: data.map(row => row.count),
           backgroundColor,
         }
       ]
     }
   }
 );
}

上面的輔助函數接受畫布、初始圖表資料和圖表顏色來建立並傳回圖表。

constructor() {
   afterNextRender({
     write: () => {
       console.log('afterNextRender write is called');
       this.chart = initializeChart(this.nativeElement(), this.data(), this.barColor());
     }
   });
}

在建構函式中,我實作了 afterNextRender 生命週期鉤,並在 write 階段將圖表附加到畫布。

我已成功將 JavaScript 圖表插入 DOM 中。 DOM 讀取和寫入只執行一次,因此畫布不會錯誤地顯示多個圖表。

實作 afterRenderEffect 生命週期鉤子來執行效果 (effect)

更新圖表數據

我想在鉤子的 earlyReadwrite 階段附加圖表資料。然後,我將重構鉤子以避免 earlyRead 階段並僅在 write 階段執行更新。

chartData = toSignal(timer(100, 1000)
    .pipe(take(15)), { initialValue: -1 });

這個 timer 運算子發出 15 個整數,我將在 earlyRead 階段每秒匯出圖表資料。產生資料後,write 階段接收圖表資料物件並在圖表中顯示新的長條圖。 toSignalObservable 創建一個唯讀訊號,以便 afterRenderEffect 鉤子可以在 earlyRead 階段讀取它。

constructor() {
  afterRenderEffect({
     earlyRead: () => {
       const index = this.chartData();
       return index < 0 ? undefined :
         { year: 2024 + index, count: Math.floor(Math.random() * 20) + 2 } as ChartDataType;
     },
     write: (randomData) => {
       const chartData = randomData();
       if (chartData) {
         const { year, count } = chartData;
         const chart = this.chart?.data; 
         chart?.labels?.push(year);
         chart?.datasets.forEach(({ data }) => data.push(count));
         this.chart?.update();
       }

       return chartData;
     }
   });

earlyRead 階段讀取圖表資料訊號,產生並傳回隨機資料。在 write 階段,回呼函數的第一個參數是隨機數據,它被附加到圖表的資料數組中。

重構以消除 earlyRead 階段

如果讀取操作可以在寫入操作之後發生,則 Angular 文件偏好使用讀取階段。我將重構 Observable 以產生圖表數據,以消除 earlyRead 階段。

chartData = toSignal(timer(100, 1000)
   .pipe(
     take(15),
     map((i) => ({ year: 2024 + i, count: Math.floor(Math.random() * 20) + 2 } as ChartDataType))
   ), { initialValue: undefined });

Observable 使用 map 運算子來產生圖表資料。

afterRenderEffect({
     write: () => {
       const chartData = this.chartData();
       if (chartData) {
         const { year, count } = chartData;
         const chart = this.chart?.data;
         chart?.labels?.push(year);
         chart?.datasets.forEach((dataset) => dataset.data.push(count));
         this.chart?.update();
       }

       return chartData;
    }
});

重構 chartData 訊號後,write 階段存取訊號的值並執行相同的邏輯以將 chartData 附加到圖表的data array 。

基於使用者輸入的反應式 DOM 讀寫

另一個案例是當使用者從下拉清單中選擇顏色名稱時更新圖表顏色。這個簡單的下拉清單應用 two-way data binding 將 ngModel 綁定到 barColor 訊號。

<div>
    <label>
        Bar Color:
        <select [(ngModel)]="barColor">
          @for (c of barColors(); track c.id) {
             <option [value]="c.id">{{ c.color }}</option>
          }
        </select>
    </label>
</div>

barColor = signal('red');

當使用者進行選擇時,應用程式會更新 this.barColor 並觸發 afterRenderEffect 生命週期鉤子。因此,我將邏輯放在 afterRenderEffect 生命週期鉤子中來更新圖表顏色。

afterRenderEffect({
     write: () => {
       this.chart?.data.datasets.forEach((dataset) => dataset.backgroundColor = this.barColor());
       this.chart?.update();

       return this.barColor();
     }
});

afterRenderEffect 鉤子中, dataset.backgroundColor = this.barColor(); 更新顏色和 this.chart.update(); 再次更新圖表。

資源:


上一篇
Day 40 - 使用 Angular 原理圖從裝飾器遷移到函數
系列文
Signal API in Angular41
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言