序
通常,当我们在Angular中使用JavaScript技术时,我们几乎会忘记框架的特性。让我们去使用它们。 web开发中一个有趣的主题是DOM操作。在Angular中,有很多方法可以操作DOM。 让我们使用它们而不是直接的JavaScript方法。
通常来说,在dom操作时会有两种概念:
- 调整DOM元素
- 调整DOM结构
调整DOM元素
我们熟悉很多修改DOM元素的JavaScript方法。例如:
- classList.add()
- setAttribute()
- Style.setPropperty()
这是js中原生的一些DOM方法
在Angular中,有多种方法可以修改DOM元素。我们将讨论几乎每一种方法,并从中选择最好的。
方法
方法1
概念:
- 模板引用变量
- ElementRef
- @Viewchild/@Viewchildren
- AfterViewInit
定义:
- 模板引用:对特定DOM元素的引用。
- ElementRef: ElementRef是一个类,它包含所有本地的DOM元素
- @viewchild/@viewchildren: 从DOM中选择子元素或所有子元素
- AfterViewInit: 这是一个生命周期钩子,在Angular组件初始化它的视图之后被调用。
@Component({
selector: 'app-root',
template:`<span #el>I am manoj.</span>
<span>I am a web developer.</span>`,
styles:[`[highlight]{background: green; color: white}`]
})
export class AppComponent implements AfterViewInit{
@ViewChild('el') span:ElementRef;
ngAfterViewInit(){
this.span.nativeElement.setAttribute('highlight', '');
}
}
步骤:
Step1:在这个例子中,我使用模板引用和@viewchild查询来获取一个HTML元素。
<span #el>I am manoj.</span>
<span>I am a web developer.</span>
@ViewChild(‘el’) span: ElementRef;
Step2: DOM元素的setAttribute用于添加属性。
this.span.nativeElement.setAttribute(‘highlight’, ‘’);
页面上展示没有任何问题,它运行得很好。但是这种方法有一个问题。这里,我们混合了呈现和表示逻辑。
通常,组件(components)代表逻辑,例如定义数组、对象和迭代等等。呈现逻辑实际上是修改DOM元素。我们应该在一个单独的指令中维护渲染逻辑。我们可以通过数据绑定机制来进行组件和指令的通信。
现在让我们考虑另一种解决这个问题的方法。
方法2
让我们创建一个指令,并将所有的呈现逻辑放在该指令中。
概念:
- ElementRef
- @Input()
- ngOnInit()
定义:
- ElementRef:它帮助去访问DOM元素
- @Input() : 数据绑定来通信组件和指令。
- ngOnInit() : 初始生命周期钩子,它在Angular创建组件后被调用。
例如:
指令:
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit{
@Input() appHighlight;
constructor( private element: ElementRef) { }
ngOnInit(){
this.element.nativeElement.setAttribute(this.appHighlight, '');
}
}
组件:
@Component({
selector: 'app-root',
template:`<span [appHighlight]="'highlight'">I am manoj.<span>
<span>I am a web developer.</span>`,
styles:[`[highlight]{background: green; color: white;}`]
})
export class AppComponent{}
步骤:
**Step1:**将ElementRef注入到指令文件的构造函数中。
constructor( private element: ElementRef) { }
Step2:
添加@input装饰器到指令中
@Input() appHighlight;
Step3:
使用setAttribute()原生元素方法在ngOnInit()生命周期钩子中添加一个属性。
ngOnInit(){this.element.nativeElement.setAttribute(this.appHighlight, ‘’)}
Step4:
将指令应用到组件模板中的span元素:
<span [appHighlight]=”’highlight’”>I am manoj.</span>
输出和前面一样,但是这里渲染逻辑从组件移动到指令。它有助于在任何地方重用相应的组件和指令。
但在这里,我们使用ElementRef类直接访问DOM元素。允许直接访问DOM会使我们的应用程序更容易受到XSS攻击。大多数人在所有地方都使用ElementRef。现在的问题是,我们应该使用什么来安全地访问DOM元素?对,我们有个解决办法。让我们看看另一种方法。
方法3
概念:
1渲染器(Renderer)-使直接DOM访问安全,并且它是一个独立的平台。它修改DOM元素而不直接接触DOM。呈现程序是由一些方法组成的服务。它有助于操作DOM。
下面我列出了一些渲染器方法
1.addClass()
2.removeClass()
3.setStyle()
4.removeStyle()
5.setProperty()
例子:
constructor( private element: ElementRef,
private renderer: Renderer2) { }
ngOnInit(){
this.renderer.setAttribute(this.element.nativeElement,this.appHighlight, '')
}
步骤:
Step1:将渲染器注入到指令文件的构造函数中。
constructor( private renderer: Renderer2) { }
Step2: 使用renderer setAttribute()方法在ngOnInit()生命周期钩子中添加一个属性。
ngOnInit(){ this.renderer.setAttribute(this.element.nativeElement , this.appHighlight, ‘’)}
这里的输出是一样的。但是我们正确且更安全地修改了DOM。像ngClass、ngStyle这样的属性指令会基于渲染器修改DOM元素。因此,接下来,我们将使用Renderer来修改DOM。这也是一种更恰当的方式。
调整DOM结构
1creatElement()
2remove()
3appendChild()
4removeChild()
下面是一些修改DOM结构的JavaScript方法的例子。这些方法我们已经很熟悉了。现在我们来看看如何在Angular中修改DOM结构。
从DOM中移除一个子组件
方法1
概念:
1Template Reference
2ElementRef
3@ViewChildren()
4AfterViewChecked
5Renderer
6QueryList
定义:
- Template Reference: 特定DOM元素的引用。
- ElementRef: 帮助去访问DOM元素。
- @ViewChildren(): 以QueryList的形式从视图dom返回指定的元素或指令
- AfterViewChecked: 它在默认更改检测程序完成一个更改检查周期后调用。
- Renderer: 使直接DOM访问更安全
- QueryList: 返回类型。它只是一个项目列表。Angular在添加或删除列表项时更新QueryList。它只在ngAfterViewInit()生命周期钩子之前初始化。
Copy
@Component({
selector: 'app-parent',
template: `<p>parent works!</p>
<app-child #child></app-child>
<button (click)='removeChild()'>remove child</button>`,
styleUrls: ['./parent.component.css']
})
export class ParentComponent implements AfterViewChecked{
@ViewChildren('child', {read: ElementRef}) childComp:QueryList<ElementRef>
constructor(private renderer: Renderer2, private host: ElementRef) {
}
ngAfterViewChecked() {
console.log(this.childComp.length)
}
removeChild(){
this.renderer.removeChild(this.host.nativeElement, this.childComp.first.nativeElement);
}
}
步骤:
- 使用template reference和ViewChildren()来获取HTML元素。
- 将Renderer和ElementRef注入构造函数
- 使用removecchild()方法来删除子组件
- 为了获得宿主元素,我们需要使用ElementRef。
显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MSdUB27A-1632643270004)(/Users/jiangyumin/Documents/img/domoutput1.png)]
点击移除按钮后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qk0Bnqkb-1632643270006)(/Users/jiangyumin/Documents/img/click.png)]
由此可见,我们成功地删除了子组件。但是,我想再次向您展示带有控制台日志的输出屏幕。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YhcAbQEt-1632643270009)(/Users/jiangyumin/Documents/img/childcount.png)]
问题:
我们已经删除了子组件,但子组件计数仍然是1。为什么?如何解决?
是的,显然我们会想到这个问题。别担心,我们有办法。
DOM和View的关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ATEDYnpE-1632643270010)(/Users/jiangyumin/Documents/img/daigram.png)]
Angular有一个视图概念。看看图表;它将清楚地说明视图和DOM的关系。视图只是DOM的引用。当我们运行Angular应用时,它会创建多个视图。
对于每个组件的创建,视图也是由Angular创建的。我们看到了外部的组件层次结构。但引擎盖下的视图层次结构也是基于组件层次结构创建的。
如果我们在DOM中做了任何改变,比如拖动、添加或删除DOM元素,视图应该立即更新。否则,这将是一个问题。
变更检测基于视图的层次结构运行。
使用方法1,我们从DOM中删除了子组件。但是视图层次结构保持不变,并且视图没有更新。所以它展示了一个child。在这里,这是一个小场景,没有问题,但如果我们删除了10个组件,而视图没有更新,Angular仍然会对所有被删除的组件运行变更检测。这将严重影响我们的应用程序。
View Container (viewContainerRef):
视图容器使DOM结构更改安全。
方法:
- insert()
- Move()
- Remove()
- createEmbeddedView()
初始化视图容器:
我们可以将任何DOM元素作为视图容器。但通常情况下,所有人都将作为视图容器。ng-container只是一个HTML标签,但它是Angular特有的。
视图有两种类型:
- Embedded Views-始终是视图容器的一部分。
- Host Views-始终是视图容器的一部分,但也是独立的。
让我们看看如何使DOM元素成为视图。
步骤:
-
在模板文件中添加ng-container标签。
<ng-container #viewcontainer></ng-container>
-
添加@viewchild以选择元素。
@ViewChild(‘viewcontainer’, {‘read’: ViewContainerRef}) viewcontainer;
在这里,ViewContainerRef是一个非常重要的部分,它使DOM节点成为视图容器。
- 创建视图并将视图添加到视图容器中。
Viewcontainer.CreateEmbeddedView(TemplateRef);
创建一个模板
它非常类似于创建视图容器。让我们看看如何创建它们。
步骤:
- 在模板文件中添加ng-template标签。
<ng-template #t></ng-template>
- 使用它创建一个模板。
@ViewChild(‘t’, {‘read’: TemplateRef}) template: Templateref;
例:
模板文件:
<p>parent works!</p>
<ng-container #viewcontainer></ng-container>
<ng-template>
<app-child #child></app-child>
</ng-template>
<button (click)='removeChild()'>remove child</button>
类文件:
@ViewChildren('child', {read: ElementRef})
childComp:QueryList<ElementRef>
@ViewChild('viewcontainer', {'read': ViewContainerRef}) viewcontainer;
@ViewChild(TemplateRef) template: TemplateRef<null>;
constructor(private renderer: Renderer2, private host: ElementRef) {}
ngAfterViewChecked() {
console.log("Child components count", this.childComp.length)
}
ngAfterViewInit(){
this.viewcontainer.createEmbeddedView(this.template);
}
removeChild(){
this.viewcontainer.remove();
}
解决方案:
这个例子只是我们问题的一个解决方案。现在,如果我们移除DOM元素,Angular会直接更新视图。
显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agj2mTVt-1632643270012)(/Users/jiangyumin/Documents/img/result.png)]
最后,我们做到了。我希望我们大家都明白view:
像*ngIf, *ngFor, *ngSwitch这样的结构指令是基于视图容器来修改DOM结构的。
接下来,我们将使用viewContainerRef和templateRef来改变DOM结构。我们可以将呈现逻辑分离到单独的指令中,就像在修改DOM元素一节中所做的那样。这将非常有用。我们可以使用像*ngFor, *ngIf这样的指令.
注意:
如果DOM元素是由Angular创建的,默认情况下视图也会被创建。如果我们使用Jquery或JavaScript直接更改DOM结构,则不会更新视图。Angular并不知道DOM元素已经创建。所以更改检测对那个DOM元素不起作用。
总结:
- 调整DOM元素–使用渲染器服务
- 调整DOM结构–使用ViewContainerRef和TemplateRef类
- 将呈现逻辑分离成指令。它有助于复用代码。
- 避免使用JavaScript或Jquery直接操作DOM。尝试使用框架的可用特性。