在Angular开发中,通过@ViewChild获取*ngIf渲染的DOM元素时需将static显式设为false,否则会返回undefined。利用AngularCDK的拖拽功能实现跨组件元素互拖,需通过全局store与RxJS动态更新可拖拽列表,并在拖拽回调中更新父子节点关系。
在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包裹的结构,则会获取失败。具体场景如下:

解决办法很简单:在@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';
官方文档的示例较为简单,未涉及跨组件拖拽的整合。以下分享实现时的关键思路。
首先,将所有需要拖拽的元素加入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,再进一步获取可拖拽的元素列表。
在模板中通过(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); }
这样,子组件与父组件内部的元素即可互相拖拽,整体交互流程便可顺畅运行。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述