Skip to content

Commit e5fa66b

Browse files
authored
Angular <sl-combobox> improvements (#2615)
1 parent 8ca3a20 commit e5fa66b

File tree

10 files changed

+230
-48
lines changed

10 files changed

+230
-48
lines changed

.changeset/cruel-oranges-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sl-design-system/angular': minor
3+
---
4+
5+
Add `ComboboxDirective` to forms

.changeset/tiny-pugs-cheat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sl-design-system/combobox': patch
3+
---
4+
5+
Fix Angular timing issue in `slotchange` handler

packages/angular/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
},
6161
"dependencies": {
6262
"@sl-design-system/checkbox": "^2.1.4",
63+
"@sl-design-system/combobox": "^0.1.4",
6364
"@sl-design-system/form": "^1.2.4",
6465
"@sl-design-system/icon": "^1.2.1",
6566
"@sl-design-system/locales": "^0.0.13",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ChangeDetectorRef, Directive, ElementRef, Inject, forwardRef } from '@angular/core';
2+
import { NG_VALIDATORS, NG_VALUE_ACCESSOR, type ValidationErrors } from '@angular/forms';
3+
import { type Combobox } from '@sl-design-system/combobox';
4+
import { FormControlElementDirective } from './form-control-element.directive';
5+
6+
@Directive({
7+
selector: 'sl-combobox',
8+
standalone: true,
9+
providers: [
10+
{
11+
provide: NG_VALUE_ACCESSOR,
12+
useExisting: forwardRef(() => ComboboxDirective),
13+
multi: true
14+
},
15+
{
16+
provide: NG_VALIDATORS,
17+
useExisting: forwardRef(() => ComboboxDirective),
18+
multi: true
19+
}
20+
]
21+
})
22+
export class ComboboxDirective extends FormControlElementDirective<Combobox> {
23+
constructor(
24+
@Inject(ElementRef) elementRef: ElementRef<Combobox>,
25+
@Inject(ChangeDetectorRef) changeDetection: ChangeDetectorRef
26+
) {
27+
super(elementRef, changeDetection);
28+
}
29+
30+
override validate(): ValidationErrors | null {
31+
if (this.element.valid) {
32+
return null;
33+
} else if (this.element.validity.valueMissing) {
34+
return { required: true };
35+
}
36+
37+
return null;
38+
}
39+
}

packages/angular/src/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@angular/core": "^18.0.0 || ^19.0.0 || ^20.0.0",
1515
"@angular/forms": "^18.0.0 || ^19.0.0 || ^20.0.0",
1616
"@sl-design-system/checkbox": "*",
17+
"@sl-design-system/combobox": "*",
1718
"@sl-design-system/dialog": "*",
1819
"@sl-design-system/form": "*",
1920
"@sl-design-system/icon": "*",

packages/angular/stories/forms.stories.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ import {
88
ReactiveFormsModule,
99
type ValidationErrors
1010
} from '@angular/forms';
11-
import { type Form } from '@sl-design-system/form';
11+
import { Form } from '@sl-design-system/form';
1212
import { type Meta, type StoryFn, moduleMetadata } from '@storybook/angular';
1313
import { ButtonComponent } from '../src/button/button.component';
1414
import { ButtonBarComponent } from '../src/button-bar/button-bar.component';
1515
import { CheckboxGroupComponent } from '../src/checkbox/checkbox-group.component';
1616
import { CheckboxComponent } from '../src/checkbox/checkbox.component';
17+
import { ComboboxComponent } from '../src/combobox/combobox.component';
1718
import { FormFieldComponent } from '../src/form/form-field.component';
1819
import { FormComponent } from '../src/form/form.component';
1920
import { CheckboxGroupDirective } from '../src/forms/checkbox-group.directive';
2021
import { CheckboxDirective } from '../src/forms/checkbox.directive';
22+
import { ComboboxDirective } from '../src/forms/combobox-directive';
2123
import { NumberFieldDirective } from '../src/forms/number-field.directive';
2224
import { RadioGroupDirective } from '../src/forms/radio-group.directive';
2325
import { SelectDirective } from '../src/forms/select.directive';
@@ -62,6 +64,26 @@ import { TextFieldComponent } from '../src/text-field/text-field.component';
6264
</sl-select>
6365
</sl-form-field>
6466
67+
<sl-form-field label="Combobox - single select">
68+
<sl-combobox formControlName="comboboxSingle" placeholder="Select an option">
69+
<sl-listbox>
70+
@for (option of options(); track option.value) {
71+
<sl-option>{{ option.label }}</sl-option>
72+
}
73+
</sl-listbox>
74+
</sl-combobox>
75+
</sl-form-field>
76+
77+
<sl-form-field label="Combobox - multiple select">
78+
<sl-combobox formControlName="comboboxMultiple" multiple placeholder="Select one or more options">
79+
<sl-listbox>
80+
@for (option of options(); track option.value) {
81+
<sl-option>{{ option.label }}</sl-option>
82+
}
83+
</sl-listbox>
84+
</sl-combobox>
85+
</sl-form-field>
86+
6587
<sl-form-field label="Switch">
6688
<sl-switch formControlName="switch" reverse value="toggled">Toggle me</sl-switch>
6789
</sl-form-field>
@@ -90,6 +112,7 @@ import { TextFieldComponent } from '../src/text-field/text-field.component';
90112
ReactiveFormsModule,
91113
CheckboxDirective,
92114
CheckboxGroupDirective,
115+
ComboboxDirective,
93116
NumberFieldDirective,
94117
RadioGroupDirective,
95118
SelectDirective,
@@ -102,6 +125,8 @@ export class AllFormControlsReactiveComponent {
102125
formGroup = new FormGroup({
103126
checkbox: new FormControl('checked'),
104127
checkboxGroup: new FormControl(['2', '1', '0']),
128+
comboboxSingle: new FormControl(''),
129+
comboboxMultiple: new FormControl(''),
105130
numberField: new FormControl(10),
106131
radioGroup: new FormControl('1'),
107132
select: new FormControl('1'),
@@ -151,6 +176,26 @@ export class AllFormControlsReactiveComponent {
151176
</sl-select>
152177
</sl-form-field>
153178
179+
<sl-form-field label="Combobox - single select">
180+
<sl-combobox formControlName="comboboxSingle" required>
181+
<sl-listbox>
182+
@for (option of options(); track option.value) {
183+
<sl-option>{{ option.label }}</sl-option>
184+
}
185+
</sl-listbox>
186+
</sl-combobox>
187+
</sl-form-field>
188+
189+
<sl-form-field label="Combobox - multiple select">
190+
<sl-combobox formControlName="comboboxMultiple" multiple required>
191+
<sl-listbox>
192+
@for (option of options(); track option.value) {
193+
<sl-option>{{ option.label }}</sl-option>
194+
}
195+
</sl-listbox>
196+
</sl-combobox>
197+
</sl-form-field>
198+
154199
<sl-form-field label="Switch">
155200
<sl-switch formControlName="switch" reverse>Toggle me</sl-switch>
156201
</sl-form-field>
@@ -185,6 +230,7 @@ export class AllFormControlsReactiveComponent {
185230
ButtonBarComponent,
186231
CheckboxDirective,
187232
CheckboxGroupDirective,
233+
ComboboxDirective,
188234
NumberFieldDirective,
189235
RadioGroupDirective,
190236
SelectDirective,
@@ -199,6 +245,8 @@ export class AllFormControlsEmptyReactiveComponent {
199245
formGroup = new FormGroup({
200246
checkbox: new FormControl(false),
201247
checkboxGroup: new FormControl([]),
248+
comboboxSingle: new FormControl(''),
249+
comboboxMultiple: new FormControl(''),
202250
numberField: new FormControl(),
203251
radioGroup: new FormControl(''),
204252
select: new FormControl(''),
@@ -252,6 +300,26 @@ export class AllFormControlsEmptyReactiveComponent {
252300
</sl-select>
253301
</sl-form-field>
254302
303+
<sl-form-field label="Combobox - single select">
304+
<sl-combobox [(ngModel)]="formGroup.comboboxSingle">
305+
<sl-listbox>
306+
<sl-option>Option 1</sl-option>
307+
<sl-option>Option 2</sl-option>
308+
<sl-option>Option 3</sl-option>
309+
</sl-listbox>
310+
</sl-combobox>
311+
</sl-form-field>
312+
313+
<sl-form-field label="Combobox - multiple select">
314+
<sl-combobox [(ngModel)]="formGroup.comboboxMultiple" multiple>
315+
<sl-listbox>
316+
<sl-option>Option 1</sl-option>
317+
<sl-option>Option 2</sl-option>
318+
<sl-option>Option 3</sl-option>
319+
</sl-listbox>
320+
</sl-combobox>
321+
</sl-form-field>
322+
255323
<sl-form-field label="Switch">
256324
<sl-switch [(ngModel)]="formGroup.switch" reverse value="toggled">Toggle me</sl-switch>
257325
</sl-form-field>
@@ -280,6 +348,7 @@ export class AllFormControlsEmptyReactiveComponent {
280348
FormsModule,
281349
CheckboxDirective,
282350
CheckboxGroupDirective,
351+
ComboboxDirective,
283352
NumberFieldDirective,
284353
RadioGroupDirective,
285354
SelectDirective,
@@ -293,6 +362,8 @@ export class AllFormControlsTemplateComponent {
293362
textField: 'Text field',
294363
textArea: 'Text area',
295364
checkbox: 'checked',
365+
comboboxSingle: 'Option 1',
366+
comboboxMultiple: ['Option 1', 'Option 2'],
296367
numberField: 10,
297368
select: '1',
298369
switch: 'toggled',
@@ -329,6 +400,26 @@ export class AllFormControlsTemplateComponent {
329400
</sl-select>
330401
</sl-form-field>
331402
403+
<sl-form-field label="Combobox - single select">
404+
<sl-combobox [(ngModel)]="formGroup.comboboxSingle" required>
405+
<sl-listbox>
406+
<sl-option>Option 1</sl-option>
407+
<sl-option>Option 2</sl-option>
408+
<sl-option>Option 3</sl-option>
409+
</sl-listbox>
410+
</sl-combobox>
411+
</sl-form-field>
412+
413+
<sl-form-field label="Combobox - multiple select">
414+
<sl-combobox [(ngModel)]="formGroup.comboboxMultiple" multiple required>
415+
<sl-listbox>
416+
<sl-option>Option 1</sl-option>
417+
<sl-option>Option 2</sl-option>
418+
<sl-option>Option 3</sl-option>
419+
</sl-listbox>
420+
</sl-combobox>
421+
</sl-form-field>
422+
332423
<sl-form-field label="Switch">
333424
<sl-switch [(ngModel)]="formGroup.switch" reverse>Toggle me</sl-switch>
334425
</sl-form-field>
@@ -363,6 +454,7 @@ export class AllFormControlsTemplateComponent {
363454
ButtonBarComponent,
364455
CheckboxDirective,
365456
CheckboxGroupDirective,
457+
ComboboxDirective,
366458
NumberFieldDirective,
367459
RadioGroupDirective,
368460
SelectDirective,
@@ -378,6 +470,8 @@ export class AllFormControlsEmptyTemplateComponent {
378470
textField: '',
379471
textArea: '',
380472
checkbox: false,
473+
comboboxSingle: '',
474+
comboboxMultiple: [],
381475
numberField: '',
382476
select: '',
383477
switch: false,
@@ -484,6 +578,7 @@ export default {
484578
LoginFormComponent,
485579
CheckboxComponent,
486580
CheckboxGroupComponent,
581+
ComboboxComponent,
487582
FormComponent,
488583
FormFieldComponent,
489584
NumberFieldComponent,

packages/angular/stories/wrappers.stories.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ButtonBarComponent } from '../src/button-bar/button-bar.component';
1313
import { CardComponent } from '../src/card/card.component';
1414
import { CheckboxGroupComponent } from '../src/checkbox/checkbox-group.component';
1515
import { CheckboxComponent } from '../src/checkbox/checkbox.component';
16+
import { ComboboxComponent } from '../src/combobox/combobox.component';
1617
import { DialogComponent } from '../src/dialog/dialog.component';
1718
import { IconComponent } from '../src/icon/icon.component';
1819
import { InlineMessageComponent } from '../src/inline-message/inline-message.component';
@@ -48,6 +49,7 @@ export default {
4849
CardComponent,
4950
CheckboxComponent,
5051
CheckboxGroupComponent,
52+
ComboboxComponent,
5153
DialogComponent,
5254
IconComponent,
5355
InlineMessageComponent,
@@ -151,6 +153,20 @@ export const Checkbox: StoryObj = {
151153
})
152154
};
153155

156+
export const Combobox: StoryObj = {
157+
render: () => ({
158+
template: `
159+
<sl-combobox placeholder="Select an option">
160+
<sl-listbox>
161+
<sl-option>Option 1</sl-option>
162+
<sl-option>Option 2</sl-option>
163+
<sl-option>Option 3</sl-option>
164+
</sl-listbox>
165+
</sl-combobox>
166+
`
167+
})
168+
};
169+
154170
export const Dialog: StoryObj = {
155171
render: () => {
156172
const onClick = (event: Event & { target: HTMLElement }) => {

0 commit comments

Comments
 (0)