-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathcreating-a-custom-form-field-control.html
executable file
·476 lines (441 loc) · 32.1 KB
/
creating-a-custom-form-field-control.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
<div class="docs-markdown"><h1>Creating a custom form field control</h1><p>It is possible to create custom form field controls that can be used inside <code><mat-form-field></code>. This
can be useful if you need to create a component that shares a lot of common behavior with a form
field, but adds some additional logic.</p>
<p>For example in this guide we'll learn how to create a custom input for inputting US telephone
numbers and hook it up to work with <code><mat-form-field></code>. Here is what we'll build by the end of this
guide:</p>
<div material-docs-example="form-field-custom-control"></div>
<p>In order to learn how to build custom form field controls, let's start with a simple input component
that we want to work inside the form field. For example, a phone number input that segments the
parts of the number into their own inputs. (Note: this is not intended to be a robust component,
just a starting point for us to learn.)</p>
<pre><code class="language-ts"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTel</span> </span>{
<span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">public</span> area: <span class="hljs-built_in">string</span>, <span class="hljs-keyword">public</span> exchange: <span class="hljs-built_in">string</span>, <span class="hljs-keyword">public</span> subscriber: <span class="hljs-built_in">string</span></span>)</span> {}
}
<span class="hljs-meta">@Component</span>({
<span class="hljs-attr">selector</span>: <span class="hljs-string">'example-tel-input'</span>,
<span class="hljs-attr">template</span>: <span class="hljs-string">`
<div role="group" [formGroup]="parts">
<input class="area" formControlName="area" maxlength="3">
<span>&ndash;</span>
<input class="exchange" formControlName="exchange" maxlength="3">
<span>&ndash;</span>
<input class="subscriber" formControlName="subscriber" maxlength="4">
</div>
`</span>,
<span class="hljs-attr">styles</span>: [<span class="hljs-string">`
div {
display: flex;
}
input {
border: none;
background: none;
padding: 0;
outline: none;
font: inherit;
text-align: center;
color: currentColor;
}
`</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTelInput</span> </span>{
<span class="hljs-attr">parts</span>: FormGroup;
<span class="hljs-meta">@Input</span>()
<span class="hljs-keyword">get</span> <span class="hljs-title">value</span>(): <span class="hljs-title">MyTel</span> | <span class="hljs-title">null</span> {
<span class="hljs-keyword">let</span> n = <span class="hljs-built_in">this</span>.parts.value;
<span class="hljs-keyword">if</span> (n.area.length == <span class="hljs-number">3</span> && n.exchange.length == <span class="hljs-number">3</span> && n.subscriber.length == <span class="hljs-number">4</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> MyTel(n.area, n.exchange, n.subscriber);
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
<span class="hljs-keyword">set</span> <span class="hljs-title">value</span>(<span class="hljs-params">tel: MyTel | <span class="hljs-literal">null</span></span>) {
tel = tel || <span class="hljs-keyword">new</span> MyTel(<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">''</span>);
<span class="hljs-built_in">this</span>.parts.setValue({<span class="hljs-attr">area</span>: tel.area, <span class="hljs-attr">exchange</span>: tel.exchange, <span class="hljs-attr">subscriber</span>: tel.subscriber});
}
<span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params">fb: FormBuilder</span>)</span> {
<span class="hljs-built_in">this</span>.parts = fb.group({
<span class="hljs-string">'area'</span>: <span class="hljs-string">''</span>,
<span class="hljs-string">'exchange'</span>: <span class="hljs-string">''</span>,
<span class="hljs-string">'subscriber'</span>: <span class="hljs-string">''</span>,
});
}
}
</code></pre>
<h2 id="providing-our-component-as-a-matformfieldcontrol" class="docs-header-link">
<span header-link="providing-our-component-as-a-matformfieldcontrol"></span>
Providing our component as a MatFormFieldControl
</h2>
<p>The first step is to provide our new component as an implementation of the <code>MatFormFieldControl</code>
interface that the <code><mat-form-field></code> knows how to work with. To do this, we will have our class
implement <code>MatFormFieldControl</code>. Since this is a generic interface, we'll need to include a type
parameter indicating the type of data our control will work with, in this case <code>MyTel</code>. We then add
a provider to our component so that the form field will be able to inject it as a
<code>MatFormFieldControl</code>.</p>
<pre><code class="language-ts"><span class="hljs-meta">@Component</span>({
...
<span class="hljs-attr">providers</span>: [{<span class="hljs-attr">provide</span>: MatFormFieldControl, <span class="hljs-attr">useExisting</span>: MyTelInput}],
})
<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTelInput</span> <span class="hljs-title">implements</span> <span class="hljs-title">MatFormFieldControl</span><<span class="hljs-title">MyTel</span>> </span>{
...
}
</code></pre>
<p>This sets up our component, so it can work with <code><mat-form-field></code>, but now we need to implement the
various methods and properties declared by the interface we just implemented. To learn more about
the <code>MatFormFieldControl</code> interface, see the
<a href="https://material.angular.io/components/form-field/api">form field API documentation</a>.</p>
<h3 id="implementing-the-methods-and-properties-of-matformfieldcontrol" class="docs-header-link">
<span header-link="implementing-the-methods-and-properties-of-matformfieldcontrol"></span>
Implementing the methods and properties of MatFormFieldControl
</h3>
<h4 id="value" class="docs-header-link">
<span header-link="value"></span>
<code>value</code>
</h4>
<p>This property allows someone to set or get the value of our control. Its type should be the same
type we used for the type parameter when we implemented <code>MatFormFieldControl</code>. Since our component
already has a value property, we don't need to do anything for this one.</p>
<h4 id="statechanges" class="docs-header-link">
<span header-link="statechanges"></span>
<code>stateChanges</code>
</h4>
<p>Because the <code><mat-form-field></code> uses the <code>OnPush</code> change detection strategy, we need to let it know
when something happens in the form field control that may require the form field to run change
detection. We do this via the <code>stateChanges</code> property. So far the only thing the form field needs to
know about is when the value changes. We'll need to emit on the stateChanges stream when that
happens, and as we continue flushing out these properties we'll likely find more places we need to
emit. We should also make sure to complete <code>stateChanges</code> when our component is destroyed.</p>
<pre><code class="language-ts">stateChanges = <span class="hljs-keyword">new</span> Subject<<span class="hljs-built_in">void</span>>();
<span class="hljs-keyword">set</span> <span class="hljs-title">value</span>(<span class="hljs-params">tel: MyTel | <span class="hljs-literal">null</span></span>) {
...
<span class="hljs-built_in">this</span>.stateChanges.next();
}
<span class="hljs-function"><span class="hljs-title">ngOnDestroy</span>(<span class="hljs-params"></span>)</span> {
<span class="hljs-built_in">this</span>.stateChanges.complete();
}
</code></pre>
<h4 id="id" class="docs-header-link">
<span header-link="id"></span>
<code>id</code>
</h4>
<p>This property should return the ID of an element in the component's template that we want the
<code><mat-form-field></code> to associate all of its labels and hints with. In this case, we'll use the host
element and just generate a unique ID for it.</p>
<pre><code class="language-ts"><span class="hljs-keyword">static</span> nextId = <span class="hljs-number">0</span>;
<span class="hljs-meta">@HostBinding</span>() id = <span class="hljs-string">`example-tel-input-<span class="hljs-subst">${MyTelInput.nextId++}</span>`</span>;
</code></pre>
<h4 id="placeholder" class="docs-header-link">
<span header-link="placeholder"></span>
<code>placeholder</code>
</h4>
<p>This property allows us to tell the <code><mat-form-field></code> what to use as a placeholder. In this
example, we'll do the same thing as <code>matInput</code> and <code><mat-select></code> and allow the user to specify it
via an <code>@Input()</code>. Since the value of the placeholder may change over time, we need to make sure to
trigger change detection in the parent form field by emitting on the <code>stateChanges</code> stream when the
placeholder changes.</p>
<pre><code class="language-ts"><span class="hljs-meta">@Input</span>()
<span class="hljs-keyword">get</span> <span class="hljs-title">placeholder</span>() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>._placeholder;
}
<span class="hljs-keyword">set</span> <span class="hljs-title">placeholder</span>(<span class="hljs-params">plh</span>) {
<span class="hljs-built_in">this</span>._placeholder = plh;
<span class="hljs-built_in">this</span>.stateChanges.next();
}
<span class="hljs-keyword">private</span> _placeholder: <span class="hljs-built_in">string</span>;
</code></pre>
<h4 id="ngcontrol" class="docs-header-link">
<span header-link="ngcontrol"></span>
<code>ngControl</code>
</h4>
<p>This property allows the form field control to specify the <code>@angular/forms</code> control that is bound
to this component. Since we haven't set up our component to act as a <code>ControlValueAccessor</code>, we'll
just set this to <code>null</code> in our component.</p>
<pre><code class="language-ts">ngControl: NgControl = <span class="hljs-literal">null</span>;
</code></pre>
<p>It is likely you will want to implement <code>ControlValueAccessor</code> so that your component can work with
<code>formControl</code> and <code>ngModel</code>. If you do implement <code>ControlValueAccessor</code> you will need to get a
reference to the <code>NgControl</code> associated with your control and make it publicly available.</p>
<p>The easy way is to add it as a public property to your constructor and let dependency injection
handle it:</p>
<pre><code class="language-ts"><span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params">
...,
<span class="hljs-meta">@Optional</span>() <span class="hljs-meta">@Self</span>() <span class="hljs-keyword">public</span> ngControl: NgControl,
...,
</span>)</span> { }
</code></pre>
<p>Note that if your component implements <code>ControlValueAccessor</code>, it may already be set up to provide
<code>NG_VALUE_ACCESSOR</code> (in the <code>providers</code> part of the component's decorator, or possibly in a module
declaration). If so, you may get a <em>cannot instantiate cyclic dependency</em> error.</p>
<p>To resolve this, remove the <code>NG_VALUE_ACCESSOR</code> provider and instead set the value accessor directly:</p>
<pre><code class="language-ts"><span class="hljs-meta">@Component</span>({
...,
<span class="hljs-attr">providers</span>: [
...,
<span class="hljs-comment">// Remove this.</span>
<span class="hljs-comment">// {</span>
<span class="hljs-comment">// provide: NG_VALUE_ACCESSOR,</span>
<span class="hljs-comment">// useExisting: forwardRef(() => MatFormFieldControl),</span>
<span class="hljs-comment">// multi: true,</span>
<span class="hljs-comment">// },</span>
],
})
<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTelInput</span> <span class="hljs-title">implements</span> <span class="hljs-title">MatFormFieldControl</span><<span class="hljs-title">MyTel</span>>, <span class="hljs-title">ControlValueAccessor</span> </span>{
<span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params">
...,
<span class="hljs-meta">@Optional</span>() <span class="hljs-meta">@Self</span>() <span class="hljs-keyword">public</span> ngControl: NgControl,
...,
</span>)</span> {
<span class="hljs-comment">// Replace the provider from above with this.</span>
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.ngControl != <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// Setting the value accessor directly (instead of using</span>
<span class="hljs-comment">// the providers) to avoid running into a circular import.</span>
<span class="hljs-built_in">this</span>.ngControl.valueAccessor = <span class="hljs-built_in">this</span>;
}
}
}
</code></pre>
<p>For additional information about <code>ControlValueAccessor</code> see the <a href="https://angular.io/api/forms/ControlValueAccessor">API docs</a>.</p>
<h4 id="focused" class="docs-header-link">
<span header-link="focused"></span>
<code>focused</code>
</h4>
<p>This property indicates whether the form field control should be considered to be in a
focused state. When it is in a focused state, the form field is displayed with a solid color
underline. For the purposes of our component, we want to consider it focused if any of the part
inputs are focused. We can use the <code>focusin</code> and <code>focusout</code> events to easily check this. We also
need to remember to emit on the <code>stateChanges</code> when the focused stated changes stream so change
detection can happen.</p>
<p>In addition to updating the focused state, we use the <code>focusin</code> and <code>focusout</code> methods to update the
internal touched state of our component, which we'll use to determine the error state.</p>
<pre><code class="language-ts">focused = <span class="hljs-literal">false</span>;
<span class="hljs-function"><span class="hljs-title">onFocusIn</span>(<span class="hljs-params">event: FocusEvent</span>)</span> {
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.focused) {
<span class="hljs-built_in">this</span>.focused = <span class="hljs-literal">true</span>;
<span class="hljs-built_in">this</span>.stateChanges.next();
}
}
<span class="hljs-function"><span class="hljs-title">onFocusOut</span>(<span class="hljs-params">event: FocusEvent</span>)</span> {
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>._elementRef.nativeElement.contains(event.relatedTarget <span class="hljs-keyword">as</span> Element)) {
<span class="hljs-built_in">this</span>.touched = <span class="hljs-literal">true</span>;
<span class="hljs-built_in">this</span>.focused = <span class="hljs-literal">false</span>;
<span class="hljs-built_in">this</span>.onTouched();
<span class="hljs-built_in">this</span>.stateChanges.next();
}
}
</code></pre>
<h4 id="empty" class="docs-header-link">
<span header-link="empty"></span>
<code>empty</code>
</h4>
<p>This property indicates whether the form field control is empty. For our control, we'll consider it
empty if all the parts are empty.</p>
<pre><code class="language-ts"><span class="hljs-keyword">get</span> <span class="hljs-title">empty</span>() {
<span class="hljs-keyword">let</span> n = <span class="hljs-built_in">this</span>.parts.value;
<span class="hljs-keyword">return</span> !n.area && !n.exchange && !n.subscriber;
}
</code></pre>
<h4 id="shouldlabelfloat" class="docs-header-link">
<span header-link="shouldlabelfloat"></span>
<code>shouldLabelFloat</code>
</h4>
<p>This property is used to indicate whether the label should be in the floating position. We'll
use the same logic as <code>matInput</code> and float the placeholder when the input is focused or non-empty.
Since the placeholder will be overlapping our control when it's not floating, we should hide
the <code>–</code> characters when it's not floating.</p>
<pre><code class="language-ts"><span class="hljs-meta">@HostBinding</span>(<span class="hljs-string">'class.floating'</span>)
<span class="hljs-keyword">get</span> <span class="hljs-title">shouldLabelFloat</span>() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.focused || !<span class="hljs-built_in">this</span>.empty;
}
</code></pre>
<pre><code class="language-css"><span class="hljs-selector-tag">span</span> {
<span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
<span class="hljs-attribute">transition</span>: opacity <span class="hljs-number">200ms</span>;
}
<span class="hljs-selector-pseudo">:host</span><span class="hljs-selector-class">.floating</span> <span class="hljs-selector-tag">span</span> {
<span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
}
</code></pre>
<h4 id="required" class="docs-header-link">
<span header-link="required"></span>
<code>required</code>
</h4>
<p>This property is used to indicate whether the input is required. <code><mat-form-field></code> uses this
information to add a required indicator to the placeholder. Again, we'll want to make sure we run
change detection if the required state changes.</p>
<pre><code class="language-ts"><span class="hljs-meta">@Input</span>()
<span class="hljs-keyword">get</span> <span class="hljs-title">required</span>() {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>._required;
}
<span class="hljs-keyword">set</span> <span class="hljs-title">required</span>(<span class="hljs-params">req: BooleanInput</span>) {
<span class="hljs-built_in">this</span>._required = coerceBooleanProperty(req);
<span class="hljs-built_in">this</span>.stateChanges.next();
}
<span class="hljs-keyword">private</span> _required = <span class="hljs-literal">false</span>;
</code></pre>
<h4 id="disabled" class="docs-header-link">
<span header-link="disabled"></span>
<code>disabled</code>
</h4>
<p>This property tells the form field when it should be in the disabled state. In addition to reporting
the right state to the form field, we need to set the disabled state on the individual inputs that
make up our component.</p>
<pre><code class="language-ts"><span class="hljs-meta">@Input</span>()
<span class="hljs-keyword">get</span> <span class="hljs-title">disabled</span>(): <span class="hljs-title">boolean</span> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>._disabled; }
<span class="hljs-keyword">set</span> <span class="hljs-title">disabled</span>(<span class="hljs-params">value: BooleanInput</span>) {
<span class="hljs-built_in">this</span>._disabled = coerceBooleanProperty(value);
<span class="hljs-built_in">this</span>._disabled ? <span class="hljs-built_in">this</span>.parts.disable() : <span class="hljs-built_in">this</span>.parts.enable();
<span class="hljs-built_in">this</span>.stateChanges.next();
}
<span class="hljs-keyword">private</span> _disabled = <span class="hljs-literal">false</span>;
</code></pre>
<h4 id="errorstate" class="docs-header-link">
<span header-link="errorstate"></span>
<code>errorState</code>
</h4>
<p>This property indicates whether the associated <code>NgControl</code> is in an error state. For example,
we can show an error if the input is invalid and our component has been touched.</p>
<pre><code class="language-ts"><span class="hljs-keyword">get</span> <span class="hljs-title">errorState</span>(): <span class="hljs-title">boolean</span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.parts.invalid && <span class="hljs-built_in">this</span>.touched;
}
</code></pre>
<p>However, there are some error triggers that we can't subscribe to (e.g. parent form submissions),
to handle such cases we should re-evaluate <code>errorState</code> on every change detection cycle.</p>
<pre><code class="language-ts"><span class="hljs-comment">/** Whether the component is in an error state. */</span>
<span class="hljs-attr">errorState</span>: <span class="hljs-built_in">boolean</span> = <span class="hljs-literal">false</span>;
<span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params">
...,
<span class="hljs-meta">@Optional</span>() <span class="hljs-keyword">private</span> _parentForm: NgForm,
<span class="hljs-meta">@Optional</span>() <span class="hljs-keyword">private</span> _parentFormGroup: FormGroupDirective
</span>)</span> {
...
}
<span class="hljs-function"><span class="hljs-title">ngDoCheck</span>(<span class="hljs-params"></span>)</span> {
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.ngControl) {
<span class="hljs-built_in">this</span>.updateErrorState();
}
}
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-title">updateErrorState</span>(<span class="hljs-params"></span>)</span> {
<span class="hljs-keyword">const</span> parent = <span class="hljs-built_in">this</span>._parentFormGroup || <span class="hljs-built_in">this</span>.parentForm;
<span class="hljs-keyword">const</span> oldState = <span class="hljs-built_in">this</span>.errorState;
<span class="hljs-keyword">const</span> newState = (<span class="hljs-built_in">this</span>.ngControl?.invalid || <span class="hljs-built_in">this</span>.parts.invalid) && (<span class="hljs-built_in">this</span>.touched || parent.submitted);
<span class="hljs-keyword">if</span> (oldState !== newState) {
<span class="hljs-built_in">this</span>.errorState = newState;
<span class="hljs-built_in">this</span>.stateChanges.next();
}
}
</code></pre>
<p>Keep in mind that <code>updateErrorState()</code> must have minimal logic to avoid performance issues.</p>
<h4 id="controltype" class="docs-header-link">
<span header-link="controltype"></span>
<code>controlType</code>
</h4>
<p>This property allows us to specify a unique string for the type of control in form field. The
<code><mat-form-field></code> will add a class based on this type that can be used to easily apply
special styles to a <code><mat-form-field></code> that contains a specific type of control. In this example
we'll use <code>example-tel-input</code> as our control type which will result in the form field adding the
class <code>mat-form-field-type-example-tel-input</code>.</p>
<pre><code class="language-ts">controlType = <span class="hljs-string">'example-tel-input'</span>;
</code></pre>
<h4 id="autofilled" class="docs-header-link">
<span header-link="autofilled"></span>
<code>autofilled</code>
</h4>
<p>This property allows us to specify Whether the input is currently in an autofilled state.
If set true it will add <code>mat-form-field-autofilled</code> class to the <code><mat-form-field></code>,
If property is not present on the control it is assumed to be false.
<pre><code class="language-ts">autofilled = <span class="hljs-string">true | false</span>;
</code></pre>
<h4 id="userAriaDescribedBy" class="docs-header-link">
<span header-link="userAriaDescribedBy"></span>
<code>userAriaDescribedBy</code>
</h4>
<p>This property allows us to specify the Value
of <code>aria-describedby</code> that should be merged with the described-by ids which are set by the form-field.
<pre><code class="language-ts">userAriaDescribedBy = <span class="hljs-string">'id' | 'space-separated list of element ids'</span>;
</code></pre>
<h4 id="setdescribedbyidsids-string" class="docs-header-link">
<span header-link="setdescribedbyidsids-string"></span>
<code>setDescribedByIds(ids: string[])</code>
</h4>
<p>This method is used by the <code><mat-form-field></code> to set element ids that should be used for the
<code>aria-describedby</code> attribute of your control. The ids are controlled through the form field
as hints or errors are conditionally displayed and should be reflected in the control's
<code>aria-describedby</code> attribute for an improved accessibility experience. </p>
<p>The <code>setDescribedByIds</code> method is invoked whenever the control's state changes. Custom controls
need to implement this method and update the <code>aria-describedby</code> attribute based on the specified
element ids. Below is an example that shows how this can be achieved.</p>
<p>Note that the method by default will not respect element ids that have been set manually on the
control element through the <code>aria-describedby</code> attribute. To ensure that your control does not
accidentally override existing element ids specified by consumers of your control, create an
input called <code>userAriaDescribedby</code> like followed:</p>
<pre><code class="language-ts"><span class="hljs-meta">@Input</span>(<span class="hljs-string">'aria-describedby'</span>) userAriaDescribedBy: <span class="hljs-built_in">string</span>;
</code></pre>
<p>The form field will then pick up the user specified <code>aria-describedby</code> ids and merge
them with ids for hints or errors whenever <code>setDescribedByIds</code> is invoked.</p>
<pre><code class="language-ts"><span class="hljs-function"><span class="hljs-title">setDescribedByIds</span>(<span class="hljs-params">ids: <span class="hljs-built_in">string</span>[]</span>)</span> {
<span class="hljs-keyword">const</span> controlElement = <span class="hljs-built_in">this</span>._elementRef.nativeElement
.querySelector(<span class="hljs-string">'.example-tel-input-container'</span>)!;
controlElement.setAttribute(<span class="hljs-string">'aria-describedby'</span>, ids.join(<span class="hljs-string">' '</span>));
}
</code></pre>
<h4 id="oncontainerclickevent-mouseevent" class="docs-header-link">
<span header-link="oncontainerclickevent-mouseevent"></span>
<code>onContainerClick(event: MouseEvent)</code>
</h4>
<p>This method will be called when the form field is clicked on. It allows your component to hook in
and handle that click however it wants. The method has one parameter, the <code>MouseEvent</code> for the
click. In our case we'll just focus the first <code><input></code> if the user isn't about to click an
<code><input></code> anyways.</p>
<pre><code class="language-ts"><span class="hljs-function"><span class="hljs-title">onContainerClick</span>(<span class="hljs-params">event: MouseEvent</span>)</span> {
<span class="hljs-keyword">if</span> ((event.target <span class="hljs-keyword">as</span> Element).tagName.toLowerCase() != <span class="hljs-string">'input'</span>) {
<span class="hljs-built_in">this</span>._elementRef.nativeElement.querySelector(<span class="hljs-string">'input'</span>).focus();
}
}
</code></pre>
<h3 id="improving-accessibility" class="docs-header-link">
<span header-link="improving-accessibility"></span>
Improving accessibility
</h3>
<p>Our custom form field control consists of multiple inputs that describe segments of a phone
number. For accessibility purposes, we put those inputs as part of a <code>div</code> element with
<code>role="group"</code>. This ensures that screen reader users can tell that all those inputs belong
together.</p>
<p>One significant piece of information is missing for screen reader users though. They won't be able
to tell what this input group represents. To improve this, we should add a label for the group
element using either <code>aria-label</code> or <code>aria-labelledby</code>.</p>
<p>It's recommended to link the group to the label that is displayed as part of the parent
<code><mat-form-field></code>. This ensures that explicitly specified labels (using <code><mat-label></code>) are
actually used for labelling the control.</p>
<p>In our concrete example, we add an attribute binding for <code>aria-labelledby</code> and bind it
to the label element id provided by the parent <code><mat-form-field></code>.</p>
<pre><code class="language-typescript"><span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyTelInput</span> <span class="hljs-title">implements</span> <span class="hljs-title">MatFormFieldControl</span><<span class="hljs-title">MyTel</span>> </span>{
...
<span class="hljs-function"><span class="hljs-title">constructor</span>(<span class="hljs-params">...
<span class="hljs-meta">@Optional</span>() <span class="hljs-keyword">public</span> parentFormField: MatFormField</span>)</span> {
</code></pre>
<pre><code class="language-html">@Component({
selector: 'example-tel-input',
template: `
<span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"group"</span> [<span class="hljs-attr">formGroup</span>]=<span class="hljs-string">"parts"</span>
[<span class="hljs-attr">attr.aria-describedby</span>]=<span class="hljs-string">"describedBy"</span>
[<span class="hljs-attr">attr.aria-labelledby</span>]=<span class="hljs-string">"parentFormField?.getLabelId()"</span>></span>
</code></pre>
<h3 id="trying-it-out" class="docs-header-link">
<span header-link="trying-it-out"></span>
Trying it out
</h3>
<p>Now that we've fully implemented the interface, we're ready to try our component out! All we need to
do is place it inside a <code><mat-form-field></code></p>
<pre><code class="language-html"><span class="hljs-tag"><<span class="hljs-name">mat-form-field</span>></span>
<span class="hljs-tag"><<span class="hljs-name">example-tel-input</span>></span><span class="hljs-tag"></<span class="hljs-name">example-tel-input</span>></span>
<span class="hljs-tag"></<span class="hljs-name">mat-form-field</span>></span>
</code></pre>
<p>We also get all the features that come with <code><mat-form-field></code> such as floating placeholder,
prefix, suffix, hints, and errors (if we've given the form field an <code>NgControl</code> and correctly report
the error state).</p>
<pre><code class="language-html"><span class="hljs-tag"><<span class="hljs-name">mat-form-field</span>></span>
<span class="hljs-tag"><<span class="hljs-name">example-tel-input</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Phone number"</span> <span class="hljs-attr">required</span>></span><span class="hljs-tag"></<span class="hljs-name">example-tel-input</span>></span>
<span class="hljs-tag"><<span class="hljs-name">mat-icon</span> <span class="hljs-attr">matPrefix</span>></span>phone<span class="hljs-tag"></<span class="hljs-name">mat-icon</span>></span>
<span class="hljs-tag"><<span class="hljs-name">mat-hint</span>></span>Include area code<span class="hljs-tag"></<span class="hljs-name">mat-hint</span>></span>
<span class="hljs-tag"></<span class="hljs-name">mat-form-field</span>></span>
</code></pre>
</div>