From 64044330f851d1e25369f69acafc2e6570a24c60 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Wed, 12 Jun 2024 15:16:35 -0700 Subject: [PATCH] docs(material/form-field): Update form-field docs & examples --- .../form-field-appearance-example.ts | 5 +- .../example-tel-input-example.html | 64 +++--- .../form-field-custom-control-example.html | 1 + .../form-field-custom-control-example.ts | 203 +++++++++++------- .../form-field-error-example.html | 14 +- .../form-field-error-example.ts | 17 +- .../form-field-harness-example.ts | 9 +- .../form-field-hint-example.html | 4 +- .../form-field-hint-example.ts | 15 +- .../form-field-label-example.html | 14 +- .../form-field-label-example.ts | 26 +-- .../form-field-overview-example.ts | 3 +- .../form-field-prefix-suffix-example.html | 14 +- .../form-field-prefix-suffix-example.ts | 11 +- .../form-field-theming-example.css | 3 - .../form-field-theming-example.html | 8 - .../form-field-theming-example.ts | 17 -- .../material/form-field/index.ts | 3 +- src/material/form-field/form-field.md | 20 +- src/material/form-field/form-field.ts | 22 +- 20 files changed, 263 insertions(+), 210 deletions(-) delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html delete mode 100644 src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts diff --git a/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts b/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts index 611acfb5f989..068fcdfc5c23 100644 --- a/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts +++ b/src/components-examples/material/form-field/form-field-appearance/form-field-appearance-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; import {MatInputModule} from '@angular/material/input'; -import {MatFormFieldModule} from '@angular/material/form-field'; /** @title Form field appearance variants */ @Component({ @@ -9,5 +9,6 @@ import {MatFormFieldModule} from '@angular/material/form-field'; templateUrl: 'form-field-appearance-example.html', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldAppearanceExample {} diff --git a/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html b/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html index 0103309a96b7..79775ce567d5 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html +++ b/src/components-examples/material/form-field/form-field-custom-control/example-tel-input-example.html @@ -1,30 +1,40 @@ -
- +
+ - + - +
diff --git a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html index cd025c09ba69..450f8afae244 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html +++ b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.html @@ -5,4 +5,5 @@ phone Include area code +

Entered value: {{form.valueChanges | async | json}}

diff --git a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts index 60008128ff42..2c0f4bc39674 100644 --- a/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts +++ b/src/components-examples/material/form-field/form-field-custom-control/form-field-custom-control-example.ts @@ -1,35 +1,39 @@ import {FocusMonitor} from '@angular/cdk/a11y'; -import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {AsyncPipe, JsonPipe} from '@angular/common'; import { + ChangeDetectionStrategy, Component, ElementRef, - Inject, - Input, OnDestroy, - Optional, - Self, - ViewChild, + booleanAttribute, + computed, + effect, forwardRef, + inject, + input, + model, + signal, + viewChild, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import { AbstractControl, ControlValueAccessor, FormBuilder, FormControl, FormGroup, - NgControl, - Validators, FormsModule, + NgControl, ReactiveFormsModule, + Validators, } from '@angular/forms'; import { MAT_FORM_FIELD, - MatFormField, MatFormFieldControl, MatFormFieldModule, } from '@angular/material/form-field'; -import {Subject} from 'rxjs'; import {MatIconModule} from '@angular/material/icon'; +import {Subject} from 'rxjs'; /** @title Form field with custom telephone number input control. */ @Component({ @@ -42,10 +46,13 @@ import {MatIconModule} from '@angular/material/icon'; MatFormFieldModule, forwardRef(() => MyTelInput), MatIconModule, + AsyncPipe, + JsonPipe, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldCustomControlExample { - form: FormGroup = new FormGroup({ + readonly form = new FormGroup({ tel: new FormControl(null), }); } @@ -71,26 +78,51 @@ export class MyTel { }, standalone: true, imports: [FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyTelInput implements ControlValueAccessor, MatFormFieldControl, OnDestroy { static nextId = 0; - @ViewChild('area') areaInput: HTMLInputElement; - @ViewChild('exchange') exchangeInput: HTMLInputElement; - @ViewChild('subscriber') subscriberInput: HTMLInputElement; - - parts: FormGroup<{ + readonly areaInput = viewChild.required('area'); + readonly exchangeInput = viewChild.required('exchange'); + readonly subscriberInput = viewChild.required('subscriber'); + ngControl = inject(NgControl, {optional: true, self: true}); + readonly parts: FormGroup<{ area: FormControl; exchange: FormControl; subscriber: FormControl; }>; - stateChanges = new Subject(); - focused = false; - touched = false; - controlType = 'example-tel-input'; - id = `example-tel-input-${MyTelInput.nextId++}`; + readonly stateChanges = new Subject(); + readonly touched = signal(false); + readonly controlType = 'example-tel-input'; + readonly id = `example-tel-input-${MyTelInput.nextId++}`; + readonly _userAriaDescribedBy = input('', {alias: 'aria-describedby'}); + readonly _placeholder = input('', {alias: 'placeholder'}); + readonly _required = input(false, { + alias: 'required', + transform: booleanAttribute, + }); + readonly _disabledByInput = input(false, { + alias: 'disabled', + transform: booleanAttribute, + }); + readonly _value = model(null, {alias: 'value'}); onChange = (_: any) => {}; onTouched = () => {}; + protected readonly _formField = inject(MAT_FORM_FIELD, { + optional: true, + }); + + private readonly _focused = signal(false); + private readonly _disabledByCva = signal(false); + private readonly _disabled = computed(() => this._disabledByInput() || this._disabledByCva()); + private readonly _focusMonitor = inject(FocusMonitor); + private readonly _elementRef = inject>(ElementRef); + + get focused(): boolean { + return this._focused(); + } + get empty() { const { value: {area, exchange, subscriber}, @@ -103,75 +135,75 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl, - @Optional() @Inject(MAT_FORM_FIELD) public _formField: MatFormField, - @Optional() @Self() public ngControl: NgControl, - ) { + constructor() { if (this.ngControl != null) { this.ngControl.valueAccessor = this; } - this.parts = formBuilder.group({ + this.parts = inject(FormBuilder).group({ area: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(3)]], exchange: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(3)]], subscriber: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(4)]], }); + + effect(() => { + // Read signals to trigger effect. + this._placeholder(); + this._required(); + this._disabled(); + // Propagate state changes. + this.stateChanges.next(); + }); + + effect(() => { + if (this._disabled()) { + this.parts.disable(); + } else { + this.parts.enable(); + } + }); + + effect(() => { + this.parts.setValue(this._value() || new MyTel('', '', '')); + }); + + this.parts.statusChanges.pipe(takeUntilDestroyed()).subscribe(() => { + this.stateChanges.next(); + }); + + this.parts.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + const tel = this.parts.valid + ? new MyTel( + this.parts.value.area || '', + this.parts.value.exchange || '', + this.parts.value.subscriber || '', + ) + : null; + this._updateValue(tel); + }); } ngOnDestroy() { @@ -179,19 +211,17 @@ export class MyTelInput implements ControlValueAccessor, MatFormFieldControl Enter your email - + @if (email.invalid) { - {{errorMessage}} + {{errorMessage()}} } diff --git a/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts b/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts index 891a4432257e..0cb5a84be9b7 100644 --- a/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts +++ b/src/components-examples/material/form-field/form-field-error/form-field-error-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {FormControl, Validators, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {MatInputModule} from '@angular/material/input'; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; import {merge} from 'rxjs'; /** @title Form field with error messages */ @@ -12,11 +12,12 @@ import {merge} from 'rxjs'; styleUrl: 'form-field-error-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldErrorExample { - email = new FormControl('', [Validators.required, Validators.email]); + readonly email = new FormControl('', [Validators.required, Validators.email]); - errorMessage = ''; + errorMessage = signal(''); constructor() { merge(this.email.statusChanges, this.email.valueChanges) @@ -26,11 +27,11 @@ export class FormFieldErrorExample { updateErrorMessage() { if (this.email.hasError('required')) { - this.errorMessage = 'You must enter a value'; + this.errorMessage.set('You must enter a value'); } else if (this.email.hasError('email')) { - this.errorMessage = 'Not a valid email'; + this.errorMessage.set('Not a valid email'); } else { - this.errorMessage = ''; + this.errorMessage.set(''); } } } diff --git a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts index 19c1012604fe..a00eec80535c 100644 --- a/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts +++ b/src/components-examples/material/form-field/form-field-harness/form-field-harness-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; -import {FormControl, Validators, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {MatInputModule} from '@angular/material/input'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; /** * @title Testing with MatFormFieldHarness @@ -11,7 +11,8 @@ import {MatFormFieldModule} from '@angular/material/form-field'; templateUrl: 'form-field-harness-example.html', standalone: true, imports: [MatFormFieldModule, MatInputModule, FormsModule, ReactiveFormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldHarnessExample { - requiredControl = new FormControl('Initial value', [Validators.required]); + readonly requiredControl = new FormControl('Initial value', [Validators.required]); } diff --git a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html index 4c46c727f6c5..c08a2d4b11d3 100644 --- a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html +++ b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.html @@ -1,8 +1,8 @@
Enter some input - - {{input.value.length}}/10 + + {{value().length}}/10 diff --git a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts index 5279fe2a0aef..5f7f5c45b031 100644 --- a/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts +++ b/src/components-examples/material/form-field/form-field-hint/form-field-hint-example.ts @@ -1,7 +1,7 @@ -import {Component} from '@angular/core'; -import {MatSelectModule} from '@angular/material/select'; -import {MatInputModule} from '@angular/material/input'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatSelectModule} from '@angular/material/select'; /** @title Form field with hints */ @Component({ @@ -10,5 +10,12 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-hint-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatSelectModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FormFieldHintExample {} +export class FormFieldHintExample { + protected readonly value = signal(''); + + protected onInput(event: Event) { + this.value.set((event.target as HTMLInputElement).value); + } +} diff --git a/src/components-examples/material/form-field/form-field-label/form-field-label-example.html b/src/components-examples/material/form-field/form-field-label/form-field-label-example.html index 0556459e2afe..5caf21f0f5f7 100644 --- a/src/components-examples/material/form-field/form-field-label/form-field-label-example.html +++ b/src/components-examples/material/form-field/form-field-label/form-field-label-example.html @@ -10,20 +10,16 @@
- - + + - + Both a label and a placeholder - + - + -- None -- Option diff --git a/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts b/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts index 7cdc7a52450b..a11f34d34301 100644 --- a/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts +++ b/src/components-examples/material/form-field/form-field-label/form-field-label-example.ts @@ -1,11 +1,13 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; import {FormBuilder, FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; import {FloatLabelType, MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; -import {MatSelectModule} from '@angular/material/select'; import {MatInputModule} from '@angular/material/input'; import {MatRadioModule} from '@angular/material/radio'; -import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatSelectModule} from '@angular/material/select'; +import {map} from 'rxjs/operators'; /** @title Form field with label */ @Component({ @@ -23,18 +25,18 @@ import {MatCheckboxModule} from '@angular/material/checkbox'; MatSelectModule, MatIconModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldLabelExample { - hideRequiredControl = new FormControl(false); - floatLabelControl = new FormControl('auto' as FloatLabelType); - options = this._formBuilder.group({ + readonly hideRequiredControl = new FormControl(false); + readonly floatLabelControl = new FormControl('auto' as FloatLabelType); + readonly options = inject(FormBuilder).group({ hideRequired: this.hideRequiredControl, floatLabel: this.floatLabelControl, }); - - constructor(private _formBuilder: FormBuilder) {} - - getFloatLabelValue(): FloatLabelType { - return this.floatLabelControl.value || 'auto'; - } + protected readonly hideRequired = toSignal(this.hideRequiredControl.valueChanges); + protected readonly floatLabel = toSignal( + this.floatLabelControl.valueChanges.pipe(map(v => v || 'auto')), + {initialValue: 'auto'}, + ); } diff --git a/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts b/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts index c5a820960ab9..c1d4ffe7a63a 100644 --- a/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts +++ b/src/components-examples/material/form-field/form-field-overview/form-field-overview-example.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; import {MatSelectModule} from '@angular/material/select'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -10,5 +10,6 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-overview-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatSelectModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldOverviewExample {} diff --git a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html index dd514eb6ba49..3bf388ecc9b2 100644 --- a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html +++ b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.html @@ -1,15 +1,21 @@
Enter your password - - Amount - + .00 diff --git a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts index ca65f80ffd12..98f37a5ad0a9 100644 --- a/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts +++ b/src/components-examples/material/form-field/form-field-prefix-suffix/form-field-prefix-suffix-example.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; -import {MatIconModule} from '@angular/material/icon'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; -import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; /** @title Form field with prefix & suffix */ @Component({ @@ -11,11 +11,12 @@ import {MatFormFieldModule} from '@angular/material/form-field'; styleUrl: 'form-field-prefix-suffix-example.css', standalone: true, imports: [MatFormFieldModule, MatInputModule, MatButtonModule, MatIconModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FormFieldPrefixSuffixExample { - hide = true; + hide = signal(true); clickEvent(event: MouseEvent) { - this.hide = !this.hide; + this.hide.set(!this.hide); event.stopPropagation(); } } diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css deleted file mode 100644 index d98172fcc0a3..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.css +++ /dev/null @@ -1,3 +0,0 @@ -.example-container mat-form-field + mat-form-field { - margin-left: 8px; -} diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html deleted file mode 100644 index 6b455564f846..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.html +++ /dev/null @@ -1,8 +0,0 @@ - - Color - - Primary - Accent - Warn - - diff --git a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts b/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts deleted file mode 100644 index 5f21cc74fef9..000000000000 --- a/src/components-examples/material/form-field/form-field-theming/form-field-theming-example.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Component} from '@angular/core'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {ThemePalette} from '@angular/material/core'; -import {MatSelectModule} from '@angular/material/select'; -import {MatFormFieldModule} from '@angular/material/form-field'; - -/** @title Form field theming */ -@Component({ - selector: 'form-field-theming-example', - templateUrl: 'form-field-theming-example.html', - styleUrl: 'form-field-theming-example.css', - standalone: true, - imports: [MatFormFieldModule, MatSelectModule, FormsModule, ReactiveFormsModule], -}) -export class FormFieldThemingExample { - colorControl = new FormControl('primary' as ThemePalette); -} diff --git a/src/components-examples/material/form-field/index.ts b/src/components-examples/material/form-field/index.ts index 4e22bcbb0597..99b315986fb7 100644 --- a/src/components-examples/material/form-field/index.ts +++ b/src/components-examples/material/form-field/index.ts @@ -4,9 +4,8 @@ export { MyTelInput, } from './form-field-custom-control/form-field-custom-control-example'; export {FormFieldErrorExample} from './form-field-error/form-field-error-example'; +export {FormFieldHarnessExample} from './form-field-harness/form-field-harness-example'; export {FormFieldHintExample} from './form-field-hint/form-field-hint-example'; export {FormFieldLabelExample} from './form-field-label/form-field-label-example'; export {FormFieldOverviewExample} from './form-field-overview/form-field-overview-example'; export {FormFieldPrefixSuffixExample} from './form-field-prefix-suffix/form-field-prefix-suffix-example'; -export {FormFieldThemingExample} from './form-field-theming/form-field-theming-example'; -export {FormFieldHarnessExample} from './form-field-harness/form-field-harness-example'; diff --git a/src/material/form-field/form-field.md b/src/material/form-field/form-field.md index a393f128eb24..83d0b2b2f45e 100644 --- a/src/material/form-field/form-field.md +++ b/src/material/form-field/form-field.md @@ -7,14 +7,16 @@ In this document, "form field" refers to the wrapper component ` (e.g. the input, textarea, select, etc.) The following Angular Material components are designed to work inside a ``: -* [`` & `