Skip to content

docs: AttributeDirectives guide for CLI #19771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aio/content/examples/attribute-directives/plnkr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files":[
"!**/*.d.ts",
"!**/*.js",
"!app/*.[1,2,3].*"
"!app/*.[0,1,2,3].*"
],
"tags": ["attribute", "directive"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// #docregion
import { Directive } from '@angular/core';

@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor() { }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* tslint:disable:no-unused-variable */
// #docregion
import { Directive, ElementRef, Input } from '@angular/core';
import { Directive, ElementRef } from '@angular/core';

@Directive({ selector: '[appHighlight]' })
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = 'yellow';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* tslint:disable:no-unused-variable member-ordering */
// #docplaster
// #docregion imports,
import { Directive, ElementRef, HostListener } from '@angular/core';
// #enddocregion imports,
import { Input } from '@angular/core';
// #docregion
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
selector: '[appHighlight]'
Expand Down Expand Up @@ -35,7 +38,7 @@ export class HighlightDirective {
// #enddocregion color

// #docregion color-2
@Input() myHighlight: string;
@Input() appHighlight: string;
// #enddocregion color-2
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* tslint:disable:member-ordering */
// #docregion
// #docregion, imports
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
// #enddocregion imports

@Directive({
selector: '[appHighlight]'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
/* tslint:disable:member-ordering */
// #docregion imports,
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
// #enddocregion imports

@Directive({
selector: '[appHighlight]'
Expand Down
148 changes: 65 additions & 83 deletions aio/content/guide/attribute-directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ An attribute directive minimally requires building a controller class annotated
the attribute.
The controller class implements the desired directive behavior.

This page demonstrates building a simple _myHighlight_ attribute
This page demonstrates building a simple _appHighlight_ attribute
directive to set an element's background color
when the user hovers over that element. You can apply it like this:

Expand All @@ -43,106 +43,84 @@ when the user hovers over that element. You can apply it like this:

### Write the directive code

Follow the [setup](guide/setup) instructions for creating a new local project
named <code>attribute-directives</code>.
Create the directive class file in a terminal window with this CLI command.

Create the following source file in the indicated folder:
<code-example language="sh" class="code-shell">
ng generate directive highlight
</code-example>

<code-example path="attribute-directives/src/app/highlight.directive.1.ts" title="src/app/highlight.directive.ts"></code-example>
The CLI creates `src/app/highlight.directive.ts`, a corresponding test file (`.../spec.ts`, and _declares_ the directive class in the root `AppModule`.

<div class="l-sub-section">

_Directives_ must be declared in [Angular Modules](guide/ngmodule) in the same manner as _components_.

</div >

The generated `src/app/highlight.directive.ts` is as follows:

The `import` statement specifies symbols from the Angular `core`:
<code-example path="attribute-directives/src/app/highlight.directive.0.ts" title="src/app/highlight.directive.ts"></code-example>

1. `Directive` provides the functionality of the `@Directive` decorator.
1. `ElementRef` [injects](guide/dependency-injection) into the directive's constructor
so the code can access the DOM element.
1. `Input` allows data to flow from the binding expression into the directive.
The imported `Directive` symbol provides the Angular the `@Directive` decorator.

Next, the `@Directive` decorator function contains the directive metadata in a configuration object
as an argument.
The `@Directive` decorator's lone configuration property specifies the directive's
[CSS attribute selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors), `[appHighlight]`.

`@Directive` requires a CSS selector to identify
the HTML in the template that is associated with the directive.
The [CSS selector for an attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)
is the attribute name in square brackets.
Here, the directive's selector is `[myHighlight]`.
Angular locates all elements in the template that have an attribute named `myHighlight`.
It's the brackets (`[]`) that make it an attribute selector.
Angular locates each element in the template that has an attribute named `appHighlight` and applies the logic of this directive to that element.

The _attribute selector_ pattern explains the name of this kind of directive.

<div class="l-sub-section">

### Why not call it "highlight"?
#### Why not "highlight"?

Though *highlight* is a more concise name than *myHighlight* and would work,
a best practice is to prefix selector names to ensure
Though *highlight* would be a more concise selector than *appHighlight* and it would work,
the best practice is to prefix selector names to ensure
they don't conflict with standard HTML attributes.
This also reduces the risk of colliding with third-party directive names.
The CLI added the `app` prefix for you.

Make sure you do **not** prefix the `highlight` directive name with **`ng`** because
that prefix is reserved for Angular and using it could cause bugs that are difficult to diagnose.
For a simple demo, the short prefix, `my`, helps distinguish your custom directive.

</div>


After the `@Directive` metadata comes the directive's controller class,
called `HighlightDirective`, which contains the logic for the directive.
Exporting `HighlightDirective` makes it accessible to other components.

Angular creates a new instance of the directive's controller class for
each matching element, injecting an Angular `ElementRef`
into the constructor.
`ElementRef` is a service that grants direct access to the DOM element
through its `nativeElement` property.
called `HighlightDirective`, which contains the (currently empty) logic for the directive.
Exporting `HighlightDirective` makes the directive accessible.

{@a apply-directive}

## Apply the attribute directive
Now edit the generated `src/app/highlight.directive.ts` to look as follows:

To use the new `HighlightDirective`, create a template that
applies the directive as an attribute to a paragraph (`<p>`) element.
In Angular terms, the `<p>` element is the attribute **host**.
<code-example path="attribute-directives/src/app/highlight.directive.1.ts" title="src/app/highlight.directive.ts"></code-example>

Put the template in its own <code>app.component.html</code>
file that looks like this:
The `import` statement specifies an additional `ElementRef` symbol from the Angular `core` library:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the : intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so


<code-example path="attribute-directives/src/app/app.component.1.html" title="src/app/app.component.html"></code-example>
You use the `ElementRef`in the directive's constructor
to [inject](guide/dependency-injection) a reference to the host DOM element,
the element to which you applied `appHighlight`.

Now reference this template in the `AppComponent`:
`ElementRef` grants direct access to the host DOM element
through its `nativeElement` property.

<code-example path="attribute-directives/src/app/app.component.ts" title="src/app/app.component.ts"></code-example>
This first implementation sets the background color of the host element to yellow.

Next, add an `import` statement to fetch the `Highlight` directive and
add that class to the `declarations` NgModule metadata. This way Angular
recognizes the directive when it encounters `myHighlight` in the template.
{@a apply-directive}

<code-example path="attribute-directives/src/app/app.module.ts" title="src/app/app.module.ts"></code-example>
## Apply the attribute directive

Now when the app runs, the `myHighlight` directive highlights the paragraph text.
To use the new `HighlightDirective`, add a paragraph (`<p>`) element to the template of the root `AppComponent` and apply the directive as an attribute.

<figure>
<img src="generated/images/guide/attribute-directives/first-highlight.png" alt="First Highlight">
</figure>
<code-example path="attribute-directives/src/app/app.component.1.html" title="src/app/app.component.html" region="applied"></code-example>

<div class="l-sub-section">
Now run the application to see the `HighlightDirective` in action.

<h3 class="no-toc">Your directive isn't working?</h3>

Did you remember to add the directive to the `declarations` attribute of `@NgModule`?
It is easy to forget!
Open the console in the browser tools and look for an error like this:

<code-example format="nocode">
EXCEPTION: Template parse errors:
Can't bind to 'myHighlight' since it isn't a known property of 'p'.
<code-example language="sh" class="code-shell">
ng serve
</code-example>

Angular detects that you're trying to bind to *something* but it can't find this directive
in the module's `declarations` array.
After specifying `HighlightDirective` in the `declarations` array,
Angular knows it can apply the directive to components declared in this module.

</div>

To summarize, Angular found the `myHighlight` attribute on the `<p>` element.
To summarize, Angular found the `appHighlight` attribute on the **host** `<p>` element.
It created an instance of the `HighlightDirective` class and
injected a reference to the `<p>` element into the directive's constructor
which sets the `<p>` element's background style to yellow.
Expand All @@ -151,15 +129,14 @@ which sets the `<p>` element's background style to yellow.

## Respond to user-initiated events

Currently, `myHighlight` simply sets an element color.
Currently, `appHighlight` simply sets an element color.
The directive could be more dynamic.
It could detect when the user mouses into or out of the element
and respond by setting or clearing the highlight color.

Begin by adding `HostListener` to the list of imported symbols;
add the `Input` symbol as well because you'll need it soon.
Begin by adding `HostListener` to the list of imported symbols.

<code-example path="attribute-directives/src/app/highlight.directive.ts" linenums="false" title="src/app/highlight.directive.ts (imports)" region="imports"></code-example>
<code-example path="attribute-directives/src/app/highlight.directive.2.ts" linenums="false" title="src/app/highlight.directive.ts (imports)" region="imports"></code-example>

Then add two eventhandlers that respond when the mouse enters or leaves,
each adorned by the `HostListener` decorator.
Expand All @@ -180,8 +157,10 @@ There are at least three problems with _that_ approach:

</div>

The handlers delegate to a helper method that sets the color on the DOM element, `el`,
which you declare and initialize in the constructor.
The handlers delegate to a helper method that sets the color on the host DOM element, `el`.

The helper method, `highlight`, was extracted from the constructor.
The revised constructor simply declares the injected `el: ElementRef`.

<code-example path="attribute-directives/src/app/highlight.directive.2.ts" linenums="false" title="src/app/highlight.directive.ts (constructor)" region="ctor"></code-example>

Expand All @@ -203,7 +182,10 @@ the mouse hovers over the `p` and disappears as it moves out.
Currently the highlight color is hard-coded _within_ the directive. That's inflexible.
In this section, you give the developer the power to set the highlight color while applying the directive.

Start by adding a `highlightColor` property to the directive class like this:
Begin by adding `Input` to the list of symbols imported from `@angular/core`.
<code-example path="attribute-directives/src/app/highlight.directive.3.ts" linenums="false" title="src/app/highlight.directive.ts (imports)" region="imports"></code-example>

Add a `highlightColor` property to the directive class like this:

<code-example path="attribute-directives/src/app/highlight.directive.2.ts" linenums="false" title="src/app/highlight.directive.ts (highlightColor)" region="color"></code-example>

Expand Down Expand Up @@ -232,16 +214,16 @@ That's good, but it would be nice to _simultaneously_ apply the directive and se

<code-example path="attribute-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (color)" region="color"></code-example>

The `[myHighlight]` attribute binding both applies the highlighting directive to the `<p>` element
The `[appHighlight]` attribute binding both applies the highlighting directive to the `<p>` element
and sets the directive's highlight color with a property binding.
You're re-using the directive's attribute selector (`[myHighlight]`) to do both jobs.
You're re-using the directive's attribute selector (`[appHighlight]`) to do both jobs.
That's a crisp, compact syntax.

You'll have to rename the directive's `highlightColor` property to `myHighlight` because that's now the color property binding name.
You'll have to rename the directive's `highlightColor` property to `appHighlight` because that's now the color property binding name.

<code-example path="attribute-directives/src/app/highlight.directive.2.ts" linenums="false" title="src/app/highlight.directive.ts (renamed to match directive selector)" region="color-2"></code-example>

This is disagreeable. The word, `myHighlight`, is a terrible property name and it doesn't convey the property's intent.
This is disagreeable. The word, `appHighlight`, is a terrible property name and it doesn't convey the property's intent.

{@a input-alias}

Expand All @@ -254,14 +236,14 @@ Restore the original property name and specify the selector as the alias in the
<code-example path="attribute-directives/src/app/highlight.directive.ts" linenums="false" title="src/app/highlight.directive.ts (color property with alias)" region="color"></code-example>

_Inside_ the directive the property is known as `highlightColor`.
_Outside_ the directive, where you bind to it, it's known as `myHighlight`.
_Outside_ the directive, where you bind to it, it's known as `appHighlight`.

You get the best of both worlds: the property name you want and the binding syntax you want:

<code-example path="attribute-directives/src/app/app.component.html" linenums="false" title="src/app/app.component.html (color)" region="color"></code-example>

Now that you're binding to `highlightColor`, modify the `onMouseEnter()` method to use it.
If someone neglects to bind to `highlightColor`, highlight in red:
Now that you're binding via the alias to the `highlightColor`, modify the `onMouseEnter()` method to use that property.
If someone neglects to bind to `appHighlightColor`, highlight the host element in red:

<code-example path="attribute-directives/src/app/highlight.directive.3.ts" linenums="false" title="src/app/highlight.directive.ts (mouse enter)" region="mouse-enter"></code-example>

Expand Down Expand Up @@ -308,7 +290,7 @@ then with the `defaultColor`, and falls back to "red" if both properties are und

<code-example path="attribute-directives/src/app/highlight.directive.ts" linenums="false" title="src/app/highlight.directive.ts (mouse-enter)" region="mouse-enter"></code-example>

How do you bind to a second property when you're already binding to the `myHighlight` attribute name?
How do you bind to a second property when you're already binding to the `appHighlight` attribute name?

As with components, you can add as many directive property bindings as you need by stringing them along in the template.
The developer should be able to write the following template HTML to both bind to the `AppComponent.color`
Expand Down Expand Up @@ -398,6 +380,6 @@ Now apply that reasoning to the following example:
The template and its component trust each other.
The `color` property doesn't require the `@Input` decorator.

* The `myHighlight` property on the left refers to an _aliased_ property of the `HighlightDirective`,
* The `appHighlight` property on the left refers to an _aliased_ property of the `HighlightDirective`,
not a property of the template's component. There are trust issues.
Therefore, the directive property must carry the `@Input` decorator.
2 changes: 1 addition & 1 deletion aio/content/guide/structural-directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ They shape or reshape the DOM's _structure_, typically by adding, removing, or m
elements.

As with other directives, you apply a structural directive to a _host element_.
The directive then does whatever it's supposed to do with that host element and its descendents.
The directive then does whatever it's supposed to do with that host element and its descendants.

Structural directives are easy to recognize.
An asterisk (*) precedes the directive attribute name as in this example.
Expand Down