From c472c5b872df68447cea94abebf712dc9a3d975f Mon Sep 17 00:00:00 2001 From: David Shevitz Date: Wed, 10 Mar 2021 00:25:18 +0000 Subject: [PATCH] docs: initial commit of content projection topic. (#41143) Content projection is an important part of Angular. However, it has never been documented in the Angular documentation set. This topic attempts to provide the basics of content projection for developers. It is not a comprehensive guide; rather, it is a starting point that we can hopefully expand upon in the future. PR Close #41143 --- .pullapprove.yml | 2 + .../e2e/src/app.e2e-spec.ts | 11 ++ .../content-projection/example-config.json | 0 .../src/app/app.component.css | 3 + .../src/app/app.component.html | 41 +++++ .../src/app/app.component.ts | 42 +++++ .../content-projection/src/app/app.module.ts | 28 ++++ .../src/app/example-zippy.template.html | 8 + .../app/zippy-basic/zippy-basic.component.ts | 10 ++ .../zippy-multislot.component.ts | 11 ++ .../zippy-ngprojectas.component.ts | 11 ++ .../content-projection/src/index.html | 13 ++ .../examples/content-projection/src/main.ts | 12 ++ .../content-projection/stackblitz.json | 10 ++ aio/content/guide/content-projection.md | 157 ++++++++++++++++++ aio/content/guide/example-apps-list.md | 6 + aio/content/navigation.json | 5 + 17 files changed, 370 insertions(+) create mode 100644 aio/content/examples/content-projection/e2e/src/app.e2e-spec.ts create mode 100644 aio/content/examples/content-projection/example-config.json create mode 100644 aio/content/examples/content-projection/src/app/app.component.css create mode 100644 aio/content/examples/content-projection/src/app/app.component.html create mode 100644 aio/content/examples/content-projection/src/app/app.component.ts create mode 100644 aio/content/examples/content-projection/src/app/app.module.ts create mode 100644 aio/content/examples/content-projection/src/app/example-zippy.template.html create mode 100644 aio/content/examples/content-projection/src/app/zippy-basic/zippy-basic.component.ts create mode 100644 aio/content/examples/content-projection/src/app/zippy-multislot/zippy-multislot.component.ts create mode 100644 aio/content/examples/content-projection/src/app/zippy-ngprojectas/zippy-ngprojectas.component.ts create mode 100644 aio/content/examples/content-projection/src/index.html create mode 100644 aio/content/examples/content-projection/src/main.ts create mode 100644 aio/content/examples/content-projection/stackblitz.json create mode 100644 aio/content/guide/content-projection.md diff --git a/.pullapprove.yml b/.pullapprove.yml index 5fa7951e73..8597e56609 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -335,6 +335,8 @@ groups: 'aio/content/guide/component-styles.md', 'aio/content/guide/view-encapsulation.md', 'aio/content/examples/component-styles/**', + 'aio/content/examples/content-projection/**', + 'aio/content/guide/content-projection.md', 'aio/content/guide/dependency-injection.md', 'aio/content/examples/dependency-injection/**', 'aio/content/images/guide/dependency-injection/**', diff --git a/aio/content/examples/content-projection/e2e/src/app.e2e-spec.ts b/aio/content/examples/content-projection/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..f9e6001d4b --- /dev/null +++ b/aio/content/examples/content-projection/e2e/src/app.e2e-spec.ts @@ -0,0 +1,11 @@ +import { browser, element, by } from 'protractor'; + +describe('Component Overview', () => { + + beforeAll(() => browser.get('')); + + it('should display Angular and Content Projection ', async () => { + expect(await element(by.css('h2')).getText()).toEqual('Angular and Content Projection'); + }); + +}); diff --git a/aio/content/examples/content-projection/example-config.json b/aio/content/examples/content-projection/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/content-projection/src/app/app.component.css b/aio/content/examples/content-projection/src/app/app.component.css new file mode 100644 index 0000000000..b7edde24e5 --- /dev/null +++ b/aio/content/examples/content-projection/src/app/app.component.css @@ -0,0 +1,3 @@ +p { + font-family: Lato; + } \ No newline at end of file diff --git a/aio/content/examples/content-projection/src/app/app.component.html b/aio/content/examples/content-projection/src/app/app.component.html new file mode 100644 index 0000000000..69c7744eab --- /dev/null +++ b/aio/content/examples/content-projection/src/app/app.component.html @@ -0,0 +1,41 @@ +

Angular and Content Projection

+ + + +

Is content projection cool?

+
+ + +
+ + + +

+ Is content projection cool? +

+

Let's learn about content projection!

+
+ + +
+ +

Here's a zippy

+ + + + + + It depends on what you do with it. + + + + +
+ +

Let's learn about content projection!

+ + +

Is content projection cool?

+
+ +
diff --git a/aio/content/examples/content-projection/src/app/app.component.ts b/aio/content/examples/content-projection/src/app/app.component.ts new file mode 100644 index 0000000000..e6fb53d482 --- /dev/null +++ b/aio/content/examples/content-projection/src/app/app.component.ts @@ -0,0 +1,42 @@ +import { Component, Directive, Input, TemplateRef, ContentChild, HostBinding, HostListener } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html' +}) +export class AppComponent {} + +@Directive({ + selector: 'button[appExampleZippyToggle]', +}) +export class ZippyToggleDirective { + @HostBinding('attr.aria-expanded') ariaExpanded = this.zippy.expanded; + @HostBinding('attr.aria-controls') ariaControls = this.zippy.contentId; + @HostListener('click') toggleZippy() { + this.zippy.expanded = !this.zippy.expanded; + } + constructor(public zippy: ZippyComponent) {} +} + +// #docregion zippycontentdirective +@Directive({ + selector: '[appExampleZippyContent]' +}) +export class ZippyContentDirective { + constructor(public templateRef: TemplateRef) {} +} +// #enddocregion zippycontentdirective + +let nextId = 0; + +@Component({ + selector: 'app-example-zippy', + templateUrl: 'example-zippy.template.html', +}) +export class ZippyComponent { + contentId = `zippy-${nextId++}`; + @Input() expanded = false; + // #docregion contentchild + @ContentChild(ZippyContentDirective) content: ZippyContentDirective; + // #enddocregion contentchild +} diff --git a/aio/content/examples/content-projection/src/app/app.module.ts b/aio/content/examples/content-projection/src/app/app.module.ts new file mode 100644 index 0000000000..6f7649da7a --- /dev/null +++ b/aio/content/examples/content-projection/src/app/app.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +import { + AppComponent, + ZippyComponent, + ZippyContentDirective, + ZippyToggleDirective, +} from './app.component'; +import { ZippyBasicComponent } from './zippy-basic/zippy-basic.component'; +import { ZippyMultislotComponent } from './zippy-multislot/zippy-multislot.component'; +import { ZippyNgprojectasComponent } from './zippy-ngprojectas/zippy-ngprojectas.component'; + +@NgModule({ + imports: [BrowserModule, FormsModule], + declarations: [ + AppComponent, + ZippyComponent, + ZippyToggleDirective, + ZippyContentDirective, + ZippyBasicComponent, + ZippyMultislotComponent, + ZippyNgprojectasComponent, + ], + bootstrap: [AppComponent] +}) +export class AppModule {} diff --git a/aio/content/examples/content-projection/src/app/example-zippy.template.html b/aio/content/examples/content-projection/src/app/example-zippy.template.html new file mode 100644 index 0000000000..886f54f7ad --- /dev/null +++ b/aio/content/examples/content-projection/src/app/example-zippy.template.html @@ -0,0 +1,8 @@ + + +
+ + + +
+ \ No newline at end of file diff --git a/aio/content/examples/content-projection/src/app/zippy-basic/zippy-basic.component.ts b/aio/content/examples/content-projection/src/app/zippy-basic/zippy-basic.component.ts new file mode 100644 index 0000000000..7f585298ff --- /dev/null +++ b/aio/content/examples/content-projection/src/app/zippy-basic/zippy-basic.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-zippy-basic', + template: ` +

Single-slot content projection

+ +` +}) +export class ZippyBasicComponent {} diff --git a/aio/content/examples/content-projection/src/app/zippy-multislot/zippy-multislot.component.ts b/aio/content/examples/content-projection/src/app/zippy-multislot/zippy-multislot.component.ts new file mode 100644 index 0000000000..243a53d6d8 --- /dev/null +++ b/aio/content/examples/content-projection/src/app/zippy-multislot/zippy-multislot.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-zippy-multislot', + template: ` +

Multi-slot content projection

+ + +` +}) +export class ZippyMultislotComponent {} diff --git a/aio/content/examples/content-projection/src/app/zippy-ngprojectas/zippy-ngprojectas.component.ts b/aio/content/examples/content-projection/src/app/zippy-ngprojectas/zippy-ngprojectas.component.ts new file mode 100644 index 0000000000..39a06914bf --- /dev/null +++ b/aio/content/examples/content-projection/src/app/zippy-ngprojectas/zippy-ngprojectas.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-zippy-ngprojectas', + template: ` +

Content projection with ngProjectAs

+ + +` +}) +export class ZippyNgprojectasComponent {} diff --git a/aio/content/examples/content-projection/src/index.html b/aio/content/examples/content-projection/src/index.html new file mode 100644 index 0000000000..37a08c9169 --- /dev/null +++ b/aio/content/examples/content-projection/src/index.html @@ -0,0 +1,13 @@ + + + + + ContentProjection + + + + + + + + diff --git a/aio/content/examples/content-projection/src/main.ts b/aio/content/examples/content-projection/src/main.ts new file mode 100644 index 0000000000..c7b673cf44 --- /dev/null +++ b/aio/content/examples/content-projection/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/aio/content/examples/content-projection/stackblitz.json b/aio/content/examples/content-projection/stackblitz.json new file mode 100644 index 0000000000..49fb8fc01a --- /dev/null +++ b/aio/content/examples/content-projection/stackblitz.json @@ -0,0 +1,10 @@ +{ + "description": "Binding Syntax", + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ], + "file": "src/app/app.component.ts", + "tags": ["Binding Syntax"] +} diff --git a/aio/content/guide/content-projection.md b/aio/content/guide/content-projection.md new file mode 100644 index 0000000000..6c017d2a8d --- /dev/null +++ b/aio/content/guide/content-projection.md @@ -0,0 +1,157 @@ +# Content projection + +This topic describes how to use content projection to create flexible, reusable components. + +
+ +To view or download the example code used in this topic, see the . + +
+ +Content projection is a pattern in which you insert, or *project*, the content you want to use inside another component. For example, you could have a `Card` component that accepts content provided by another component. + +The following sections describe common implementations of content projection in Angular, including: + +* [Single-slot content projection](#single-slot). With this type of content projection, a component accepts content from a single source. +* [Multi-slot content projection](#multi-slot). In this scenario, a component accepts content from multiple sources. +* [Conditional content projection](#conditional). Components that use conditional content projection render content only when specific conditions are met. + +{@a single-slot } +## Single-slot content projection + +The most basic form of content projection is *single-slot content projection*. Single-slot content projection refers to creating a component into which you can project one component. + +To create a component that uses single-slot content projection: + +1. [Create](guide/component-overview) a component. + +1. In the template for your component, add an `ng-content` element where you want the projected content to appear. + +For example, the following component uses an `ng-content` element to display a message. + + + +With the `ng-content` element in place, users of this component can now project their own message into the component. For example: + + + +
+ +The `ng-content` element is a placeholder that does not create a real DOM element. Custom attributes applied to `ng-content` are ignored. + +
+ +{@a multi-slot} +## Multi-slot content projection + +A component can have multiple slots. Each slot can specify a CSS selector that determines which content goes into that slot. This pattern is referred to as *multi-slot content projection*. With this pattern, you must specify where you want the projected content to appear. You accomplish this task by using the `select` attribute of `ng-content`. + +To create a component that uses multi-slot content projection: + +1. [Create](guide/component-overview) a component. + +1. In the template for your component, add an `ng-content` element where you want the projected content to appear. + +1. Add a `select` attribute to the `ng-content` elements. Angular supports [selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) for any combination of tag name, attribute, CSS class, and the `:not` pseudo-class. + + For example, the following component uses two `ng-content` elements. + + + +Content that uses the `question` attribute is projected into the `ng-content` element with the `select=[question]` attribute. + + + +
+ +
ng-content without a select attribute
+ +If your component includes an `ng-content` element without a `select` attribute, that instance receives all projected components that do not match any of the other `ng-content` elements. + +In the preceding example, only the second `ng-content` element defines a `select` attribute. As a result, the first `ng-content` element receives any other content projected into the component. + +
+ +{@a conditional } + +## Conditional content projection + +If your component needs to _conditionally_ render content, or render content multiple times, you should configure that component to accept an `ng-template` element that contains the content you want to conditionally render. + +Using an `ng-content` element in these cases is not recommended, because when the consumer of a component supplies the content, that content is _always_ initialized, even if the component does not define an `ng-content` element or if that `ng-content` element is inside of an `ngIf` statement. + +With an `ng-template` element, you can have your component explicitly render content based on any condition you want, as many times as you want. Angular will not initialize the content of an `ng-template` element until that element is explicitly rendered. + +The following steps demonstrate a typical implementation of conditional content projection using `ng-template`. + +1. [Create](guide/component-overview) a component. + +1. In the component that accepts an `ng-template` element, use an `ng-container` element to render that template, such as: + + + + + This example uses the `ngTemplateOutlet` directive to render a given `ng-template` element, which you will define in a later step. You can apply an `ngTemplateOutlet` directive to any type of element. This example assigns the directive to an `ng-container` element because the component does not need to render a real DOM element. + +1. Wrap the `ng-container` element in another element, such as a `div` element, and apply your conditional logic. + + + + +1. In the template where you want to project content, wrap the projected content in an `ng-template` element, such as: + + + + + The `ng-template` element defines a block of content that a component can render based on its own logic. A component can get a reference to this template content, or [`TemplateRef`](/api/core/TemplateRef), by using either the [`@ContentChild`](/api/core/ContentChild) or [`@ContentChildren`](/api/core/ContentChildren) decorators. The preceding example creates a custom directive, `appExampleZippyContent`, as an API to mark the `ng-template` for the component's content. With the `TemplateRef`, the component can render the referenced content by using either the [`ngTemplateOutlet`](/api/common/NgTemplateOutlet) directive, or with [`ViewContainerRef.createEmbeddedView`](/api/core/ViewContainerRef#createembeddedview). + +1. Create a directive with a selector that matches the custom attribute for your template. In this directive, inject a TemplateRef instance. + + + + + In the previous step, you added an `ng-template` element with a custom attribute, `appExampleZippyDirective`. This code provides the logic that Angular will use when it encounters that custom attribute. In this case, that logic instructs Angular to instantiate a template reference. + +1. In the component you want to project content into, use `@ContentChild` to get the template of the project content. + + + + + Prior to this step, your application has a component that instantiates a template when certain conditions are met. You've also created a directive that provides a reference to that template. In this last step, the `@ContentChild` decorator instructs Angular to instantiate the template in the designated component. + +
+ + In the case of multi-slot content projection, you can use `@ContentChildren` to get a QueryList of projected elements. + +
+ +{@a ngprojectas } + +## Projecting content in more complex environments + +As described in [Multi-slot Content Projection](#multi-slot), you typically use either an attribute, element, CSS Class, or some combination of all three to identify where to project your content. For example, in the following HTML template, a paragraph tag uses a custom attribute, `question`, to project content into the `app-zippy-multislot` component. + + + +In some cases, you might want to project content as a different element. For example, the content you want to project might be a child of another +element. You can accomplish this by using the `ngProjectAs` attribute. + +For instance, consider the following HTML snippet: + + + + +This example uses an `ng-container` attribute to simulate projecting a component into a more complex structure. + +
+ +
Reminder!
+ +The `ng-container` element is a logical construct that you can use to group other DOM elements; however, the `ng-container` itself is not rendered in the DOM tree. + +
+ +In this example, the content we want to project resides inside another element. To project this content as intended, the template uses the `ngProjectAs` attribute. With `ngProjectAs`, the entire `ng-container` element is projected into a component using the `question` selector. diff --git a/aio/content/guide/example-apps-list.md b/aio/content/guide/example-apps-list.md index 1cab4c8eaf..ea803e728b 100644 --- a/aio/content/guide/example-apps-list.md +++ b/aio/content/guide/example-apps-list.md @@ -154,6 +154,12 @@ For more information, see [Built-in directives](guide/built-in-directives). Demonstrates Angular built-in template functions. For more information, see the [`$any()` type cast function section](guide/template-expression-operators#the-any-type-cast-function) of [Template expression operators](guide/template-expression-operators). +### Content projection + + + +Demonstrates how to use Angular's content projection feature when creating reusable components. + ### Interpolation diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 38bb3a6bf3..d5b6a9e2d9 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -135,6 +135,11 @@ "title": "Sharing data between child and parent directives and components", "tooltip": "Introductory guide to sharing data between parent and child directives or components." }, + { + "url": "guide/content-projection", + "title": "Content Projection", + "tooltip": "Learn how to create reusable components using Angular's content projection feature." + }, { "url": "guide/dynamic-component-loader", "title": "Dynamic Components",