Angular Renderer2执行结果异常问题排查求助
Angular指令替换填空题下划线为输入框的问题修复
问题背景
需要将填空题中的下划线替换为输入框,例如:
- Newton’s __________ Law of Motion states
- __________ is the product of an object’s mass and velocity.
实现了Angular指令appReplaceUnderscores,但遇到两个核心问题:
- 移除原生元素内的SPAN子节点时,存在节点残留的情况
- 在输入框前后添加SPAN节点时,位置错乱,有时两个SPAN都出现在输入框之后
问题代码及现象
指令调用的HTML结构
<span class="fw-semibold primary-color" [appReplaceUnderscores]="fillInBlank.question"> <input [(ngModel)]="userAnswer" class="form-control autofocus tertiary-bg-color answerField d-inline" type="text" required name="userAnswer" placeholder="Enter your answer here" (change)="processAnswer()" [disabled]="answer !== null"> </span>
移除节点的代码及问题
原代码遍历子节点移除非INPUT元素:
nativeElement.childNodes.forEach(node => { console.log('node.nodeName', node.nodeName) if(node.nodeName === 'INPUT') { inputNode = node; console.log('inputNode', inputNode) console.log('typeOf inputNode', typeof inputNode) } else { this.renderer.removeChild(nativeElement, node) console.log('node removed', node) } })
问题现象:执行后仍有SPAN节点残留,例如:
<span _ngcontent-msf-c27="" class="fw-semibold primary-color"> <input _ngcontent-msf-c27="" type="text" required="" name="userAnswer" placeholder="Enter your answer here" class="form-control autofocus tertiary-bg-color answerField d-inline ng-dirty ng-touched ng-invalid"> <span _ngcontent-msf-c27="" class="d-inline"> Law of Motion states that for every.... </span> </span>
添加节点的代码及问题
原代码添加前后SPAN的逻辑:
this.renderer.insertBefore(nativeElement, spanBeforeInput, inputNode) console.log('nativeElement after child prepended', nativeElement) this.renderer?.appendChild(nativeElement, spanAfterInput);
问题现象:有时两个SPAN都被添加到输入框之后,例如:
<span _ngcontent-msf-c27="" class="fw-semibold primary-color"> <input _ngcontent-msf-c27="" type="text" required="" name="userAnswer" placeholder="Enter your answer here" class="form-control autofocus tertiary-bg-color answerField d-inline ng-dirty ng-touched ng-invalid"> <span _ngcontent-msf-c27="" class="d-inline"> Law of Motion states that for every... </span> <span _ngcontent-msf-c27="" class="d-inline">Newton’s Law of </span> <span _ngcontent-msf-c27="" class="d-inline"> states that every object... </span> </span>
完整指令代码
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; @Directive({ selector: '[appReplaceUnderscores]' }) export class ReplaceUnderscoresDirective { @Input() set appReplaceUnderscores(question) { console.log('question', question) this.replaceUnderscore(question) } constructor( private el: ElementRef<HTMLElement>, private renderer:Renderer2 ) { console.log(this.el.nativeElement); } private replaceUnderscore = (question) => { const nativeElement: HTMLElement = this.el.nativeElement; console.log('nativeElement', nativeElement) let inputNode:ChildNode = null; nativeElement.childNodes.forEach(node => { console.log('node.nodeName', node.nodeName) if(node.nodeName === 'INPUT') { inputNode = node; console.log('inputNode', inputNode) console.log('typeOf inputNode', typeof inputNode) } else { this.renderer.removeChild(nativeElement, node) console.log('node removed', node) } }) const nativeElementAfterChildrenRemoved = nativeElement.cloneNode(true); console.log('naativeElement after children removed', nativeElementAfterChildrenRemoved) const splits = question.split('__________') console.log('splits', splits) const textBeforeUnderscores = this.renderer?.createText(splits[0]); const textAfterUnderscores = this.renderer?.createText(splits[1]); const spanBeforeInput = this.renderer?.createElement('span'); this.renderer?.appendChild(spanBeforeInput, textBeforeUnderscores); this.renderer.addClass(spanBeforeInput, "d-inline") console.log('spanBeforeInput', spanBeforeInput) const spanAfterInput = this.renderer?.createElement('span'); this.renderer?.appendChild(spanAfterInput, textAfterUnderscores); this.renderer.addClass(spanAfterInput, "d-inline") console.log('spanAfterInput', spanAfterInput) this.renderer.insertBefore(nativeElement, spanBeforeInput, inputNode) console.log('nativeElement after child prepended', nativeElement) // spanBeforeInput?.before(inputNode) // spanAfterInput?.after(inputNode) this.renderer?.appendChild(nativeElement, spanAfterInput); console.log('nativeElement after child appended', nativeElement) } }
问题原因及修复方案
1. 移除节点残留的原因
childNodes是动态DOM集合,遍历过程中移除节点会改变集合长度,导致后续节点索引前移,forEach循环会跳过这些节点。
修复:将childNodes转换为静态数组后再遍历:
// 转换为数组避免动态集合的影响 Array.from(nativeElement.childNodes).forEach(node => { if(node.nodeName === 'INPUT') { inputNode = node; } else { this.renderer.removeChild(nativeElement, node); } });
2. 添加节点位置错乱的原因
使用appendChild会将SPAN添加到父元素末尾,若输入框不是父元素最后一个节点(比如残留节点),就会导致位置错误。
修复:直接将后置SPAN插入到输入框的下一个节点之前:
// 插入前置SPAN到输入框之前 this.renderer.insertBefore(nativeElement, spanBeforeInput, inputNode); // 插入后置SPAN到输入框之后 this.renderer.insertBefore(nativeElement, spanAfterInput, inputNode.nextSibling);
额外优化
- 处理分割后数组的边界情况(下划线在题目开头或结尾)
- 移除冗余的console.log,减少生产环境输出
修复后的完整指令代码
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; @Directive({ selector: '[appReplaceUnderscores]' }) export class ReplaceUnderscoresDirective { @Input() set appReplaceUnderscores(question: string) { this.replaceUnderscore(question); } constructor( private el: ElementRef<HTMLElement>, private renderer: Renderer2 ) {} private replaceUnderscore = (question: string) => { const nativeElement: HTMLElement = this.el.nativeElement; let inputNode: ChildNode | null = null; // 静态数组遍历,避免动态DOM集合的节点遗漏 Array.from(nativeElement.childNodes).forEach(node => { if (node.nodeName === 'INPUT') { inputNode = node; } else { this.renderer.removeChild(nativeElement, node); } }); if (!inputNode) return; // 确保找到输入节点 const splits = question.split('__________'); // 处理下划线在开头/结尾的情况 const textBefore = splits[0] || ''; const textAfter = splits[1] || ''; // 创建前置文本SPAN const spanBefore = this.renderer.createElement('span'); this.renderer.appendChild(spanBefore, this.renderer.createText(textBefore)); this.renderer.addClass(spanBefore, 'd-inline'); // 创建后置文本SPAN const spanAfter = this.renderer.createElement('span'); this.renderer.appendChild(spanAfter, this.renderer.createText(textAfter)); this.renderer.addClass(spanAfter, 'd-inline'); // 插入前置SPAN到输入框之前 this.renderer.insertBefore(nativeElement, spanBefore, inputNode); // 插入后置SPAN到输入框之后 this.renderer.insertBefore(nativeElement, spanAfter, inputNode.nextSibling); } }
内容的提问来源于stack exchange,提问作者koque
相关产品推荐
相关产品推荐

