Form
Accordion Multiselect
Usage#
import { WDS_ACCORDION_MULTISELECT } from '@wds/angular/accordion-multiselect';Examples#
Default#
wds-accordion-item sections that expand independently. In HTML value and expanded are space-separated lists; show-checkbox adds a checkbox to each option.
import { Component } from '@angular/core';
import { WdsAccordionMultiselectComponent, WdsAccordionOptionComponent } from '@wds/angular/accordion-multiselect';
import { WdsAccordionItemComponent } from '@wds/angular/accordion';
@Component({
selector: 'app-accordion-multiselect-default-demo',
imports: [WdsAccordionMultiselectComponent, WdsAccordionOptionComponent, WdsAccordionItemComponent],
template: `
<wds-accordion-multiselect class="w-full max-w-lg" aria-label="Services" show-checkbox expanded="nails" value="gel">
<wds-accordion-item value="nails" heading="Nails">
<wds-accordion-option value="classic">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Classic manicure</div>
<div class="text-muted-foreground">Cuticle care, shaping and polish.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$30</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="french">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">French manicure</div>
<div class="text-muted-foreground">Classic white tips.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$40</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="gel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Gel manicure</div>
<div class="text-muted-foreground">Long-lasting color for up to three weeks.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$45</div>
<div class="text-muted-foreground">75 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="pedicure">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Spa pedicure</div>
<div class="text-muted-foreground">Scrub, mask and foot massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$60</div>
<div class="text-muted-foreground">90 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
<wds-accordion-item value="face" heading="Facial treatments">
<wds-accordion-option value="cleansing">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Hydrogen cleansing</div>
<div class="text-muted-foreground">Deep cleansing and hydration.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$70</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="peel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Chemical peel</div>
<div class="text-muted-foreground">Evens out skin tone and texture.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$80</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="kobido">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Kobido massage</div>
<div class="text-muted-foreground">Japanese lifting massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$85</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
</wds-accordion-multiselect>
`,
})
export class AccordionMultiselectDefaultDemo {}Without checkbox#
show-checkbox a selected option is marked by its background (--wds-accordion-option-selected).
import { Component } from '@angular/core';
import { WdsAccordionMultiselectComponent, WdsAccordionOptionComponent } from '@wds/angular/accordion-multiselect';
import { WdsAccordionItemComponent } from '@wds/angular/accordion';
@Component({
selector: 'app-accordion-multiselect-without-checkbox-demo',
imports: [WdsAccordionMultiselectComponent, WdsAccordionOptionComponent, WdsAccordionItemComponent],
template: `
<wds-accordion-multiselect class="w-full max-w-lg" aria-label="Services" expanded="nails face" value="french">
<wds-accordion-item value="nails" heading="Nails">
<wds-accordion-option value="classic">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Classic manicure</div>
<div class="text-muted-foreground">Cuticle care, shaping and polish.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$30</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="french">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">French manicure</div>
<div class="text-muted-foreground">Classic white tips.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$40</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="gel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Gel manicure</div>
<div class="text-muted-foreground">Long-lasting color for up to three weeks.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$45</div>
<div class="text-muted-foreground">75 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="pedicure">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Spa pedicure</div>
<div class="text-muted-foreground">Scrub, mask and foot massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$60</div>
<div class="text-muted-foreground">90 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
<wds-accordion-item value="face" heading="Facial treatments">
<wds-accordion-option value="cleansing">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Hydrogen cleansing</div>
<div class="text-muted-foreground">Deep cleansing and hydration.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$70</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="peel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Chemical peel</div>
<div class="text-muted-foreground">Evens out skin tone and texture.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$80</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="kobido">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Kobido massage</div>
<div class="text-muted-foreground">Japanese lifting massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$85</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
</wds-accordion-multiselect>
`,
})
export class AccordionMultiselectWithoutCheckboxDemo {}Hide count#
hide-count turns it off.
import { Component } from '@angular/core';
import { WdsAccordionMultiselectComponent, WdsAccordionOptionComponent } from '@wds/angular/accordion-multiselect';
import { WdsAccordionItemComponent } from '@wds/angular/accordion';
@Component({
selector: 'app-accordion-multiselect-hide-count-demo',
imports: [WdsAccordionMultiselectComponent, WdsAccordionOptionComponent, WdsAccordionItemComponent],
template: `
<wds-accordion-multiselect class="w-full max-w-lg" aria-label="Services" show-checkbox hide-count value="classic peel">
<wds-accordion-item value="nails" heading="Nails">
<wds-accordion-option value="classic">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Classic manicure</div>
<div class="text-muted-foreground">Cuticle care, shaping and polish.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$30</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="french">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">French manicure</div>
<div class="text-muted-foreground">Classic white tips.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$40</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="gel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Gel manicure</div>
<div class="text-muted-foreground">Long-lasting color for up to three weeks.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$45</div>
<div class="text-muted-foreground">75 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="pedicure">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Spa pedicure</div>
<div class="text-muted-foreground">Scrub, mask and foot massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$60</div>
<div class="text-muted-foreground">90 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
<wds-accordion-item value="face" heading="Facial treatments">
<wds-accordion-option value="cleansing">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Hydrogen cleansing</div>
<div class="text-muted-foreground">Deep cleansing and hydration.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$70</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="peel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Chemical peel</div>
<div class="text-muted-foreground">Evens out skin tone and texture.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$80</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="kobido">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Kobido massage</div>
<div class="text-muted-foreground">Japanese lifting massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$85</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
</wds-accordion-multiselect>
`,
})
export class AccordionMultiselectHideCountDemo {}Controlled#
value is an array of the selected option values; wds-change reports it in detail.value, with the toggled option and its new selected state.
import { Component, signal } from '@angular/core';
import { FormField, form } from '@angular/forms/signals';
import { WDS_ACCORDION_MULTISELECT } from '@wds/angular/accordion-multiselect';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsAccordionMultiselectField } from '@wds/angular/forms';
const CATEGORIES = [
{
id: 'nails',
name: 'Nails',
services: [
{ id: 'classic', name: 'Classic manicure', description: 'Cuticle care, shaping and polish.', duration: '45 min', price: '$30' },
{ id: 'french', name: 'French manicure', description: 'Classic white tips.', duration: '60 min', price: '$40' },
{ id: 'gel', name: 'Gel manicure', description: 'Long-lasting color for up to three weeks.', duration: '75 min', price: '$45' },
{ id: 'pedicure', name: 'Spa pedicure', description: 'Scrub, mask and foot massage.', duration: '90 min', price: '$60' },
],
},
{
id: 'face',
name: 'Facial treatments',
services: [
{ id: 'cleansing', name: 'Hydrogen cleansing', description: 'Deep cleansing and hydration.', duration: '60 min', price: '$70' },
{ id: 'peel', name: 'Chemical peel', description: 'Evens out skin tone and texture.', duration: '45 min', price: '$80' },
{ id: 'kobido', name: 'Kobido massage', description: 'Japanese lifting massage.', duration: '60 min', price: '$85' },
],
},
];
@Component({
selector: 'app-accordion-multiselect-controlled-demo',
imports: [...WDS_ACCORDION_MULTISELECT, FormField, WdsAccordionMultiselectField, WdsButtonComponent],
template: `
<div class="flex w-lg max-w-full flex-col gap-3">
<wds-accordion-multiselect aria-label="Services" [expanded]="['nails', 'face']" [formField]="bookingForm.services">
@for (category of categories; track category.id) {
<wds-accordion-item [value]="category.id" [heading]="category.name">
@for (service of category.services; track service.id) {
<wds-accordion-option [value]="service.id">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">{{ service.name }}</div>
<div class="text-muted-foreground">{{ service.description }}</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">{{ service.price }}</div>
<div class="text-muted-foreground">{{ service.duration }}</div>
</div>
</div>
</wds-accordion-option>
}
</wds-accordion-item>
}
</wds-accordion-multiselect>
<div class="flex items-center justify-between gap-4 text-sm">
<span class="text-muted-foreground">Selected: {{ booking().services.join(', ') || 'none' }}</span>
<wds-button variant="outline" size="sm" [disabled]="!booking().services.length" (click)="clear()">Clear</wds-button>
</div>
</div>
`,
})
export class AccordionMultiselectControlledDemo {
protected readonly categories = CATEGORIES;
protected readonly booking = signal({ services: ['french'] });
protected readonly bookingForm = form(this.booking);
protected clear(): void {
this.booking.set({ services: [] });
}
}Disabled#
disabled on the control disables every option; on an option it disables that option.
import { Component } from '@angular/core';
import { WdsAccordionMultiselectComponent, WdsAccordionOptionComponent } from '@wds/angular/accordion-multiselect';
import { WdsAccordionItemComponent } from '@wds/angular/accordion';
@Component({
selector: 'app-accordion-multiselect-disabled-demo',
imports: [WdsAccordionMultiselectComponent, WdsAccordionOptionComponent, WdsAccordionItemComponent],
template: `
<wds-accordion-multiselect class="w-full max-w-lg" aria-label="Services" show-checkbox disabled expanded="nails" value="gel">
<wds-accordion-item value="nails" heading="Nails">
<wds-accordion-option value="classic">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Classic manicure</div>
<div class="text-muted-foreground">Cuticle care, shaping and polish.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$30</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="french">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">French manicure</div>
<div class="text-muted-foreground">Classic white tips.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$40</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="gel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Gel manicure</div>
<div class="text-muted-foreground">Long-lasting color for up to three weeks.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$45</div>
<div class="text-muted-foreground">75 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="pedicure">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Spa pedicure</div>
<div class="text-muted-foreground">Scrub, mask and foot massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$60</div>
<div class="text-muted-foreground">90 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
<wds-accordion-item value="face" heading="Facial treatments">
<wds-accordion-option value="cleansing">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Hydrogen cleansing</div>
<div class="text-muted-foreground">Deep cleansing and hydration.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$70</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="peel">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Chemical peel</div>
<div class="text-muted-foreground">Evens out skin tone and texture.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$80</div>
<div class="text-muted-foreground">45 min</div>
</div>
</div>
</wds-accordion-option>
<wds-accordion-option value="kobido">
<div class="flex items-start justify-between gap-4">
<div>
<div class="font-medium">Kobido massage</div>
<div class="text-muted-foreground">Japanese lifting massage.</div>
</div>
<div class="shrink-0 text-right">
<div class="font-medium">$85</div>
<div class="text-muted-foreground">60 min</div>
</div>
</div>
</wds-accordion-option>
</wds-accordion-item>
</wds-accordion-multiselect>
`,
})
export class AccordionMultiselectDisabledDemo {}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsAccordionMultiselectField connects the control to [formField] (a string[]). required() treats an empty array as a value, so use minLength(path, 1) for "at least one".
import { Component, computed, signal } from '@angular/core';
import { FormField, FormRoot, form, minLength } from '@angular/forms/signals';
import { WDS_ACCORDION_MULTISELECT } from '@wds/angular/accordion-multiselect';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsAccordionMultiselectField } from '@wds/angular/forms';
const CATEGORIES = [
{
id: 'nails',
name: 'Nails',
services: [
{ id: 'classic', name: 'Classic manicure', price: '$30' },
{ id: 'gel', name: 'Gel manicure', price: '$45' },
{ id: 'pedicure', name: 'Spa pedicure', price: '$60' },
],
},
{
id: 'face',
name: 'Facial treatments',
services: [
{ id: 'cleansing', name: 'Hydrogen cleansing', price: '$70' },
{ id: 'peel', name: 'Chemical peel', price: '$80' },
],
},
];
@Component({
selector: 'app-accordion-multiselect-signal-forms-demo',
imports: [...WDS_ACCORDION_MULTISELECT, ...WDS_FIELD, FormField, FormRoot, WdsAccordionMultiselectField, WdsButtonComponent],
template: `
<form class="flex w-lg max-w-full flex-col gap-4" [formRoot]="bookingForm">
<wds-field [invalid]="showErrors()">
<wds-field-label>Services</wds-field-label>
<wds-accordion-multiselect show-checkbox [expanded]="['nails']" [formField]="bookingForm.services">
@for (category of categories; track category.id) {
<wds-accordion-item [value]="category.id" [heading]="category.name">
@for (service of category.services; track service.id) {
<wds-accordion-option [value]="service.id">
<div class="flex justify-between gap-4">
<span>{{ service.name }}</span>
<span class="text-muted-foreground">{{ service.price }}</span>
</div>
</wds-accordion-option>
}
</wds-accordion-item>
}
</wds-accordion-multiselect>
<wds-field-error [errors]="showErrors() ? bookingForm.services().errors() : []" />
</wds-field>
<wds-button type="submit" class="self-start">Book</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class AccordionMultiselectSignalFormsDemo {
protected readonly categories = CATEGORIES;
protected readonly booking = signal<{ services: string[] }>({ services: [] });
protected readonly submitted = signal('—');
protected readonly bookingForm = form(
this.booking,
(path) => {
// `required()` treats an empty array as a value, so require at least one item with `minLength()`.
minLength(path.services, 1, { message: 'Select at least one service.' });
},
{
submission: {
action: async (form) => {
this.submitted.set(JSON.stringify(form().value()));
},
},
}
);
/** Errors appear after the user leaves the control or submits the form. */
protected readonly showErrors = computed(() => this.bookingForm.services().touched() && this.bookingForm.services().invalid());
}Template-driven#
WdsAccordionMultiselectValueAccessor makes ngModel work on the control; required needs at least one selected option.
import { Component, signal } from '@angular/core';
import { FormsModule, type NgForm } from '@angular/forms';
import { WDS_ACCORDION_MULTISELECT } from '@wds/angular/accordion-multiselect';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsAccordionMultiselectValueAccessor } from '@wds/angular/forms';
const CATEGORIES = [
{
id: 'nails',
name: 'Nails',
services: [
{ id: 'classic', name: 'Classic manicure', price: '$30' },
{ id: 'gel', name: 'Gel manicure', price: '$45' },
{ id: 'pedicure', name: 'Spa pedicure', price: '$60' },
],
},
{
id: 'face',
name: 'Facial treatments',
services: [
{ id: 'cleansing', name: 'Hydrogen cleansing', price: '$70' },
{ id: 'peel', name: 'Chemical peel', price: '$80' },
],
},
];
@Component({
selector: 'app-accordion-multiselect-template-driven-demo',
imports: [...WDS_ACCORDION_MULTISELECT, ...WDS_FIELD, FormsModule, WdsAccordionMultiselectValueAccessor, WdsButtonComponent],
template: `
<form #bookingForm="ngForm" class="flex w-lg max-w-full flex-col gap-4" (ngSubmit)="save(bookingForm)">
<wds-field [invalid]="!!services.invalid && !!services.touched">
<wds-field-label>Services</wds-field-label>
<!-- For an array, required means at least one selected option. -->
<wds-accordion-multiselect #services="ngModel" name="services" show-checkbox [expanded]="['nails']" [(ngModel)]="booking.services" required>
@for (category of categories; track category.id) {
<wds-accordion-item [value]="category.id" [heading]="category.name">
@for (service of category.services; track service.id) {
<wds-accordion-option [value]="service.id">
<div class="flex justify-between gap-4">
<span>{{ service.name }}</span>
<span class="text-muted-foreground">{{ service.price }}</span>
</div>
</wds-accordion-option>
}
</wds-accordion-item>
}
</wds-accordion-multiselect>
@if (services.invalid && services.touched) {
<wds-field-error>Select at least one service.</wds-field-error>
}
</wds-field>
<wds-button type="submit" class="self-start">Book</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class AccordionMultiselectTemplateDrivenDemo {
protected readonly categories = CATEGORIES;
protected readonly booking = { services: [] as string[] };
protected readonly submitted = signal('—');
protected save(form: NgForm): void {
if (form.invalid) {
form.form.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(form.value));
}
}Reactive forms#
WdsAccordionMultiselectValueAccessor works with formControl and formControlName; Validators.required fails for an empty array.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { WDS_ACCORDION_MULTISELECT } from '@wds/angular/accordion-multiselect';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsAccordionMultiselectValueAccessor } from '@wds/angular/forms';
const CATEGORIES = [
{
id: 'nails',
name: 'Nails',
services: [
{ id: 'classic', name: 'Classic manicure', price: '$30' },
{ id: 'gel', name: 'Gel manicure', price: '$45' },
{ id: 'pedicure', name: 'Spa pedicure', price: '$60' },
],
},
{
id: 'face',
name: 'Facial treatments',
services: [
{ id: 'cleansing', name: 'Hydrogen cleansing', price: '$70' },
{ id: 'peel', name: 'Chemical peel', price: '$80' },
],
},
];
@Component({
selector: 'app-accordion-multiselect-reactive-forms-demo',
imports: [...WDS_ACCORDION_MULTISELECT, ...WDS_FIELD, ReactiveFormsModule, WdsAccordionMultiselectValueAccessor, WdsButtonComponent],
template: `
<form class="flex w-lg max-w-full flex-col gap-4" [formGroup]="bookingForm" (ngSubmit)="save()">
<wds-field [invalid]="services.invalid && services.touched">
<wds-field-label>Services</wds-field-label>
<wds-accordion-multiselect show-checkbox [expanded]="['nails']" formControlName="services">
@for (category of categories; track category.id) {
<wds-accordion-item [value]="category.id" [heading]="category.name">
@for (service of category.services; track service.id) {
<wds-accordion-option [value]="service.id">
<div class="flex justify-between gap-4">
<span>{{ service.name }}</span>
<span class="text-muted-foreground">{{ service.price }}</span>
</div>
</wds-accordion-option>
}
</wds-accordion-item>
}
</wds-accordion-multiselect>
@if (services.invalid && services.touched) {
<wds-field-error>Select at least one service.</wds-field-error>
}
</wds-field>
<wds-button type="submit" class="self-start">Book</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class AccordionMultiselectReactiveFormsDemo {
protected readonly categories = CATEGORIES;
protected readonly bookingForm = new FormGroup({
// For an array, Validators.required means at least one selected option.
services: new FormControl<string[]>([], { nonNullable: true, validators: [Validators.required] }),
});
protected readonly services = this.bookingForm.controls.services;
protected readonly submitted = signal('—');
protected save(): void {
if (this.bookingForm.invalid) {
this.bookingForm.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(this.bookingForm.getRawValue()));
}
}API References#
wds-accordion-multiselect#
wds-accordion-item with wds-accordion-option).
Sections expand independently, and the header of a section shows how many of its options are selected.
A form control like <select multiple>: each selected option is a separate name entry in FormData,
required needs at least one option, and a form reset restores the initial selection.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| value | string[] | value="a b". | |
| expanded | string[] | value) of the expanded sections. In HTML a space-separated list. | |
| name | string | '' | |
| required | boolean | false | |
| disabled | boolean | false | |
| invalid | boolean | false | |
| show-checkbox.showCheckbox | boolean | false | |
| hide-count.hideCount | boolean | false |
Events
| Event | Description |
|---|---|
| wds-changeCustomEvent<WdsAccordionMultiselectChangeDetail> | |
| wds-open-changeCustomEvent<WdsAccordionOpenChangeDetail> |
Methods
| Method | Description |
|---|---|
| formResetCallback() | |
| formDisabledCallback(disabled: boolean) | |
| checkValidity() | true when the value is valid; otherwise fires invalid on the element. |
| reportValidity() | checkValidity(), and shows the browser validation bubble when the value is invalid. |
| setCustomValidity(message: string) | input.setCustomValidity(). |
Slots
| Slot | Description |
|---|---|
| (default) | wds-accordion-item sections with wds-accordion-option options. |
wds-accordion-option#
wds-accordion-multiselect. The whole row is a toggle
(role="checkbox"): a click, Space or Enter changes the selection. The content is free.
The state is in data-state="checked|unchecked".
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| value | string | '' | value of the parent. |
| disabled | boolean | false |
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base | |
| indicator | show-checkbox on the parent). |
| check | |
| content |
CSS custom properties
| Property | Description |
|---|---|
| --wds-accordion-option-selected |
Accessibility#
Screen reader#
wds-accordion-multiselect has role="group": give it a name with aria-label, or wds-field-label in wds-field.
aria-expanded and aria-controls; the section content has role="region" named by the header. The count of selected options in the header is visual only.
wds-accordion-option has role="checkbox" with aria-checked, named by its content. The optional checkbox and check icon are hidden from screen readers.
invalid and disabled set aria-invalid and aria-disabled on the group, and a disabled option gets aria-disabled. In a form each selected option is submitted as a separate name entry.
Keyboard support#
| Key | Function |
|---|---|