From bc97322641ea88c78696a2f96ecbab7f0ee3c208 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Mon, 14 Nov 2022 12:59:15 -0800 Subject: [PATCH] docs: add host directives guide (#48057) PR Close #48057 --- .pullapprove.yml | 1 + .../guide/directive-composition-api.md | 219 ++++++++++++++++++ aio/content/navigation.json | 5 + aio/ngsw-config.json | 2 + packages/core/src/metadata/directives.ts | 7 +- 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 aio/content/guide/directive-composition-api.md diff --git a/.pullapprove.yml b/.pullapprove.yml index 96db426ef3c..0c3e5fbf00f 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -325,6 +325,7 @@ groups: 'aio/content/guide/two-way-binding.md', 'aio/content/examples/two-way-binding/**/{*,.*}', 'aio/content/guide/built-in-directives.md', + 'aio/content/guide/directive-composition-api.md', 'aio/content/examples/built-in-directives/**/{*,.*}', 'aio/content/images/guide/built-in-directives/**/{*,.*}', 'aio/content/guide/template-reference-variables.md', diff --git a/aio/content/guide/directive-composition-api.md b/aio/content/guide/directive-composition-api.md new file mode 100644 index 00000000000..7b2a101228d --- /dev/null +++ b/aio/content/guide/directive-composition-api.md @@ -0,0 +1,219 @@ +# Directive composition API + +Angular directives offer a great way to encapsulate reusable behaviors— directives can apply +attributes, CSS classes, and event listeners to an element. + +The *directive composition API* lets you apply directives to a component's host element from +_within_ the component. + +## Adding directives to a component + +You apply directives to a component by adding a `hostDirectives` property to a component's +decorator. We call such directives *host directives*. + +In this example, we apply the directive `MenuBehavior` to the host element of `AdminMenu`. This +works similarly to applying the `MenuBehavior` to the `` element in a template. + +```typescript +@Component({ + selector: 'admin-menu', + template: 'admin-menu.html', + hostDirectives: [MenuBehavior], +}) +export class AdminMenu { } +``` + +When the framework renders a component, Angular also creates an instance of each host directive. The +directives' host bindings apply to the component's host element. By default, host directive inputs +and outputs are not exposed as part of the component's public API. See +[Including inputs and outputs](#including-inputs-and-outputs) below for more information. + +**Angular applies host directives statically at compile time.** You cannot dynamically add +directives at runtime. + +**Directives used in `hostDirectives` must be `standalone: true`.** + +**Angular ignores the `selector` of directives applied in the `hostDirectives` property.** + +## Including inputs and outputs + +When you apply `hostDirectives` to your component, the inputs and outputs from the host directives +are not included in your component's API by default. You can explicitly include inputs and outputs +in your component's API by expanding the entry in `hostDirectives`: + +```typescript +@Component({ + selector: 'admin-menu', + template: 'admin-menu.html', + hostDirectives: [{ + directive: MenuBehavior, + inputs: ['menuId'], + outputs: ['menuClosed'], + }], +}) +export class AdminMenu { } +``` + +By explicitly specifying the inputs and outputs, consumers of the component with `hostDirective` can +bind them in a template: + +```html + + +``` + +Furthermore, you can alias inputs and outputs from `hostDirective` to customize the API of your +component: + +```typescript +@Component({ + selector: 'admin-menu', + template: 'admin-menu.html', + hostDirectives: [{ + directive: MenuBehavior, + inputs: ['menuId: id'], + outputs: ['menuClosed: closed'], + }], +}) +export class AdminMenu { } +``` + +```html + + +``` + +## Adding directives to another directive + +You can also add `hostDirectives` to other directives, in addition to components. This enables the +transitive aggregation of multiple behaviors. + +In the following example, we define two directives, `Menu` and `Tooltip`. We then compose the behavior +of these two directives in `MenuWithTooltip`. Finally, we apply `MenuWithTooltip` +to `SpecializedMenuWithTooltip`. + +When `SpecializedMenuWithTooltip` is used in a template, it creates instances of all of `Menu` +, `Tooltip`, and `MenuWithTooltip`. Each of these directives' host bindings apply to the host +element of `SpecializedMenuWithTooltip`. + +```typescript +@Directive({...}) +export class Menu { } + +@Directive({...}) +export class Tooltip { } + +// MenuWithTooltip can compose behaviors from multiple other directives +@Directive({ + hostDirectives: [Tooltip, Menu], +}) +export class MenuWithTooltip { } + +// CustomWidget can apply the already-composed behaviors from MenuWithTooltip +@Directive({ + hostDirectives: [MenuWithTooltip], +}) +export class SpecializedMenuWithTooltip { } +``` + +## Host directive semantics + +### Directive execution order + +Host directives go through the same lifecycle as components and directives used directly in a +template. However, host directives always execute their constructor, lifecycle hooks, and bindings _ +before_ the component or directive on which they are applied. + +The following example shows minimal use of a host directive: + +```typescript +@Component({ + selector: 'admin-menu', + template: 'admin-menu.html', + hostDirectives: [MenuBehavior], +}) +export class AdminMenu { } +``` + +The order of execution here is: + +1. `MenuBehavior` instantiated +2. `AdminMenu` instantiated +3. `MenuBehavior` receives inputs (`ngOnInit`) +4. `AdminMenu` receives inputs (`ngOnInit`) +5. `MenuBehavior` applies host bindings +6. `AdminMenu` applies host bindings + +This order of operations means that components with `hostDirectives` can override any host bindings +specified by a host directive. + +This order of operations extends to nested chains of host directives, as shown in the following +example. + +```typescript +@Directive({...}) +export class Tooltip { } + +@Directive({ + hostDirectives: [Tooltip], +}) +export class CustomTooltip { } + +@Directive({ + hostDirectives: [CustomTooltip], +}) +export class EvenMoreCustomTooltip { } +``` + +In the example above, the order of execution is: + +1. `Tooltip` instantiated +2. `CustomTooltip` instantiated +3. `EvenMoreCustomTooltip` instantiated +4. `Tooltip` receives inputs (`ngOnInit`) +5. `CustomTooltip` receives inputs (`ngOnInit`) +6. `EvenMoreCustomTooltip` receives inputs (`ngOnInit`) +7. `Tooltip` applies host bindings +8. `CustomTooltip` applies host bindings +9. `EvenMoreCustomTooltip` applies host bindings + +### Dependency injection + +A component or directive that specifies `hostDirectives` can inject the instances of those host +directives and vice versa. + +When applying host directives to a component, both the component and host directives can define +providers. + +If a component or directive with `hostDirectives` and those host directives both provide the same +injection token, the providers defined by class with `hostDirectives` take precedence over providers +defined by the host directives. + +### Performance + +While the directive composition API offers a powerful tool for reusing common behaviors, excessive +use of host directives can impact your application's memory use. If you create components or +directives that use _many_ host directives, you may inadvertently balloon the memory used by your +application. + +The following example shows a component that applies several host directives. + +```typescript +@Component({ + hostDirectives: [ + DisabledState, + RequiredState, + ValidationState, + ColorState, + RippleBehavior, + ], +}) +export class CustomCheckbox { } +``` + +This example declares a custom checkbox component that includes five host directives. This +means that Angular will create six objects each time a `CustomCheckbox` renders— one for the +component and one for each host directive. For a few checkboxes on a page, this won't pose any +significant issues. However, if your page renders _hundreds_ of checkboxes, such as in a table, then +you could start to see an impact of the additional object allocations. Always be sure to profile +your application to determine the right composition pattern for your use case. diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 6f5dd41276a..ca0410685ab 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -269,6 +269,11 @@ "url": "guide/structural-directives", "title": "Structural directives", "tooltip": "Structural directives manipulate the layout of the page." + }, + { + "url": "guide/directive-composition-api", + "title": "Directive composition API", + "tooltip": "Apply directive to host elements." } ] }, diff --git a/aio/ngsw-config.json b/aio/ngsw-config.json index a3ef676e209..ef327a74ce5 100644 --- a/aio/ngsw-config.json +++ b/aio/ngsw-config.json @@ -90,6 +90,7 @@ "!/**/api/common/SelectControlValueAccessor-*", "!/**/api/common/index/MaxLengthValidator-*", "!/**/cookbook/ts-to-js*", + "!/analytics/{0,1}", "!/apf/{0,1}", "!/api/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "!/api/*/testing/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", @@ -102,6 +103,7 @@ "!/api/testing/*-*", "!/api/upgrade/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)", "!/api/upgrade/*/index/*", + "!/cli/usage-analytics-gathering/{0,1}", "!/config/solution-tsconfig/{0,1}", "!/config/tsconfig/{0,1}", "!/devtools/{0,1}", diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index b67025f5492..92a0e2f9d8e 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -333,8 +333,13 @@ export interface Directive { /** * Standalone directives that should be applied to the host whenever the directive is matched. - * By default none of the inputs or outputs of the host directives will be available on the host, + * By default, none of the inputs or outputs of the host directives will be available on the host, * unless they are specified in the `inputs` or `outputs` properties. + * + * You can additionally alias inputs and outputs by putting a colon and the alias after the + * original input or output name. For example, if a directive applied via `hostDirectives` + * defines an input named `menuDisabled`, you can alias this to `disabled` by adding + * `'menuDisabled: disabled'` as an entry to `inputs`. */ hostDirectives?: (Type|{ directive: Type,