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')。我们需要在

 

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