Skip to content

Commit 5de7a22

Browse files
authored
docs(material/form-field): fix handling of error state in the custom form field control docs (#22782)
1 parent 0a4dbe1 commit 5de7a22

File tree

3 files changed

+48
-28
lines changed

3 files changed

+48
-28
lines changed

guides/creating-a-custom-form-field-control.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,30 @@ For additional information about `ControlValueAccessor` see the [API docs](https
227227
This property indicates whether the form field control should be considered to be in a
228228
focused state. When it is in a focused state, the form field is displayed with a solid color
229229
underline. For the purposes of our component, we want to consider it focused if any of the part
230-
inputs are focused. We can use the `FocusMonitor` from `@angular/cdk` to easily check this. We also
231-
need to remember to emit on the `stateChanges` stream so change detection can happen.
230+
inputs are focused. We can use the `focusin` and `focusout` events to easily check this. We also
231+
need to remember to emit on the `stateChanges` when the focused stated changes stream so change
232+
detection can happen.
233+
234+
In addition to updating the focused state, we use the `focusin` and `focusout` methods to update the
235+
internal touched state of our component, which we'll use to determine the error state.
232236

233237
```ts
234238
focused = false;
235239

236-
constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>) {
237-
...
238-
fm.monitor(elRef, true).subscribe(origin => {
239-
this.focused = !!origin;
240+
onFocusIn(event: FocusEvent) {
241+
if (!this.focused) {
242+
this.focused = true;
240243
this.stateChanges.next();
241-
});
244+
}
242245
}
243246

244-
ngOnDestroy() {
245-
...
246-
this.fm.stopMonitoring(this.elRef);
247+
onFocusOut(event: FocusEvent) {
248+
if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) {
249+
this.touched = true;
250+
this.focused = false;
251+
this.onTouched();
252+
this.stateChanges.next();
253+
}
247254
}
248255
```
249256

@@ -319,11 +326,13 @@ private _disabled = false;
319326

320327
#### `errorState`
321328

322-
This property indicates whether the associated `NgControl` is in an error state. Since we're not
323-
using an `NgControl` in this example, we don't need to do anything other than just set it to `false`.
329+
This property indicates whether the associated `NgControl` is in an error state. In this example,
330+
we show an error if the input is invalid and our component has been touched.
324331

325332
```ts
326-
errorState = false;
333+
get errorState(): boolean {
334+
return this.parts.invalid && this.touched;
335+
}
327336
```
328337

329338
#### `controlType`

src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<div role="group" class="example-tel-input-container"
22
[formGroup]="parts"
3-
[attr.aria-labelledby]="_formField?.getLabelId()">
3+
[attr.aria-labelledby]="_formField?.getLabelId()"
4+
(focusin)="onFocusIn($event)"
5+
(focusout)="onFocusOut($event)">
46
<input class="example-tel-input-element"
57
formControlName="area" size="3"
68
maxLength="3"

src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export class MyTelInput
6363
parts: FormGroup;
6464
stateChanges = new Subject<void>();
6565
focused = false;
66+
touched = false;
6667
controlType = 'example-tel-input';
6768
id = `example-tel-input-${MyTelInput.nextId++}`;
6869
onChange = (_: any) => {};
@@ -130,7 +131,7 @@ export class MyTelInput
130131
}
131132

132133
get errorState(): boolean {
133-
return this.parts.invalid && this.parts.dirty;
134+
return this.parts.invalid && this.touched;
134135
}
135136

136137
constructor(
@@ -155,19 +156,32 @@ export class MyTelInput
155156
]
156157
});
157158

158-
_focusMonitor.monitor(_elementRef, true).subscribe(origin => {
159-
if (this.focused && !origin) {
160-
this.onTouched();
161-
}
162-
this.focused = !!origin;
163-
this.stateChanges.next();
164-
});
165-
166159
if (this.ngControl != null) {
167160
this.ngControl.valueAccessor = this;
168161
}
169162
}
170163

164+
ngOnDestroy() {
165+
this.stateChanges.complete();
166+
this._focusMonitor.stopMonitoring(this._elementRef);
167+
}
168+
169+
onFocusIn(event: FocusEvent) {
170+
if (!this.focused) {
171+
this.focused = true;
172+
this.stateChanges.next();
173+
}
174+
}
175+
176+
onFocusOut(event: FocusEvent) {
177+
if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) {
178+
this.touched = true;
179+
this.focused = false;
180+
this.onTouched();
181+
this.stateChanges.next();
182+
}
183+
}
184+
171185
autoFocusNext(control: AbstractControl, nextElement?: HTMLInputElement): void {
172186
if (!control.errors && nextElement) {
173187
this._focusMonitor.focusVia(nextElement, 'program');
@@ -180,11 +194,6 @@ export class MyTelInput
180194
}
181195
}
182196

183-
ngOnDestroy() {
184-
this.stateChanges.complete();
185-
this._focusMonitor.stopMonitoring(this._elementRef);
186-
}
187-
188197
setDescribedByIds(ids: string[]) {
189198
const controlElement = this._elementRef.nativeElement
190199
.querySelector('.example-tel-input-container')!;

0 commit comments

Comments
 (0)