How to append a div tag that contains directives to a dynamic component - boreddev


Not too long ago, I read a question on the forum as follows:


“People give me some advice. I'm using Dynamic Component Loader. It is every time I load a new component I want to append an additional div tag that uses Directive into the component. I used Renderer2 and append gets the DOM into it. but Directive doesn't work. In the previous Angular Js, I used $ compile but I just switched to Angular 6 and I searched it all the time.


To illustrate the question, we will create the following:


first DynamicComponent serves the purpose of creating dynamic


@Component({
selector: 'app-dynamic',
template: `

Dynamic Component

`
})
export class DynamicComponent implements OnInit{
ngOnInit() {}
}

1 Directive ChangeColorDirective which can change the font color for the host element on which it is located:


@Directive({
selector: '(change-color)'
})
export class ChangeColorDirective implements OnInit{
@Input() textColor: string;
@HostBinding('style.color') color = 'blue';
ngOnInit() {
// this.color = this.textColor;
}
}

I will create dynamic component at AppComponent as follows:


@Component({
selector: 'app-root',
template: `

Component duoc chen them la:



`,
styleUrls: ('./app.component.css')
})
export class AppComponent implements OnInit {
@ViewChild(AdDirective) adHost: AdDirective;
constructor(...){}
ngOnInit() {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
let componentRef = componentFactory.create(this.injector);
let viewContainerRef = this.adHost.viewContainerRef;
if (viewContainerRef._embeddedViews.length === 0) {
viewContainerRef.insert(componentRef.hostView);
} else {
viewContainerRef.clear();
}
let textNode = this.renderer.createText("test color");
let divNode = this.renderer.createElement('div');
this.renderer.setAttribute(divNode, 'change-color', '');
this.renderer.appendChild(divNode,textNode);
let parentNode = componentRef.location.nativeElement;
this.renderer.appendChild(parentNode, divNode);
}
}

I explain the code in AppComponent:


  • In AppComponent.html, I use 1 To mark the position we will insert dynamic components into it. Inside, I use the adHost directive as follows:
@Directive({
selector: '(ad-host)'
})
export class AdDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}

  • This is a small trick, I use adHost Directive to get access to the ViewContainerRef of the element that I will put the dynamic component on. The reason is I can get the value of ViewContainerRef at ngOnInit of the AppComponent, without having to wait change detection and then get it at ngAfterViewInit as we often do. The trick here is to use the adHost directive, and to use ViewQuery: @ViewChild to retrieve the initialization of that directive. The ViewContainer is now retrieved via the mechanism dependency injection so it's always worth it.
  • Next, I created a card of the form:

test color

  • and append it to the newly created dynamic component template

According to the expected results, the color of the text of 'test color' must change to 'blue'. But the result is still black as usual. You can watch it run on StackBlitz here.


Okay, why is that?


Mistake right at the problem approach, using Renderer2 or nativeDomAPI or JQuery to insert / append / remove 1 Dom element that contains Angular's directive is an incorrect solution. Because inside Angular represents the component by a data structure called view good component view. This data structure is made up of component factory. The number of nodes that component factory contains is determined at the complication time (not the run time). While you append the div tag at run-time, the component factory is completely unaware of the node to compile. When the component factory fails to compile that node, resulting in the view component created does not contain that node, leading to the change detection process that occurs on the view component that never runs through that node (namely, instantiate ChangeColorDirective directive and assign hostBinding) color to the style attribute of the div tag). So the text in the div tag does not change color.


I will give specific evidence to a note later in source code of Angular:


“A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.


Properties of elements in a View can change, but the structure (number and order) of elements in a View cannot. Changing the structure of Elements can only be done by inserting, moving or removing nested Views via a ViewContainerRef. Each View can contain many View Containers. ”


If you still don't understand what the above sentence means, look at AppComponent for now. Why do we need to access the ViewContainerRef just to assign the host component of Dynamic Component into it? Why not do it directly by retrieving the nativeElement of DynamicComponent and then inserting it into the AppComponent template via Renderer2, JQuery, nativeDomAPI? You can do that, but change detection happens inside Angular, it doesn't happen on HTML templates.
I have advice for you:


We should avoid removing / append / insert any HTML element that is related to the framework (for example, contains internal directives) with Render2, JQuery or native Dom API. It should only remove / append / insert HTML elements that Angular does not know about "


How to fix


You can see right from the comment above of Angular: “Changing the structure of Elements can only be done by inserting, moving, removing nested views in through a ViewContainerRef. Each View can contain multiple ViewContainers ”. Maybe you do not understand what ViewContainer helps for these TH, let me illustrate for you the ViewNodes of the component when there is no view container


It is a fixed sequence of elements, both in order and in structure. Then you have no way to insert more nodes at all.

And this is when there is a ViewContainer node in the Component's ViewNodes:


Because the ViewContainer Node may contain other templates, we can dynamically insert views into that structure.


Ok, so in short, the first way would be to use ViewContainer. In addition I have 2 more ways, a total of 3 ways:


  • Use the View Container
  • Use Content Projection
  • use @ angular / element (or the term about custom-element emerging in the community now). This way I will write in another article.

Method 1: Use ViewContainer


As explained, we need to place an extra ViewContainer node in the Dynamic Component template. This is also the position we want the card

appear in dynamic component:


@Component({
selector: 'app-dynamic',
template: `

Dynamic Component



`
})
export class DynamicComponent implements OnInit{
@Input() template: TemplateRef;
@Input() componentRef: ComponentRef;
@ViewChild(AdDirective) adHost: AdDirective;
ngOnInit() {...}
}

To insert a tag into this position, we need to create either ComponentRef or TemplateRef from outside the AppComponent, and then pass it into the corresponding input: template or componentRef properties.
Depending on the problem posed, if the paragraph

The insert is fixed, ie it has specific format such as:


hello

then you just need to put it in 1 of AppComponent, and use view query to get it.



hello


If the insert tag is dynamic, the text can be changed, then you need to create a component that contains the template, and attach the corresponding bindings so that it can work as we want. Here I created ContentComponent


@Component({
template: `
Hello {{title}}
`
})
export class ContentComponent {
@Input() title: string;
}

Changes on AppComponent's ngOnInit


ngOnInit(){
let contentFactory = this.componentFactoryResolver.resolveComponentFactory(ContentComponent);
let contentView = contentFactory.create(this.injector);
(contentView.instance).title = 'World';
(componentRef.instance).template = this.tp;
}

The rest is on DynamicComponent's ngOnInit that decides which view type to insert into ViewContainer. Remember to choose 1 of 2


ngOnInit() {
this.adHost.viewContainerRef.createEmbeddedView(this.template);
//this.adHost.viewContainerRef.insert(this.componentRef.hostView);
}

Test the result on Stackblitz, and it works as expected.
The entire code way 1 above Here stackblitz


Method 2: Use Content Projection


Here is how to use it . I will into the position in Dynamic Component that you want to insert the tag.


@Component({
selector: 'app-dynamic',
template: `

Dynamic Component



`
})
export class DynamicComponent implements OnInit{…}

While initializing componentRef for DynamicComponent, we can pass an additional nested array to its second argument. This is the HTML element that will be replaced


ngOnInit() {
...
let template = this.tp.createEmbeddedView(null);
let content = ((contentView.location.nativeElement))
// hoac content = (template.rootNodes)
template.detectChanges();
contentView.hostView.detectChanges();
let componentRef = componentFactory.create(this.injector, content);
....
}

Here I have to call detectChange () because at this time both: EmbeddedView is generated from template, or HostView is born from component that has not run change detection yet, so it has no color. Because we created it manually, we have to call the detectChange () function to run. The first way to run without this function is because we embed EmbeddedView into ViewContainerRef, so it will be run change detection next time we don't need to run it manually.


The entire code way 2 above Here stackblitz


Ok, these are 2 ways I think of. There are 3 ways to use it custom element, but I will present a separate article. Hope you understand and use dynamic components effectively


0 Comments

×