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",