Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components-examples/material/select/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
export {SelectCustomTriggerExample} from './select-custom-trigger/select-custom-trigger-example';
export {SelectDisabledExample} from './select-disabled/select-disabled-example';
export {SelectDisabledSignalFormExample} from './select-disabled-signal-form/select-disabled-signal-form-example';
export {SelectErrorStateMatcherExample} from './select-error-state-matcher/select-error-state-matcher-example';
export {SelectFormExample} from './select-form/select-form-example';
export {SelectFormSignalFormExample} from './select-form-signal-form/select-form-signal-form-example';
export {SelectHintErrorExample} from './select-hint-error/select-hint-error-example';
export {SelectMultipleExample} from './select-multiple/select-multiple-example';
export {SelectMultipleSignalFormExample} from './select-multiple-signal-form/select-multiple-signal-form-example';
export {SelectNoRippleExample} from './select-no-ripple/select-no-ripple-example';
export {SelectOptgroupExample} from './select-optgroup/select-optgroup-example';
export {SelectOverviewExample} from './select-overview/select-overview-example';
Expand All @@ -13,4 +16,5 @@ export {SelectValueBindingExample} from './select-value-binding/select-value-bin
export {SelectReactiveFormExample} from './select-reactive-form/select-reactive-form-example';
export {SelectInitialValueExample} from './select-initial-value/select-initial-value-example';
export {SelectSelectableNullExample} from './select-selectable-null/select-selectable-null-example';
export {SelectHintErrorSignalFormExample} from './select-hint-error-signal-form/select-hint-error-signal-form-example';
export {SelectHarnessExample} from './select-harness/select-harness-example';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<p>
<mat-checkbox [formField]="form.disableField">Disable select</mat-checkbox>
</p>

<h4>mat-select</h4>
<mat-form-field>
<mat-label>Choose an option</mat-label>
<mat-select [formField]="form.option">
<mat-option value="option1">Option 1</mat-option>
<mat-option value="option2" disabled>Option 2 (disabled)</mat-option>
<mat-option value="option3">Option 3</mat-option>
</mat-select>
</mat-form-field>

<h4>native html select</h4>
<mat-form-field>
<mat-label>Choose an option</mat-label>
<select matNativeControl [formField]="form.vehicle">
<option value="" selected></option>
<option value="volvo">Volvo</option>
<option value="saab" disabled>Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
</mat-form-field>

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Component, signal} from '@angular/core';
import {disabled, form, FormField} from '@angular/forms/signals';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatCheckboxModule} from '@angular/material/checkbox';

/** @title Disabled select (signal forms) */
@Component({
selector: 'select-disabled-signal-form-example',
templateUrl: 'select-disabled-signal-form-example.html',
imports: [MatCheckboxModule, FormField, MatFormFieldModule, MatSelectModule, MatInputModule],
})
export class SelectDisabledSignalFormExample {
// TODO - more distinct name?
protected form = form(signal({option: '', vehicle: '', disableField: false}), p => {
disabled(p.option, {when: ({valueOf}) => valueOf(p.disableField)});
disabled(p.vehicle, {when: ({valueOf}) => valueOf(p.disableField)});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<form>
<h4>mat-select</h4>
<mat-form-field>
<mat-label>Favorite food</mat-label>
<mat-select [formField]="selectedValue">
@for (food of foods; track food) {
<mat-option [value]="food.value">{{food.viewValue}}</mat-option>
}
</mat-select>
</mat-form-field>
<p> Selected food: {{selectedValue().value()}} </p>
<h4>native html select</h4>
<mat-form-field>
<mat-label>Favorite car</mat-label>
<select matNativeControl [formField]="selectedCar">
<option value="" selected></option>
@for (car of cars; track car) {
<option [value]="car.value">{{car.viewValue}}</option>
}
</select>
</mat-form-field>
<p> Selected car: {{selectedCar().value()}} </p>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Component, signal} from '@angular/core';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {form, FormField} from '@angular/forms/signals';

interface Food {
value: string;
viewValue: string;
}

interface Car {
value: string;
viewValue: string;
}

/**
* @title Select in a form (signal forms)
*/
@Component({
selector: 'select-form-signal-form-example',
templateUrl: 'select-form-signal-form-example.html',
imports: [FormField, MatFormFieldModule, MatSelectModule, MatInputModule],
})
export class SelectFormSignalFormExample {
protected selectedValue = form(signal(''));
protected selectedCar = form(signal(''));

protected foods: Food[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'},
];

protected cars: Car[] = [
{value: 'volvo', viewValue: 'Volvo'},
{value: 'saab', viewValue: 'Saab'},
{value: 'mercedes', viewValue: 'Mercedes'},
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Car {
}

/**
* @title Select in a form
* @title Select in a form (template-driven forms)

@msmallest msmallest Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@crisbeto When updating this example, I realized that merely adding one example would make things tricky, without a more comprehensive overhaul.

My reasoning:

  1. Adding the respective signal form example made me specify this existing example as (template-driven forms, as is consistent with my previous PRs.
  2. Because this example is renamed, it is highlighted in the "Overview" page. In retrospect, this has already happened to other component examples embedded in overviews, but for point 4 I think this case with selects is different.
  3. I would then just replace the embedded example in the overview with my new signal form example
  4. Problem A: Now that there is one signal form example in the overview, but all the other embedded examples in the overview are fairly iterative on top of this basic example that is converted, I think there would be an inconsistency.
  5. Problem B: All those examples embedded in the "Overview" could all be replaced by additional signal form examples, but now there would be very many examples in the "Examples" tab.

How should I go about this? In my opinion, I would just do the following two things

  1. Replace all form related examples and with signal form examples, rather than adding more examples on top of the existing ones
  2. Make a singular "Select with forms" which has reactive/template/signal forms just like in a previous PR, for the button toggles: https://material.angular.dev/components/button-toggle/examples#button-toggle-forms

My reasoning being that signal forms are now the encouraged default for forms, the docs recently updated the page for custom Material controls to be signal forms first, and that there are plenty of older docs versions to look for older iterations of template/reactive form examples.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there's some more awkwardness left in the support for signal forms, we should probably just promote those in all the examples.

@msmallest msmallest Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR despite select multiple definitely being one of the remaining akwardnesses of signal forms left, I think mat-select multiple handles it perfectly. Thoughts? If this all sounds right, I will proceed with the rest of the examples.

Unless there's some more awkwardness left in the support for signal forms, we should probably just promote those in all the examples.

Now that you mention it, select multiple is the one edge case with signal forms that has problems right now: angular/angular#68511 There is an outstanding PR pending more decisions in this issue.

However, it seems that it works great in Material. Example project I made for reference: https://stackblitz.com/edit/mhqdr1d1?file=src%2Fexample%2Fselect-multiple-example.ts. Boolean, number, string, string array (added this based on my follow notes on unit tests for select), and object arrays in a mat-select multiple each. I have not looked at the internals of how mat-select multiple is implemented, but in practice mat-select seems like it lines up with the guidance from the Angular Skills which was just amended today: angular/angular@306b99c.

"<select multiple> is unsupported. Use one boolean field + checkbox per option"

      <!-- Multiple choices: one boolean field per option, bound to a checkbox -->
      <label>
        <input type="checkbox" [formField]="bookingForm.package.extras.wifi" />
        WiFi
      </label>
      <label>
        <input type="checkbox" [formField]="bookingForm.package.extras.gym" />
        Gym
      </label>

I threw together a temp branch with a signal forms version of the mat-select harness test and the mat-select itself test, and modified the multiple variants

I had to modify some assertions due to value equality differences, null vs [] defaults for values, and a few other things that you may want to vet if I was in the right to do so, but with all that considered, those passed once adjusted to the new constraints.

*/
@Component({
selector: 'select-form-example',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<h4>mat select</h4>
<mat-form-field>
<mat-label>Favorite animal</mat-label>
<mat-select [formField]="animalControl">
<mat-option>--</mat-option>
@for (animal of animals; track animal) {
<mat-option [value]="animal.name">{{animal.name}}</mat-option>
}
</mat-select>
@if (animalControl().getError('required')) {
<mat-error>Please choose an animal</mat-error>
}
@if (animalSound(); as animalSound) {
<mat-hint>{{animalSound}}</mat-hint>
}
</mat-form-field>

<h4>native html select</h4>
<mat-form-field>
<mat-label>Select your car (required)</mat-label>
<select matNativeControl [formField]="selectFormControl">
<option label="--select something --"></option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
@if (selectFormControl().getError('required')) {
<mat-error>This field is required</mat-error>
}
<mat-hint>You can pick up your favorite car here</mat-hint>
</mat-form-field>

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Component, computed, signal} from '@angular/core';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {form, FormField, required} from '@angular/forms/signals';

interface Animal {
name: string;
sound: string;
}

/** @title Select with form field features (signal forms) */
@Component({
selector: 'select-hint-error-signal-form-example',
templateUrl: 'select-hint-error-signal-form-example.html',
imports: [MatFormFieldModule, MatSelectModule, FormField, MatInputModule],
})
export class SelectHintErrorSignalFormExample {
protected animalControl = form(signal<string>(''), p => {
required(p);
});
protected selectFormControl = form(signal(''), p => {
required(p);
});

protected animalSound = computed<string | undefined>(
() => this.animals.find(a => a.name === this.animalControl().value())?.sound,
);

protected animals: Animal[] = [
{name: 'Dog', sound: 'Woof!'},
{name: 'Cat', sound: 'Meow!'},
{name: 'Cow', sound: 'Moo!'},
{name: 'Fox', sound: 'Wa-pa-pa-pa-pa-pa-pow!'},
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<mat-form-field>
<mat-label>Toppings</mat-label>
<mat-select [formField]="toppings" multiple>
@for (topping of toppingList; track topping) {
<mat-option [value]="topping">{{topping}}</mat-option>
}
</mat-select>
</mat-form-field>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Component, signal} from '@angular/core';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {form, FormField} from '@angular/forms/signals';

/** @title Select with multiple selection (signal forms) */
@Component({
selector: 'select-multiple-signal-form-example',
templateUrl: 'select-multiple-signal-form-example.html',
imports: [MatFormFieldModule, MatSelectModule, FormField],
})
export class SelectMultipleSignalFormExample {
toppings = form(signal<string[]>([]));
toppingList = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato'] as const;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import {MatFormFieldModule} from '@angular/material/form-field';
imports: [MatFormFieldModule, MatSelectModule, FormsModule, ReactiveFormsModule],
})
export class SelectMultipleExample {
toppings = new FormControl('');
toppings = new FormControl<string[]>([]);
toppingList: string[] = ['Extra cheese', 'Mushroom', 'Onion', 'Pepperoni', 'Sausage', 'Tomato'];
}
14 changes: 10 additions & 4 deletions src/material/select/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Including `FormField` (signal forms), `FormsModule` (`NgModel`) and `ReactiveFor
`compareWith` function. (Additional information about using a custom `compareWith` function can be found in the
[Angular forms documentation](https://angular.dev/api/forms/SelectControlValueAccessor#compareWith)).

<!-- example(select-form) -->
<!-- example(select-form-signal-form) -->

### Form field features

Expand All @@ -44,7 +44,7 @@ include error messages, hint text, prefix & suffix, and theming. For additional
these features, see the
[form field documentation](https://material.angular.dev/components/form-field/overview).

<!-- example(select-hint-error) -->
<!-- example(select-hint-error-signal-form) -->

### Setting a static placeholder

Expand All @@ -58,11 +58,17 @@ In some cases that `<mat-form-field>` may use the placeholder as the label (see

It is possible to disable the entire select or individual options in the select by using the
disabled property on the `<select>` or `<mat-select>` and the `<option>` or `<mat-option>` elements respectively.

For signal forms, the [`disabled`](https://angular.dev/api/forms/signals/disabled) rule sets the
disabled behavior of the `<select>` or `<mat-select>`, and the disabled property should not be bound
to the selects in the template. However, individual `<option>` or `<mat-option>` elements can be
bound as disabled in the template.

When working with Reactive Forms, the select component can be disabled/enabled via form controls.
This can be accomplished by creating a `FormControl` with the disabled property
`FormControl({value: '', disabled: true})` or using `FormControl.enable()`, `FormControl.disable()`.

<!-- example(select-disabled) -->
<!-- example(select-disabled-signal-form) -->

### Resetting the select value

Expand Down Expand Up @@ -99,7 +105,7 @@ Using multiple selection with a native select element (`<select multiple>`) is d
inside `<mat-form-field>`, as the inline listbox appearance is inconsistent with other
Material Design components.

<!-- example(select-multiple) -->
<!-- example(select-multiple-signal-form) -->

### Customizing the trigger label

Expand Down
Loading