From cd724d8f6a910d4dcfbb218059b83e1f46c779f3 Mon Sep 17 00:00:00 2001 From: Michael Small Date: Sat, 10 Oct 2026 15:40:09 -0500 Subject: [PATCH 1/5] docs: update autocomplete display/value example to signal forms --- .../autocomplete-display-example.html | 18 ++++- .../autocomplete-display-example.ts | 71 +++++++++++-------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.html b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.html index 479304681f7a..905ab70cec2f 100644 --- a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.html +++ b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.html @@ -1,11 +1,23 @@ +
value: {{myControl().value().value | json}}
+
rawValue: {{myControl().value().rawValue | json}}
+
Assignee - - - @for (option of filteredOptions | async; track option) { + + + @for (option of filteredOptions(); track option) { {{option.name}} } + @for (error of myControl.rawValue().errors(); track error) { + {{error.message}} + }
diff --git a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts index b711580e27bf..6167804a9bb1 100644 --- a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts @@ -1,11 +1,9 @@ -import {Component} from '@angular/core'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {Observable} from 'rxjs'; -import {map, startWith} from 'rxjs/operators'; -import {AsyncPipe} from '@angular/common'; +import {Component, computed, signal} from '@angular/core'; import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {JsonPipe} from '@angular/common'; +import {form, FormField, required, validate} from '@angular/forms/signals'; export interface User { name: string; @@ -18,37 +16,54 @@ export interface User { selector: 'autocomplete-display-example', templateUrl: 'autocomplete-display-example.html', styleUrl: 'autocomplete-display-example.css', - imports: [ - FormsModule, - MatFormFieldModule, - MatInputModule, - MatAutocompleteModule, - ReactiveFormsModule, - AsyncPipe, - ], + imports: [MatFormFieldModule, MatInputModule, MatAutocompleteModule, JsonPipe, FormField], }) export class AutocompleteDisplayExample { - myControl = new FormControl(''); - options: User[] = [{name: 'Mary'}, {name: 'Shelley'}, {name: 'Igor'}]; - filteredOptions: Observable; - - constructor() { - this.filteredOptions = this.myControl.valueChanges.pipe( - startWith(''), - map(value => { - const name = typeof value === 'string' ? value : value?.name; - return name ? this._filter(name as string) : this.options.slice(); - }), - ); - } + // TODO - split form / model into own class fields? + // TODO - make case matter? + protected myControl = form( + signal<{rawValue: string; value: User | null}>({rawValue: '', value: null}), + p => { + validate(p.rawValue, ({valueOf}) => { + const rawValue = valueOf(p.rawValue); + const matchingOption = + this.options().find(option => option.name.toLowerCase() === rawValue.toLowerCase()) ?? + null; + + if (!matchingOption) { + return {message: 'Value must be one of the options', kind: 'exactMatch'}; + } + return null; + }); + }, + ); + + protected options = signal([{name: 'Mary'}, {name: 'Shelley'}, {name: 'Igor'}]); + + protected filteredOptions = computed(() => { + const name = this.myControl().value().rawValue; + const options = this.options(); + return name ? this._filter(name) : options; + }); - displayFn(user: User): string { + protected displayFn(user: User | null): string { return user && user.name ? user.name : ''; } + protected updateRawValue(rawValue: string): void { + const matchingOption = + this.options().find(option => option.name.toLowerCase() === rawValue.trim().toLowerCase()) ?? + null; + this.myControl().value.update(model => ({...model, value: matchingOption})); + } + + protected selectOption(user: User): void { + this.myControl().value.update(model => ({...model, rawValue: user.name, value: user})); + } + private _filter(name: string): User[] { const filterValue = name.toLowerCase(); - return this.options.filter(option => option.name.toLowerCase().includes(filterValue)); + return this.options().filter(option => option.name.toLowerCase().includes(filterValue)); } } From 9bdde92ce0d26b024bb04d5e3720068fb5000988 Mon Sep 17 00:00:00 2001 From: Michael Small Date: Sat, 10 Oct 2026 15:42:14 -0500 Subject: [PATCH 2/5] docs: update autocomplete filter example to signal forms --- .../autocomplete-filter-example.html | 4 +-- .../autocomplete-filter-example.ts | 28 ++++--------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.html b/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.html index 8ac756c675af..3bf8a109f3ce 100644 --- a/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.html +++ b/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.html @@ -5,10 +5,10 @@ placeholder="Pick one" aria-label="Number" matInput - [formControl]="myControl" + [formField]="form" [matAutocomplete]="auto"> - @for (option of filteredOptions | async; track option) { + @for (option of filteredOptions(); track option) { {{option}} } diff --git a/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.ts b/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.ts index 45c462c7608b..fd29ca33aab9 100644 --- a/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-filter/autocomplete-filter-example.ts @@ -1,8 +1,5 @@ -import {Component} from '@angular/core'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {Observable} from 'rxjs'; -import {map, startWith} from 'rxjs/operators'; -import {AsyncPipe} from '@angular/common'; +import {Component, computed, signal} from '@angular/core'; +import {form, FormField} from '@angular/forms/signals'; import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -14,26 +11,13 @@ import {MatFormFieldModule} from '@angular/material/form-field'; selector: 'autocomplete-filter-example', templateUrl: 'autocomplete-filter-example.html', styleUrl: 'autocomplete-filter-example.css', - imports: [ - FormsModule, - MatFormFieldModule, - MatInputModule, - MatAutocompleteModule, - ReactiveFormsModule, - AsyncPipe, - ], + imports: [MatFormFieldModule, MatInputModule, MatAutocompleteModule, FormField], }) export class AutocompleteFilterExample { - myControl = new FormControl(''); - options: string[] = ['One', 'Two', 'Three']; - filteredOptions: Observable; + protected form = form(signal('')); + protected options = ['One', 'Two', 'Three'] as const; - constructor() { - this.filteredOptions = this.myControl.valueChanges.pipe( - startWith(''), - map(value => this._filter(value || '')), - ); - } + protected filteredOptions = computed(() => this._filter(this.form().value())); private _filter(value: string): string[] { const filterValue = value.toLowerCase(); From dc29f8f7793d9fa8b4472f29cf7cef0b741593dc Mon Sep 17 00:00:00 2001 From: Michael Small Date: Sat, 10 Oct 2026 15:47:47 -0500 Subject: [PATCH 3/5] docs: update autocomplete optgroup example to signal forms --- .../autocomplete-optgroup-example.html | 6 +- .../autocomplete-optgroup-example.ts | 233 ++++++++---------- 2 files changed, 111 insertions(+), 128 deletions(-) diff --git a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.html b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.html index 8181071dec7a..18d929846071 100644 --- a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.html +++ b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.html @@ -1,14 +1,14 @@ -
+ States Group - @for (group of stateGroupOptions | async; track group) { + @for (group of stateGroupOptions(); track group) { @for (name of group.names; track name) { {{name}} diff --git a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts index 28da1f125f07..8b9bc2263f2d 100644 --- a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts @@ -1,11 +1,9 @@ -import {Component, inject} from '@angular/core'; -import {FormBuilder, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {Observable} from 'rxjs'; -import {startWith, map} from 'rxjs/operators'; -import {AsyncPipe} from '@angular/common'; +import {Component, computed, signal} from '@angular/core'; +import {FormField, FormRoot} from '@angular/forms/signals'; import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {form} from '@angular/forms/signals'; export interface StateGroup { letter: string; @@ -24,135 +22,120 @@ export const _filter = (opt: string[], value: string): string[] => { @Component({ selector: 'autocomplete-optgroup-example', templateUrl: 'autocomplete-optgroup-example.html', - imports: [ - FormsModule, - ReactiveFormsModule, - MatFormFieldModule, - MatInputModule, - MatAutocompleteModule, - AsyncPipe, - ], + imports: [MatFormFieldModule, MatInputModule, MatAutocompleteModule, FormField, FormRoot], }) export class AutocompleteOptgroupExample { - private _formBuilder = inject(FormBuilder); + protected stateForm = form(signal({stateGroup: ''})); + private _stateGroups = stateGroups; - stateForm = this._formBuilder.group({ - stateGroup: '', - }); - - stateGroups: StateGroup[] = [ - { - letter: 'A', - names: ['Alabama', 'Alaska', 'Arizona', 'Arkansas'], - }, - { - letter: 'C', - names: ['California', 'Colorado', 'Connecticut'], - }, - { - letter: 'D', - names: ['Delaware'], - }, - { - letter: 'F', - names: ['Florida'], - }, - { - letter: 'G', - names: ['Georgia'], - }, - { - letter: 'H', - names: ['Hawaii'], - }, - { - letter: 'I', - names: ['Idaho', 'Illinois', 'Indiana', 'Iowa'], - }, - { - letter: 'K', - names: ['Kansas', 'Kentucky'], - }, - { - letter: 'L', - names: ['Louisiana'], - }, - { - letter: 'M', - names: [ - 'Maine', - 'Maryland', - 'Massachusetts', - 'Michigan', - 'Minnesota', - 'Mississippi', - 'Missouri', - 'Montana', - ], - }, - { - letter: 'N', - names: [ - 'Nebraska', - 'Nevada', - 'New Hampshire', - 'New Jersey', - 'New Mexico', - 'New York', - 'North Carolina', - 'North Dakota', - ], - }, - { - letter: 'O', - names: ['Ohio', 'Oklahoma', 'Oregon'], - }, - { - letter: 'P', - names: ['Pennsylvania'], - }, - { - letter: 'R', - names: ['Rhode Island'], - }, - { - letter: 'S', - names: ['South Carolina', 'South Dakota'], - }, - { - letter: 'T', - names: ['Tennessee', 'Texas'], - }, - { - letter: 'U', - names: ['Utah'], - }, - { - letter: 'V', - names: ['Vermont', 'Virginia'], - }, - { - letter: 'W', - names: ['Washington', 'West Virginia', 'Wisconsin', 'Wyoming'], - }, - ]; - - stateGroupOptions: Observable; - - constructor() { - this.stateGroupOptions = this.stateForm.get('stateGroup')!.valueChanges.pipe( - startWith(''), - map(value => this._filterGroup(value || '')), - ); - } + protected stateGroupOptions = computed(() => + this._filterGroup(this.stateForm.stateGroup().value()), + ); private _filterGroup(value: string): StateGroup[] { if (value) { - return this.stateGroups + return this._stateGroups .map(group => ({letter: group.letter, names: _filter(group.names, value)})) .filter(group => group.names.length > 0); } - return this.stateGroups; + return this._stateGroups; } } + +const stateGroups: StateGroup[] = [ + { + letter: 'A', + names: ['Alabama', 'Alaska', 'Arizona', 'Arkansas'], + }, + { + letter: 'C', + names: ['California', 'Colorado', 'Connecticut'], + }, + { + letter: 'D', + names: ['Delaware'], + }, + { + letter: 'F', + names: ['Florida'], + }, + { + letter: 'G', + names: ['Georgia'], + }, + { + letter: 'H', + names: ['Hawaii'], + }, + { + letter: 'I', + names: ['Idaho', 'Illinois', 'Indiana', 'Iowa'], + }, + { + letter: 'K', + names: ['Kansas', 'Kentucky'], + }, + { + letter: 'L', + names: ['Louisiana'], + }, + { + letter: 'M', + names: [ + 'Maine', + 'Maryland', + 'Massachusetts', + 'Michigan', + 'Minnesota', + 'Mississippi', + 'Missouri', + 'Montana', + ], + }, + { + letter: 'N', + names: [ + 'Nebraska', + 'Nevada', + 'New Hampshire', + 'New Jersey', + 'New Mexico', + 'New York', + 'North Carolina', + 'North Dakota', + ], + }, + { + letter: 'O', + names: ['Ohio', 'Oklahoma', 'Oregon'], + }, + { + letter: 'P', + names: ['Pennsylvania'], + }, + { + letter: 'R', + names: ['Rhode Island'], + }, + { + letter: 'S', + names: ['South Carolina', 'South Dakota'], + }, + { + letter: 'T', + names: ['Tennessee', 'Texas'], + }, + { + letter: 'U', + names: ['Utah'], + }, + { + letter: 'V', + names: ['Vermont', 'Virginia'], + }, + { + letter: 'W', + names: ['Washington', 'West Virginia', 'Wisconsin', 'Wyoming'], + }, +]; From 1e479e0e674d786245ac9f99155843a5bfa9dae4 Mon Sep 17 00:00:00 2001 From: Michael Small Date: Sat, 10 Oct 2026 15:55:40 -0500 Subject: [PATCH 4/5] docs: update autocomplete overview example to signal forms --- .../autocomplete-overview-example.html | 8 ++--- .../autocomplete-overview-example.ts | 30 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.html b/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.html index 51c22a153544..d49826451edd 100644 --- a/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.html +++ b/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.html @@ -4,9 +4,9 @@ + [formField]="stateField.value"> - @for (state of filteredStates | async; track state) { + @for (state of filteredStates(); track state) { {{state.name}} | @@ -19,8 +19,8 @@
+ [checked]="stateField.disabled().value" + (change)="stateField().value.update(value => ({...value, disabled: !value.disabled}))"> Disable Input? diff --git a/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.ts b/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.ts index c45f505b4b71..67d996bea464 100644 --- a/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-overview/autocomplete-overview-example.ts @@ -1,9 +1,6 @@ -import {Component} from '@angular/core'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {Observable} from 'rxjs'; -import {map, startWith} from 'rxjs/operators'; +import {Component, computed, signal} from '@angular/core'; +import {disabled, form, FormField} from '@angular/forms/signals'; import {MatSlideToggleModule} from '@angular/material/slide-toggle'; -import {AsyncPipe} from '@angular/common'; import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -22,20 +19,24 @@ export interface State { templateUrl: 'autocomplete-overview-example.html', styleUrl: 'autocomplete-overview-example.css', imports: [ - FormsModule, MatFormFieldModule, MatInputModule, MatAutocompleteModule, - ReactiveFormsModule, MatSlideToggleModule, - AsyncPipe, + FormField, ], }) export class AutocompleteOverviewExample { - stateCtrl = new FormControl(''); - filteredStates: Observable; + protected stateField = form(signal({value: '', disabled: false}), p => { + disabled(p.value, { + when: ({valueOf}) => valueOf(p.disabled), + }); + }); + protected filteredStates = computed(() => + this._filterStates(this.stateField().value().value), + ); - states: State[] = [ + protected states: State[] = [ { name: 'Arkansas', population: '2.978M', @@ -62,13 +63,6 @@ export class AutocompleteOverviewExample { }, ]; - constructor() { - this.filteredStates = this.stateCtrl.valueChanges.pipe( - startWith(''), - map(state => (state ? this._filterStates(state) : this.states.slice())), - ); - } - private _filterStates(value: string): State[] { const filterValue = value.toLowerCase(); From 5f51d930467366595b9d564d0b84ea571f330a9f Mon Sep 17 00:00:00 2001 From: Michael Small Date: Sat, 10 Oct 2026 15:57:20 -0500 Subject: [PATCH 5/5] docs: lint --- .../autocomplete-display/autocomplete-display-example.ts | 2 +- .../autocomplete-optgroup/autocomplete-optgroup-example.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts index 6167804a9bb1..b4e8dd119364 100644 --- a/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-display/autocomplete-display-example.ts @@ -3,7 +3,7 @@ import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {JsonPipe} from '@angular/common'; -import {form, FormField, required, validate} from '@angular/forms/signals'; +import {form, FormField, validate} from '@angular/forms/signals'; export interface User { name: string; diff --git a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts index 8b9bc2263f2d..3784984a93ce 100644 --- a/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts +++ b/src/components-examples/material/autocomplete/autocomplete-optgroup/autocomplete-optgroup-example.ts @@ -1,9 +1,8 @@ import {Component, computed, signal} from '@angular/core'; -import {FormField, FormRoot} from '@angular/forms/signals'; +import {FormField, FormRoot, form} from '@angular/forms/signals'; import {MatAutocompleteModule} from '@angular/material/autocomplete'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; -import {form} from '@angular/forms/signals'; export interface StateGroup { letter: string;