Angular获取普通Dom元素的方法
在Angular开发中,获取DOM元素是高频操作,但一个容易被忽略的细节就能卡住开发者很长时间。通常我们通过模板变量名(#greet)配合@ViewChild获取元素,这种方式在绝大多数场景下都很顺手。然而一旦遇上由*ngIf控制显示的元素,问题立刻暴露——获取到的往往是undefined。下面查看一段经典代码:
import { Component, ViewChild, AfterViewInit } from '@angular/core';
@Component({
selector: 'my-app',
template: `
Welcome to Angular World
Hello {{ name }}
`,
})
export class AppComponent {
name: string = 'Semlinker';
@ViewChild('greet')
greetDiv: ElementRef;
ngAfterViewInit() {
console.log(this.greetDiv.nativeElement);
}
}
这段代码在普通元素上运行良好,但一旦换成*ngIf包裹的结构,就会扑空。具体场景如下:

将static改成false 获取
解决方法其实很简单:在@ViewChild的配置选项中,将static显式设置为false。Angular默认的static值就是false,但当你向@ViewChild传递第二个参数时,某些版本下该值可能会被误配置为true。明确指定static: false能让查询行为符合预期——等到结构渲染完成后再去查找。改造后的代码如下:
@ViewChild('dropList', { read: CdkDropList, static: false }) dropList: CdkDropList;
ngAfterViewInit(): void {
if (this.dropList) {
console.log(this.dropList)
}
}
使用这一方案,就能顺利获取到*ngIf渲染后的cdkDropList实例。顺便提一下,上面这段代码源自一个实际功能:buttonGroup内的按钮与某个列表里的按钮可以互相拖拽,底层正是利用Angular自带的cdk/drag-drop实现。
import { CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
自己实现的思路
官方文档中的demo比较简单,并未涉及跨组件的拖拽整合。这里分享实现时的核心思路。
首先,将所有需要拖拽的元素都加入cdkDropList。在A组件和B组件各自初始化时,获取各自的DOM元素,并注册到一个全局store中,每个元素附带一个唯一的componentId。
A、B组件通过cdkDropListConnectedTo属性控制跨组件拖拽的目标范围,该属性绑定到一个动态数组_connectableDropLists。在页面初始化时,利用RxJS订阅特定componentId的变化,一旦store中有新的拖拽列表注册进来,就更新_connectableDropLists。关键代码片段如下:
const parentId = this.storeService.getProperty(this.pageId, this.componentId, 'parentId');
this.dragDropService.getDragListsAsync(this.pageId, parentId.value)
.pipe(takeUntil(this.destroy))
.subscribe(dropLists => {
this._connectableDropLists = dropLists || [];
});
this.storeService.getPropertyAsync(this.pageId, this.componentId, 'children')
.pipe(takeUntil(this.destroy)).subscribe(result => {
if (!result || result.length === 0) {
this._children = [];
this._dragData = [];
this.changeRef.markForCheck();
} else {
const dropbuttonArray = result.filter((item) => {
const itemType = this.storeService.getProperty(this.pageId, item, 'componentType');
if (itemType === AdmComponentType.DropdownButton) return item;
});
if (dropbuttonArray.length > 0) {
this._connectableDropLists = [];
dropbuttonArray.forEach(comId => {
this.dragDropService.getDragListsAsync(this.pageId, comId)
.pipe(takeUntil(this.destroy))
.subscribe(dropLists => {
this._connectableDropLists.push(...dropLists);
});
});
}
}
});
由于A组件是B组件的父级,因此需要通过当前组件id获取父级id,再进一步获取可拖拽的元素列表。
通过cdkDragData 把拖拽的元素的value,id等值带上
在模板中通过(cdkDropListDropped)="drop($event)"注册拖拽结束的回调。回调里需要处理数据更新——本质上是删除旧父级下的子节点,再将当前组件添加到新父级下面,同时更新parentId。另外,buttonGroup内部的按钮之间也允许互相拖拽,所以需要加一层判断做特殊处理。
drop(event: CdkDragDrop) { if (event.previousContainer != event.container) { const { eventData } = event.item.data; const componentId = eventData[event.previousIndex]; const oldParentId = this.storeService.getProperty(this.pageId, componentId, 'parentId', false)?.value; // delete oldParent children const oldParent = this.storeService.getProperties(this.pageId, oldParentId); const index = oldParent.children.indexOf(componentId); oldParent.children.splice(index, 1); // add newParent children const oldChildren = this.itemDatas.map(x => x.id.value); oldChildren.splice(event.currentIndex, 0, componentId); this.storeService.setProperty(this.pageId, componentId, 'parentId', { value: this.componentId }, [[this.pageId, componentId]]); this.storeService.setProperty(this.pageId, oldParentId, 'children', oldParent.children, [[this.pageId, oldParentId]]); this.storeService.setProperty(this.pageId, this.componentId, 'children', oldChildren); this.changeDetector.markForCheck(); return; } moveItemInArray(this.itemDatas, event.previousIndex, event.currentIndex); const children = this.itemDatas.map(x => x.id.value); this.storeService.setProperty(this.pageId, this.componentId, 'children', children); }
这样一来,子组件和父组件内部的元素就能互相拖拽,整个交互流程也就顺畅起来。
