docs: move template ref vars doc to concepts section clarify, add scope section, and update headers (#31195)

Fixes #31186. This commit adds more context about the behavior
of template reference variables in nested templates and moves
doc into concepts section.

PR Close #31195
This commit is contained in:
Kapunahele Wong 2019-06-21 15:31:19 -04:00 committed by Joey Perrott
parent 60e377ed77
commit e3d2f46bbd
5 changed files with 226 additions and 37 deletions

View File

@ -0,0 +1,14 @@
h3 {
font-weight: 700;
}
pre, .wrapper {
background-color: rgb(240, 250, 250);
padding: 1rem;
border: 1px solid #444;
}
input {
margin: .5rem;
padding: .5rem;
}

View File

@ -39,8 +39,7 @@
<h2>Reference variables, forms, and NgForm</h2>
<!-- #docregion ngForm -->
<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">
<label for="name"
>Name <input class="form-control" name="name" ngModel required />
<label for="name">Name <input class="form-control" name="name" ngModel required />
</label>
<button type="submit">Submit</button>
</form>
@ -53,3 +52,60 @@
<p>JSON: {{ itemForm.form.value | json }}</p>
<hr />
<h2>Template Reference Variable Scope</h2>
<p>This section demonstrates in which situations you can access local template reference variables (<code>#ref</code>).</p>
<h3>Accessing in a child template</h3>
<!-- Accessing a template reference variable from an inner template
works as the context is inherited. Try changing the text in the
input to see how it is immediately reflected through the template
reference variable. -->
<div class="example">
<!-- #docregion template-ref-vars-scope1 -->
<input #ref1 type="text" [(ngModel)]="firstExample" />
<span *ngIf="true">Value: {{ ref1.value }}</span>
<!-- #enddocregion template-ref-vars-scope1 -->
</div>
<!-- In this case there's a "hidden" ng-template around the
span and the definition of the variable is outside of it. Thus, you access a template variable from a parent template (which is logical as the context is inherited). -->
<p>Here's the desugared syntax:</p>
<pre><code [innerText]="desugared1"></code></pre>
<h3>Accessing from outside parent template. (Doesn't work.)</h3>
<!-- Accessing the template reference variable from outside the parent template does
not work. The value to the right is empty and changing the
value of the input will have no effect. -->
<div class="example">
<!-- #docregion template-ref-vars-scope2 -->
<input *ngIf="true" #ref2 type="text" [(ngModel)]="secondExample" />
<!-- <span>Value: {{ ref2?.value }}</span> -->
<!-- uncomment the above and the app breaks -->
<!-- #enddocregion template-ref-vars-scope2 -->
</div>
<p>Here's the desugared syntax:</p>
<pre><code [innerText]="desugared2"></code></pre>
<h3>*ngFor and template reference variable scope</h3>
<!-- The template is instantiated twice because *ngFor iterates
over the two items in the array, so it's impossible to define what ref2 is referencing. -->
<pre><code [innerText]="ngForExample"></code></pre>
<h3>Accessing a on an <code>ng-template</code></h3>
See the console output to see that when you declare the variable on an <code>ng-template</code>, the variable refers to a <code>TemplateRef</code> instance, which represents the template.
<!-- #docregion template-ref -->
<ng-template #ref3></ng-template>
<button (click)="log(ref3)">Log type of #ref</button>
<!-- #enddocregion template-ref -->

View File

@ -8,7 +8,38 @@ import { NgForm } from '@angular/forms';
styleUrls: ['./app.component.css']
})
export class AppComponent {
@ViewChild('itemForm') form: NgForm;
public firstExample = 'Hello, World!';
public secondExample = 'Hello, World!';
public ref2 = '';
public desugared1 = `
<input #ref1 type="text" [(ngModel)]="firstExample" /><!-- A new template! -->
<ng-template [ngIf]="true">
<!-- and it works -->
<span>Value: {{ ref1.value }}</span>
</ng-template>`
;
public desugared2 = `<ng-template [ngIf]="true">
<!-- The reference, ref2, is defined within a template -->
<input #ref2 type="text" [(ngModel)]="secondExample" />
</ng-template>
<!-- Attempting to access ref2 from outside the above template doesn't work. -->
<span>Value: {{ ref2?.value }}</span>`;
public ngForExample = `<ng-container *ngFor="let i of [1,2]">
<input #ref type="text" [value]="i" />
</ng-container>
<!-- The template is instantiated twice because *ngFor iterates
over the two items in the array, so it's impossible to define what ref2 is referencing.-->
{{ ref.value }}`;
@ViewChild('itemForm', { static: false }) form: NgForm;
get submitMessage() { return this._submitMessage; }
private _submitMessage = ''; // tslint:disable-line: variable-name
@ -25,4 +56,7 @@ export class AppComponent {
console.warn(`Faxing ${value} ...`);
}
log(ref3: any) {
console.warn(ref3.constructor);
}
}

View File

@ -1,7 +1,15 @@
# Template reference variables (`#var`)
# Template variables
A **template reference variable** is often a reference to a DOM element within a template.
It can also refer to a directive (which contains a component), an element, [TemplateRef](api/core/TemplateRef), or a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components" title="MDN: Web Components">web component</a>.
Template variables help you use data from one part of a template in another part of the template.
With template variables, you can perform tasks such as respond to user input or finely tune your application's forms.
A template variable can refer to the following:
* a DOM element within a template
* a directive
* an element
* [TemplateRef](api/core/TemplateRef)
* a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components" title="MDN: Web Components">web component</a>
<div class="alert is-helpful">
@ -9,61 +17,138 @@ See the <live-example></live-example> for a working example containing the code
</div>
Use the hash symbol (#) to declare a reference variable.
The following reference variable, `#phone`, declares a `phone` variable on an `<input>` element.
## Syntax
In the template, you use the hash symbol, `#`, to declare a template variable.
The following template variable, `#phone`, declares a `phone` variable on an `<input>` element.
<code-example path="template-reference-variables/src/app/app.component.html" region="ref-var" header="src/app/app.component.html"></code-example>
You can refer to a template reference variable anywhere in the component's template.
You can refer to a template variable anywhere in the component's template.
Here, a `<button>` further down the template refers to the `phone` variable.
<code-example path="template-reference-variables/src/app/app.component.html" region="ref-phone" header="src/app/app.component.html"></code-example>
Angular assigns each template reference variable a value based on where you declare the variable:
## How Angular assigns values to template variables
Angular assigns a template variable a value based on where you declare the variable:
* If you declare the variable on a component, the variable refers to the component instance.
* If you declare the variable on a standard HTML tag, the variable refers to the element.
* If you declare the variable on an `<ng-template>` element, the variable refers to a `TemplateRef` instance, which represents the template.
For more information on `<ng-template>`, see the [ng-template](guide/structural-directives#the-ng-template) section of [Structural directives](guide/structural-directives).
* If the variable specifies a name on the right-hand side, such as `#var="ngModel"`, the variable refers to the directive or component on the element with a matching `exportAs` name.
<!-- What does the second half of this mean?^^ Can we explain this more fully? Could I see a working example? -kw -->
<h3 class="no-toc">How a reference variable gets its value</h3>
### Using `NgForm` with template variables
In most cases, Angular sets the reference variable's value to the element on which it is declared.
In most cases, Angular sets the template variable's value to the element on which it occurs.
In the previous example, `phone` refers to the phone number `<input>`.
The button's click handler passes the `<input>` value to the component's `callPhone()` method.
The `NgForm` directive can change that behavior and set the value to something else. In the following example, the template reference variable, `itemForm`, appears three times separated
by HTML.
The `NgForm` directive demonstrates getting a reference to a different value by reference a directive's `exportAs` name.
In the following example, the template variable, `itemForm`, appears three times separated by HTML.
<code-example path="template-reference-variables/src/app/app.component.html" region="ngForm" header="src/app/hero-form.component.html"></code-example>
The reference value of `itemForm`, without the `ngForm` attribute value, would be
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement).
There is, however, a difference between a `Component` and a `Directive` in that a `Component`
will be referenced without specifying the attribute value, and a `Directive` will not
change the implicit reference (that is, the element).
Without the `ngForm` attribute value, the reference value of `itemForm` would be
the [HTMLFormElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement), `<form>`.
There is, however, a difference between a `Component` and a `Directive` in that Angular references a `Component` without specifying the attribute value, and a `Directive` does not change the implicit reference, or the element.
<!-- What is the train of thought from talking about a form element to the difference between a component and a directive? Why is the component directive conversation relevant here? -kw -->
With `NgForm`, `itemForm` is a reference to the [NgForm](api/forms/NgForm "API: NgForm") directive with the ability to track the value and validity of every control in the form.
Unlike the native `<form>` element, the `NgForm` directive has a `form` property.
The `NgForm` `form` property allows you to disable the submit button if the `itemForm.form.valid` is invalid.
## Template variable scope
However, with `NgForm`, `itemForm` is a reference to the [NgForm](api/forms/NgForm "API: NgForm")
directive with the ability to track the value and validity of every control in the form.
You can refer to a template variable anywhere within its surrounding template.
[Structural directives](guide/built-in-directives), such as `*ngIf` and `*ngFor`, or `<ng-template>` act as a template boundary.
You cannot access template variables outside of these boundaries.
The native `<form>` element doesn't have a `form` property, but the `NgForm` directive does, which allows disabling the submit button
if the `itemForm.form.valid` is invalid and passing the entire form control tree
to the parent component's `onSubmit()` method.
<div class="alert is-helpful">
<h3 class="no-toc">Template reference variable considerations</h3>
Define a variable only once in the template so the runtime value remains predictable.
A template _reference_ variable (`#phone`) is not the same as a template _input_ variable (`let phone`) such as in an [`*ngFor`](guide/built-in-directives#template-input-variable).
See [_Structural directives_](guide/structural-directives#template-input-variable) for more information.
</div>
The scope of a reference variable is the entire template. So, don't define the same variable name more than once in the same template as the runtime value will be unpredictable.
### Accessing in a nested template
### Alternative syntax
An inner template can access template variables that the outer template defines.
You can use the `ref-` prefix alternative to `#`.
This example declares the `fax` variable as `ref-fax` instead of `#fax`.
In the following example, changing the text in the `<input>` changes the value in the `<span>` because Angular immediately updates changes through the template variable, `ref1`.
<code-example path="template-reference-variables/src/app/app.component.html" region="template-ref-vars-scope1" header="src/app/app.component.html"></code-example>
<code-example path="template-reference-variables/src/app/app.component.html" region="ref-fax" header="src/app/app.component.html"></code-example>
In this case, there is an implied `<ng-template>` around the `<span>` and the definition of the variable is outside of it.
Accessing a template variable from the parent template works because the child template inherits the context from the parent template.
Rewriting the above code in a more verbose form explicitly shows the `<ng-template>`.
```html
<input #ref1 type="text" [(ngModel)]="firstExample" />
<!-- New template -->
<ng-template [ngIf]="true">
<!-- Since the context is inherited, the value is available to the new template -->
<span>Value: {{ ref1.value }}</span>
</ng-template>
```
However, accessing a template variable from outside the parent template doesn't work.
```html
<input *ngIf="true" #ref2 type="text" [(ngModel)]="secondExample" />
<span>Value: {{ ref2?.value }}</span> <!-- doesn't work -->
```
The verbose form shows that `ref2` is outside the parent template.
```
<ng-template [ngIf]="true">
<!-- The reference is defined within a template -->
<input #ref2 type="text" [(ngModel)]="secondExample" />
</ng-template>
<!-- ref2 accessed from outside that template doesn't work -->
<span>Value: {{ ref2?.value }}</span>
```
Consider the following example that uses `*ngFor`.
```
<ng-container *ngFor="let i of [1,2]">
<input #ref type="text" [value]="i" />
</ng-container>
{{ ref.value }}
```
Here, `ref.value` doesn't work.
The structural directive, `*ngFor` instantiates the template twice because `*ngFor` iterates over the two items in the array.
It is impossible to define what the `ref.value` reference signifies.
With structural directives, such as `*ngFor` or `*ngIf`, there is no way for Angular to know if a template is ever instantiated.
As a result, Angular isn't able to access the value and returns an error.
### Accessing a template variable within `<ng-template>`
When you declare the variable on an `<ng-template>`, the variable refers to a `TemplateRef` instance, which represents the template.
<code-example path="template-reference-variables/src/app/app.component.html" region="template-ref" header="src/app/app.component.html"></code-example>
In this example, clicking the button calls the `log()` function, which outputs the value of `#ref3` to the console.
Because the `#ref` variable is on an `<ng-template>`, the value is `TemplateRef`.
The following is the expanded browser console output of the `TemplateRef()` function with the name of `TemplateRef`.
<code-example language="sh">
&#9660; ƒ TemplateRef()
name: "TemplateRef"
__proto__: Function
</code-example>

View File

@ -192,11 +192,6 @@
"title": "Two-way binding",
"tooltip": "Introductory guide to sharing data between a class and a template."
},
{
"url": "guide/template-reference-variables",
"title": "Template reference variables",
"tooltip": "Introductory guide to referring to DOM elements within a template."
},
{
"url": "guide/template-expression-operators",
"title": "Template expression operators",
@ -777,6 +772,11 @@
"url": "guide/event-binding-concepts",
"title": "How event binding works",
"tooltip": "About event binding."
},
{
"url": "guide/template-reference-variables",
"title": "Template variables",
"tooltip": "Introductory guide to referring to DOM elements within a template."
}
]
},