In Angular, a component is a special directive that has its own template (HTML) and style (CSS). So using components makes our code highly decoupled, reusable, and easily extensible. The usual component definitions are as follows:

Demo.com ponent. Ts:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component'.templateUrl: './demo.component.html'.styleUrls: ['./demo.component.scss']})export class DemoComponent implements OnInit {

	constructor() {
	}

	ngOnInit() {
	}
}
Copy the code

Demo.com ponent. HTML:

<div class="demo">
	<h2>Demo-component - I'm a simple component</h2>
</div>
Copy the code

Demo.com ponent. SCSS:

.demo { padding: 10px; border: 2px solid red; h2 { margin: 0; color: #262626; }}Copy the code

At this point we refer to the component, which renders the parsed content of the component:

<demo-component></demo-component>
Copy the code

Suppose there is a requirement that this component can accept external projections, that is, that the component ends up presenting more than what it defines? This is where ng-content, the main character of this article, comes in.

Simple projection

HTML: demo.component.html: demo.component.scss: demo.component.html: demo.component.scss: demo.component.html: demo.component.scss:

Demo.com ponent. HTML:

<div class="demo">
	<h2>Demo-component - A component that can be embedded with external content</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
</div>
Copy the code

Demo.com ponent. SCSS:

.demo { padding: 10px; border: 2px solid red; h2 { margin: 0; color: #262626; } .content { padding: 10px; margin-top: 10px; line-height: 20px; color: #FFFFFF; background-color: #de7d28; }}Copy the code

The background color of the container is deliberately defined as orange for effect display.

When we reference the component, we can project content from the outside, which will be shown in the orange area:

<demo-component>I am externally embedded content</demo-component>
Copy the code

Targeted projection

If there are several at the same time, how will external content be projected?

Let’s take a look at an example first. To differentiate, I’ll add a modified demo.component.html and demo.component.scss in the blue area as follows:

Demo.com ponent. HTML:

<div class="demo">
	<h2>Demo-component - A component that can be embedded with external content</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content></ng-content>
	</div>
</div>
Copy the code

Demo.com ponent. SCSS:

.demo { padding: 10px; border: 2px solid red; h2 { margin: 0; color: #262626; } .content { padding: 10px; margin-top: 10px; line-height: 20px; color: #FFFFFF; background-color: #de7d28; &.blue { background-color: blue; }}}Copy the code

To reference this component:

<demo-component>I am externally embedded content</demo-component>
Copy the code

At this point, we see external content projected into the blue area:

Of course, if you place the orange area code after the blue area code, the external content is projected to the orange area:

So we can see from the example above that external content will be projected into the last one of the component templates if simple is also present.

So given this question, we might be thinking, can we target the external content into the corresponding middle? The answer is clearly yes.

To handle this, support a SELECT attribute that lets you project specific content in a specific place. This property supports CSS selectors (tag selectors, class selectors, property selectors…) To match what you want. If the SELECT attribute is not set on ng-content, it will accept either the entire content or the content that does not match any other Ng-Content element.

For example, demo.component. HTML and demo.component. SCSS are modified as follows:

Demo.com ponent. HTML:

<div class="demo">
	<h2>Demo-component - A component that can be embedded with external content</h2>
	<div class="content">
		<ng-content></ng-content>
	</div>
	<div class="content blue">
		<ng-content select="header"></ng-content>
	</div>
	<div class="content red">
		<ng-content select=".demo2"></ng-content>
	</div>
	<div class="content green">
		<ng-content select="[name=demo3]"></ng-content>
	</div>
</div>
Copy the code

Demo.com ponent. SCSS:

.demo { padding: 10px; border: 2px solid red; h2 { margin: 0; color: #262626; } .content { padding: 10px; margin-top: 10px; line-height: 20px; color: #FFFFFF; background-color: #de7d28; &.blue { background-color: blue; } &.red { background-color: red; } &.green { background-color: green; }}}Copy the code

As you can see from the code above, the blue area will receive the tag header, the red area will receive the div with class “demo2”, the green area will receive the div with attribute name “Demo3”, and the orange area will receive the rest of the external content (start, I am the external embedded content, end).

To reference this component:

<demo-component>At first, I'm the external embedded content,<header>I'm external embedded content, I'm in the header</header>
	<div class="demo2">I'm an external embedded content, and my div class is "demo2"</div>
	<div name="demo3">I'm an external embedded content demo, and the attribute name of my div is "Demo3"</div>The end of the</demo-component>
Copy the code

At this point, we will see the external content projected to the specified.

Expand the knowledge

ngProjectAs

We now know that the select attribute of ng-content can specify that external content is projected to the specified.

There is a limitation to correctly projecting content from the SELECT attribute – whether it is the tag header, div with class “demo2”, or div with attribute name “demo3”, all of these tags are direct child nodes of the component tag.

So what happens if it’s not a direct child node? We’ll simply modify the code that references the Demo-Component by placing the header tag in a div as follows:

<demo-component>At first, I'm the external embedded content,<div>
		<header>I'm external embedded content, I'm in the header</header>
	</div>
	<div class="demo2">I'm an external embedded content, and my div class is "demo2"</div>
	<div name="demo3">I'm an external embedded content demo, and the attribute name of my div is "Demo3"</div>The end of the</demo-component>
Copy the code

Now, we see that the contents of the header tag are no longer projected into the blue area, but into the orange area.
does not match the previous tag header, so the content is projected into the orange area
.

To solve this problem, we must use the ngProjectAs attribute, which can be applied to any element. Details are as follows:

<demo-component>At first, I'm the external embedded content,<div ngProjectAs="header">
		<header>I'm external embedded content, I'm in the header</header>
	</div>
	<div class="demo2">I'm an external embedded content, and my div class is "demo2"</div>
	<div name="demo3">I'm an external embedded content demo, and the attribute name of my div is "Demo3"</div>The end of the</demo-component>
Copy the code

By setting the ngProjectAs attribute, the div of the tag header points to SELECT =”header”, and the contents of the tag header are projected into the blue area:

<ng-content>Don’t “produce” content

Do a test

To experiment, define a demo-child-component:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-child-component'.template: '

I'm demo-child-component

'
}) export class DemoChildComponent implements OnInit { constructor() { } ngOnInit() { console.log('Demo-child-Component initialization complete! '); }}Copy the code

Change the demo-component component to:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component'.template: `  
      
`
}) export class DemoComponent implements OnInit { show = true; constructor() { } ngOnInit() { } } Copy the code

Then project the demo-child-Component to the demo-Component:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
Copy the code

At this point, in the console we see a print-out that demo-child-Component initialization is complete! These words. But when we click the button to switch, the Demo-child-Component initialization is complete! This means that our Demo-child-component was instantiated only once – never destroyed or recreated.

Why does this happen?

A reason for


does not “produce” content, it just projects existing content. You can think of it as equivalent to node.appendChild(el) or the $(node).append(el) method in jQuery: with these methods, the node is not cloned, it is simply moved to its new location. Therefore, the life cycle of projected content is bound to where it is declared, rather than displayed there.

This also explains the principle of the previous question: if there are several at the same time, how will the external content be projected?

There are two reasons for this behavior: expected consistency and performance. What “expected consistency” means is that as a developer, you can guess the behavior of an application based on its code. Suppose I write the following code:

<demo-component>
	<demo-child-component></demo-child-component>
</demo-component>
Copy the code

Obviously the Demo-child-component will be instantiated once, but now suppose we use a component from a third-party library:

<third-party-wrapper>
    <demo-child-component></demo-child-component>
</third-party-wrapper>
Copy the code

If a third-party library had control over the demo-Child-Component’s life cycle, I wouldn’t know how many times it was instantiated. The only way to do this is to look at the code of third-party libraries to understand their internal processing logic. The point of binding a component’s life cycle to our application component instead of a wrapper is that the developer can control that the counter is instantiated only once, without knowing the internal code of a third-party library.

Performance is more important. Because ng-content just moves elements, it can be done at compile time rather than run time, greatly reducing the actual application’s effort.

The solution

To allow components to control instantiation of projected child components, we can do this in two ways: we can use

elements and ngTemplateOutlet around our content, or we can use structural directives with “*” syntax. For simplicity, we will use the

syntax in the example.

Change the demo-component component to:

import { Component, OnInit } from '@angular/core';

@Component({
	selector: 'demo-component'.template: `  
      
`
}) export class DemoComponent implements OnInit { @ContentChild(TemplateRef) template: TemplateRef; show = true; constructor() { } ngOnInit() { } } Copy the code

We then include the Demo-child-component in the ng-template:

<demo-component>
    <ng-template>
        <demo-child-component></demo-child-component>
    </ng-template>
</demo-component>
Copy the code

At this point, when we click the button to switch, the console will print out demo-child-Component initialization complete! These words.

The resources

ng-content: The hidden docs


Please indicate the source of reprint, thank you!