Angular 5可编辑Div中动态插入可点击自定义Span组件的问题
解决Angular 5中在contenteditable Div任意位置插入动态组件的问题
咱们先把核心问题掰明白:你现在用ViewContainerRef只能在预定义的<ng-container>里生成组件,但contenteditable的myMessage div是原生DOM环境,直接把Angular组件塞进去行不通,得换个思路——把组件动态挂载到div里的指定DOM节点上,同时保留组件的事件和样式。
下面是具体的分步解决方案:
1. 先处理光标位置,插入占位DOM节点
要在用户光标所在位置插入组件,首先得获取myMessage div里的光标位置,然后插入一个空的<span>作为占位符,后续把Angular组件挂载到这个占位符上。
先在Message.ts里添加一个获取光标位置并插入占位元素的函数:
getCursorInsertPosition(): HTMLElement { const editableDiv = document.getElementById('myMessage'); let range, placeholder; if (window.getSelection) { const sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { range = sel.getRangeAt(0); // 创建占位span,作为组件的挂载容器 placeholder = document.createElement('span'); range.insertNode(placeholder); // 调整光标到占位符后面,不影响后续输入 range.setStartAfter(placeholder); range.setEndAfter(placeholder); sel.removeAllRanges(); sel.addRange(range); } } else if ((document as any).selection && (document as any).selection.createRange) { // 兼容IE(如果需要的话) range = (document as any).selection.createRange(); placeholder = document.createElement('span'); range.pasteHTML(placeholder.outerHTML); } return placeholder; }
2. 修改动态组件创建逻辑,挂载到占位节点上
原来的createSpanPhone是用target.createComponent把组件放到ng-container里,现在要改成把组件挂载到刚才创建的占位DOM节点上。需要用到ComponentFactoryResolver和Injector,同时要把组件的宿主视图关联到占位节点。
修改Message.ts的createSpanPhone方法:
import { ComponentFactoryResolver, ComponentRef, Injector } from '@angular/core'; // 记得注入Injector constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {} // 定义数组存储动态组件,方便后续获取数据 dynamicComponents: ComponentRef<any>[] = []; createSpanPhone(spanIDNumber, phoneDescription, phoneValue ){ // 1. 获取光标位置的占位节点 const placeholder = this.getCursorInsertPosition(); if (!placeholder) return; // 2. 创建PhoneComponent的工厂 const phoneComponentFactory = this.cfr.resolveComponentFactory(PhoneComponent); // 3. 创建组件实例,把占位节点作为宿主元素 const componentRef: ComponentRef<PhoneComponent> = phoneComponentFactory.create( this.injector, [], placeholder ); // 4. 把组件实例关联到当前视图的变更检测树,确保事件和数据绑定生效 componentRef.changeDetectorRef.detectChanges(); // 5. 保存组件引用,方便后续获取数据 this.dynamicComponents.push(componentRef); // 6. 给组件传递动态数据 componentRef.instance.phoneDesc = phoneDescription; componentRef.instance.phoneVal = phoneValue; }
3. 调整PhoneComponent,支持输入数据绑定
原来的PhoneComponent里的文本是硬编码的,现在改成绑定输入属性,方便动态传入数据:
import { Component, Input } from '@angular/core'; import { faPhone } from '@fortawesome/free-solid-svg-icons'; @Component({ selector: 'my-phone', template: ` <span contenteditable="false" (click)="test()" class="BAN_Tags_IN_Text"> <fa-icon [icon]="faPhone" class="faSpanIcon"></fa-icon> <span class="phoneDesc" data-attr="EN">{{phoneDesc}}</span> <span class="phoneVal"><b>{{phoneVal}}</b></span> </span> ` }) export class PhoneComponent { faPhone = faPhone; @Input() phoneDesc: string; @Input() phoneVal: string; constructor(){} test(){ console.log("Hiiii, clicked phone tag!"); // 这里可以打开修改对话框,修改后直接更新phoneDesc和phoneVal即可 } }
4. 保存时收集所有组件的数据
当用户要保存推送内容时,遍历之前保存的dynamicComponents数组,提取每个组件的核心数据:
saveMessage() { const messageData = { content: document.getElementById('myMessage').innerHTML, tags: this.dynamicComponents.map(comp => { if (comp.instance instanceof PhoneComponent) { return { type: 'phone', description: comp.instance.phoneDesc, value: comp.instance.phoneVal }; } // 同理处理位置、网址等其他组件 return null; }).filter(tag => tag !== null) }; // 把messageData发送到后端或者处理移动端推送逻辑 console.log('保存的数据:', messageData); }
关键注意事项
- 因为
contenteditable的特性,用户可能会删除插入的span组件,所以要监听myMessage的input事件,及时从dynamicComponents数组中移除被删除的组件引用(可以给每个组件的宿主节点加唯一ID,然后对比DOM节点是否存在)。 - Angular 5里需要把动态组件(比如
PhoneComponent)添加到模块的entryComponents数组中,否则会报错:
@NgModule({ declarations: [PhoneComponent], entryComponents: [PhoneComponent] // 必须手动声明 }) export class YourModule {}
- 组件挂载后,如果需要更新数据,直接修改组件实例的属性即可,Angular的变更检测会自动更新视图。
这样调整后,就能在myMessage div的任意光标位置插入带样式、图标和点击事件的PhoneComponent,并且能在保存时准确获取对应的标签数据啦。
内容的提问来源于stack exchange,提问作者Aless Hosry
相关产品推荐
相关产品推荐

