Skip to content

Commit

Permalink
docs: add host directives guide (#48057)
Browse files Browse the repository at this point in the history
PR Close #48057
  • Loading branch information
jelbourn authored and thePunderWoman committed Nov 15, 2022
1 parent 38b2ed3 commit fc47141
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 1 deletion.
1 change: 1 addition & 0 deletions .pullapprove.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
219 changes: 219 additions & 0 deletions aio/content/guide/directive-composition-api.md
Original file line number Diff line number Diff line change
@@ -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 `<admin-menu>` 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

<admin-menu menuId="top-menu" (menuClosed)="logMenuClosed()">
```

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

<admin-menu id="top-menu" (closed)="logMenuClosed()">
```

## 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.
5 changes: 5 additions & 0 deletions aio/content/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions aio/ngsw-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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}",
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/metadata/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>|{
directive: Type<unknown>,
Expand Down

0 comments on commit fc47141

Please sign in to comment.