Angular 17+ 高級教程 – Component 組件 の Query Elements

前言

Angular 是 MVVM 框架。

MVVM 的宗旨是 "不要直接操作 DOM"。

在 Component 組件 の Template Binding Syntax 文章中,我們列舉了一些常見的 DOM Manipulation。

const element = document.querySelector<HTMLElement>('.selector')!; // query element
element.textContent = 'value'; // update text
element.title = 'title'; // update property
element.setAttribute('data-value', 'value'); // set attribute (note: attribute and property are not the same thing)
element.style.padding = '16px'; // change style
element.classList.add('new-class'); // add class

const headline = document.createElement('h1'); // create element
headline.textContent = 'Hello World';
element.appendChild(headline); // append a element
element.innerHTML = `<h1>Hello World</h1>`; // write raw HTML

element.addEventListener('click', () => console.log('clicked')); // listen and handle a event

Template Binding Syntax 替代了上面許多的 DOM Manipulation,但任然有些 DOM Manipulation 是它沒有覆蓋到的。

比如說

  1. Query Child Elements
    document.querySelectorAll
  2. Query Parent Element
    document.body.parentNode 或者 document.body.closest
  3. Query Content Projection (a.k.a slot) Elements
    slot.assignedElements
     

這篇,我們就來樸上這些 DOM Manipulation 替代方案,看看在 Angular 要如何 Query Elements。

 

Query Parent Element

在強調組件化的項目,query 往往和 Shadow DOM 隔離息息相關。

Query parent element in Shadow DOM

上圖是一個 W3C Web Components 的例子,有兩個組件 my-parent 和 my-child,它們都有 Shadow DOM 概念。

假如我們 select my-child,然後嘗試 query my-parent,結果是這樣

因爲有 Shadow DOM 隔離,my-child 無法直接 query 到 my-parent,唯一的方法是一層一層拿

先找到 shadowRoot 然後 .host 纔可以越出 Shadow DOM 的界限。

Angular 也有 Shadow DOM,雖然實現手法是模擬的,但基本規則是一樣的。

那 Angular 也需要一層一層往上找嗎?當然不用!

inject Parent 組件

首先,Angular 並沒有提供一個直接和完整的 query parent element 方案。

Angular 只是藉助 NodeInjector 依賴注入的機制,讓我們可以 query parent 組件實例(注:是 parent 組件實例,而不是 parent element)

在 Component 組件 の Dependency Injection & NodeInjector 文章中,我們就學習過了,子組件可以 inject 祖先組件的實例。

inject Parent Element

如果不想要組件實例,想要 element 的話,可以用一個很蠢的方法。

首先在 Parent 組件裏,通過 inject ElementRef 拿到 nativeElement,然後存起來。

接着在 Child 組件,inject Parent 組件實例,然後再從實例調出 element。

那這麼蠢的方式,難道沒有人抱怨嗎?當然有!

Github Issue – Ability to request injection from a specific parent injector

有人提議可以通過 options read 來表達想要 inject 的是 element 而不是組件實例。

const parentElement = inject(ParentComponent, { read: ElementRef });

但這個提議被 Angular 團隊否決了。

我自己覺得這個提議在表達上是 ok 的,但在目前 DI 的機制上或許不那麼容易加入這個新概念。

我們在 NodeInjector 文章裏學習過 inject 函數的查找規則,inject(ElementRef) 是一個特殊對待

它不像組件、指令、providers 那樣把 NodeInjectorFactory 存在 LView 裏,只要找到它,調用就可以了。

inject(ElementRef) 是依賴 current TNode 生成的,如果在 Parent constructor 裏,此時的 TNode 是 Parent,如果去到了 Child constructor 那 TNode 就是 Child 了。

至少從目前 DI 機制來看,想從 Child constructor 獲取到 Parent ElementRef 並不是那麼容易的。

NodeInjector Tree !== DOM Tree

Angular 沒有提供一個直接和完整的 query parent element 方案。

上面的 "直接",指的是 Angular 只提供了間接的方法,通過 inject 組件實例再獲取 element。

const parentElement = inject(ParentComponent).nativeElement

上面的 "完整" 指的是,有些情況下你根本獲取不到 parent element,Angular 完全沒有提供方法。

Angular 是通過 inject parent 組件實例來獲取 parent element 的,要知道 inject 走的是 NodeInjector Tree,而不是 DOM Tree。

這兩棵樹結構有時候是不一致的。

在兩種情況下 NodeInjector Tree 會和 DOM Tree 不一致。

第一種是 Content Projection

對於 query parent element,這種情況下兩棵樹雖然不一致,但不要緊,因爲 Angular 出來的效果是和 W3C Shadow DOM 的 slot 效果是一樣的。

第二種是 Dynamic Component

在 Dynamic Component 的情況下就麻煩了,這個下一篇纔會教。

總結

1. Shadow DOM 需要一層一層 parentNode.host 才能 query 到 parent element,Angular 不需要這麼麻煩,它可以直接 inject 祖先組件實例。

2. 雖然 Angular inject 祖先組件實例很方便,但那不是 element,要拿到 element 需要在祖先組件 inject(ElementRef) 這個超級麻煩,代碼管理也嚴重扣分。

3. DI 走的是 NodeInjector Tree,但我們或許想要的是 DOM Tree 的 parent element,當這兩棵樹結構不一致時,要特別小心。

 

Query Child Elements

在強調組件化的項目,query 往往和 Shadow DOM 隔離息息相關。

Query child elements in Shadow DOM

上圖是一個 W3C Web Components 的例子,有兩個組件 my-parent 和 my-child,它們都有 Shadow DOM 概念。

假如我們從 body 嘗試 query h1 elements,結果是這樣

因爲有 Shadow DOM 隔離,我們無法從 body 直接 query 到 Shadow DOM 內的 elements。

我們需要先進入 shadowRoot 再 query。(注: 要進入 shadowRoot,attachShadow mode 必須是 'open' 哦)

即便如此,my-parent 的 shadowRoot.querySelectorAll 也只能 query 到 my-parent shadowRoot 的範圍,

my-child 任然被另一個 shadowRoot 隔離着。如果我們想 query 所有後裔的 h1 elements,那就必須一層一層 shadowRoot 進入。

這個體驗和上面 query parent in Shadow DOM 是一樣的麻煩。

Angular 也有 Shadow DOM,雖然實現手法是模擬的,但基本規則是一樣的。

那 Angular 也需要一層一層往下 query 嗎?是的!這一點 Angular 選擇了和 Shadow DOM 保持一致。

Query child elements in Angular

我們先看一個簡單的例子,然後再去逛源碼理解它背後的原理和機制。

下圖是 App Template

我們要 query 出裏頭的兩個 <p>,#paragraph 是啥,下面會講解。

@ViewChildren

下圖是 App 組件

屬性 + @ViewChildren decorator = query 語句。(又是 decorator 黑魔法...)

這個語句的字面意思是,query 'paragraph',然後賦值給屬性 paragraphQueryList,類型是 QueryList,

黑魔法後 QueryList 對象裏就裝着 2 個 <p> HTMLElement。

Template Variables

上面最重要的一點是,query 並不是用 CSS Selector!

@ViewChildren('.class'),@ViewChildren('tag') 都是錯誤的語法。

@ViewChildren('paragraph') 對應的是 <p #paragraph>

這個叫 Template Variables,是 Angular 的設計。

簡單的說 Template Variables 的用途就是讓我們在節點上打個標籤,然後我們就可以引用這個節點去做點事情。

要取任何變量名字都可以哦

ngAfterViewInit

這個 QueryList 並不是馬上就被賦值的哦,要等到組件生命週期 AfterViewInit QueryList 纔可以使用。

@ViewChild

如果我們只是想 query 一個 element 我們可以用 @ViewChild。
@ViewChildren 和 @ViewChild 的區別,類似於 querySelectorAll 和 querySelector 的區別。

@ViewChild 的類型不是 QueryList,而是直接拿到 ElementRef。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

首先,Angular 不是用 CSS Selector 來做 query 的。它搞了一個叫 Template Variables 的東西來替代。(或許是想簡化複雜度)

Template Variables 長這樣

我們不能 querySelectorAll('p') 也不能 querySelectorAll('.class')。我們需要在

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章