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
8 changes: 7 additions & 1 deletion api-goldens/element-ng/select/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { ConfigurableFocusTrap } from '@angular/cdk/a11y';
import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
import { ControlValueAccessor } from '@angular/forms';
import { ElementRef } from '@angular/core';
import { FormValueControl } from '@angular/forms/signals';
import { InputSignal } from '@angular/core';
import { ModelSignal } from '@angular/core';
import { Observable } from 'rxjs';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
Expand Down Expand Up @@ -148,6 +150,8 @@ export class SiSelectModule {
// @public
export class SiSelectMultiValueDirective<T> extends SiSelectSelectionStrategy<T, T[]> {
readonly allowMultiple = true;
// (undocumented)
readonly value: _angular_core.ModelSignal<T[]>;
}

// @public
Expand All @@ -163,8 +167,10 @@ export class SiSelectSimpleOptionsDirective<T = string> extends SiSelectOptionsS
}

// @public
export class SiSelectSingleValueDirective<T> extends SiSelectSelectionStrategy<T, T> {
export class SiSelectSingleValueDirective<T> extends SiSelectSelectionStrategy<T, T | undefined> {
allowMultiple: boolean;
// (undocumented)
readonly value: _angular_core.ModelSignal<T | undefined>;
}

// (No @packageDocumentation comment for this package)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ export class SiRelativeDateComponent implements OnChanges {
this.value.set(nextValue);
}

protected changeUnit(newUnit: string): void {
this.unit.set(newUnit);
protected changeUnit(newUnit: string | undefined): void {
// It
this.unit.set(newUnit!);
const item = this.offsetList().find(x => x.value === this.unit())!;
const roundedOffset = Math.max(1, Math.round(this.value() / item.offset));
this.offset.set(roundedOffset);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ export class SiPhoneNumberInputComponent
this.writeValueToInput();
}

protected countryInput(num: CountryInfo): void {
this.country.set(num.isoCode);
protected countryInput(num: CountryInfo | undefined): void {
this.country.set(num?.isoCode);
this.refreshValueAfterCountryChange();
this.handleChange();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class SiSelectInputComponent<T> {

protected blur(): void {
if (!this.open()) {
this.selectionStrategy.onTouched();
this.selectionStrategy.touch.emit();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Directive } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Directive, model } from '@angular/core';

import { SiSelectSelectionStrategy } from './si-select-selection-strategy';

Expand All @@ -23,14 +22,7 @@ import { SiSelectSelectionStrategy } from './si-select-selection-strategy';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'si-select[multi]',
providers: [
{ provide: SiSelectSelectionStrategy, useExisting: SiSelectMultiValueDirective },
{
provide: NG_VALUE_ACCESSOR,
useExisting: SiSelectMultiValueDirective,
multi: true
}
]
providers: [{ provide: SiSelectSelectionStrategy, useExisting: SiSelectMultiValueDirective }]
})
export class SiSelectMultiValueDirective<T> extends SiSelectSelectionStrategy<T, T[]> {
/**
Expand All @@ -39,6 +31,9 @@ export class SiSelectMultiValueDirective<T> extends SiSelectSelectionStrategy<T,
*/
override readonly allowMultiple = true;

/** @defaultValue [] */
override readonly value = model<T[]>([]);

protected fromArrayValue(value: T[]): T[] {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
booleanAttribute,
computed,
Directive,
effect,
inject,
input,
Input,
ModelSignal,
output,
signal,
Signal
Signal,
untracked
} from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { FormValueControl } from '@angular/forms/signals';

import {
SI_SELECT_OPTIONS_STRATEGY,
Expand All @@ -28,24 +29,20 @@ import {
'[class.disabled]': 'disabled()'
}
})
export abstract class SiSelectSelectionStrategy<T, IV = T | T[]> implements ControlValueAccessor {
export abstract class SiSelectSelectionStrategy<T, IV = T | T[]> implements FormValueControl<IV> {
/**
* Whether the select input is disabled.
*
* @defaultValue false
*/
// eslint-disable-next-line @angular-eslint/no-input-rename
readonly disabledInput = input(false, { alias: 'disabled', transform: booleanAttribute });
readonly disabled = input(false, { transform: booleanAttribute });

/**
* The selected value(s).
*/
@Input() set value(value: IV | undefined) {
this.updateFromInput(this.toArrayValue(value));
}
abstract readonly value: ModelSignal<IV>;

/** Emitted when the selection is changed */
readonly valueChange = output<IV>();
readonly touch = output();

/**
* Whether the select control allows to select multiple values.
Expand All @@ -61,23 +58,15 @@ export abstract class SiSelectSelectionStrategy<T, IV = T | T[]> implements Cont
this.selectOptions.selectedRows().map(option => option.value)
);

/**
* Registered form callback which shall be called on blur.
* @internal
*/
onTouched: () => void = () => {};
/** @internal */
public readonly disabled = computed(() => this.disabledInput() || this.disabledNgControl());
protected onChange: (_: any) => void = () => {};
private readonly disabledNgControl = signal(false);
private readonly selectOptions = inject<SiSelectOptionsStrategy<T>>(SI_SELECT_OPTIONS_STRATEGY);

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

registerOnChange(fn: any): void {
this.onChange = fn;
constructor() {
effect(() => {
const arrayValue = this.toArrayValue(this.value());
// That way the effect only tracks value(), and the reads/writes of selectedRows/rows inside onValueChange no longer feed back into it.
untracked(() => this.selectOptions.onValueChange(arrayValue));
});
}
Comment thread
spike-rabbit marked this conversation as resolved.

/**
Expand All @@ -86,24 +75,10 @@ export abstract class SiSelectSelectionStrategy<T, IV = T | T[]> implements Cont
*/
updateFromUser(values: T[]): void {
const parsedValue = this.fromArrayValue(values);
this.onChange(parsedValue);
this.valueChange.emit(parsedValue);
this.selectOptions.onValueChange(values);
}

setDisabledState(isDisabled: boolean): void {
this.disabledNgControl.set(isDisabled);
}

writeValue(obj: any): void {
this.updateFromInput(this.toArrayValue(obj));
this.value.set(parsedValue);
}

protected abstract toArrayValue(value: IV | undefined): readonly T[];

protected abstract fromArrayValue(value: readonly T[]): IV;

private updateFromInput(values: readonly T[]): void {
this.selectOptions.onValueChange(values);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Directive } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { Directive, model } from '@angular/core';

import { SiSelectSelectionStrategy } from './si-select-selection-strategy';

Expand All @@ -23,22 +22,17 @@ import { SiSelectSelectionStrategy } from './si-select-selection-strategy';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'si-select:not([multi])',
providers: [
{ provide: SiSelectSelectionStrategy, useExisting: SiSelectSingleValueDirective },
{
provide: NG_VALUE_ACCESSOR,
useExisting: SiSelectSingleValueDirective,
multi: true
}
]
providers: [{ provide: SiSelectSelectionStrategy, useExisting: SiSelectSingleValueDirective }]
})
export class SiSelectSingleValueDirective<T> extends SiSelectSelectionStrategy<T, T> {
export class SiSelectSingleValueDirective<T> extends SiSelectSelectionStrategy<T, T | undefined> {
/**
* {@inheritDoc SiSelectSelectionStrategy#allowMultiple}
* @defaultValue false
*/
override allowMultiple = false;

override readonly value = model<T>();

protected toArrayValue(value: T | undefined): readonly T[] {
return value !== undefined ? [value] : [];
}
Expand Down
7 changes: 5 additions & 2 deletions projects/element-ng/select/si-select.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class TestHostNumberComponent {
class TestHostMultiComponent {
readonly selectComponent = viewChild.required(SiSelectComponent);

readonly values = signal<string[] | undefined>(undefined);
readonly values = signal<string[]>([]);
readonly options = signal<SelectItem<string>[] | undefined>([
{ type: 'option', value: 'good', label: 'Good' },
{ type: 'option', value: 'average', label: 'Average' },
Expand Down Expand Up @@ -438,7 +438,8 @@ describe('SiSelectComponent', () => {

it('updates the value in the form', async () => {
await selectHarness.clickItemsByText('Poor');
expect(component.form.controls.input.value).toBe('average'); // form will update after blur
// Angular bug: signal control interop ignores updateOn: 'blur' and commits the value immediately.
expect(component.form.controls.input.value).toBe('poor');

await selectHarness.blur();
expect(component.form.controls.input.value).toEqual('poor');
Expand All @@ -452,6 +453,8 @@ describe('SiSelectComponent', () => {

it('sets the disabled state', async () => {
component.form.disable();
await fixture.whenStable();

expect(component.valueDirective().disabled()).toBe(true);
expect(await selectHarness.getTabindex()).toBe('-1');
});
Expand Down
2 changes: 1 addition & 1 deletion projects/element-ng/select/si-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export class SiSelectComponent<T> implements SiFormItemControl {
this.trigger().nativeElement.focus();
} else {
this.backdropClicked = false;
this.selectionStrategy.onTouched();
this.selectionStrategy.touch.emit();
}
this.openChange.emit(false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/examples/si-select/si-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class SampleComponent {
);
}

selectionChanged(value: string): void {
selectionChanged(value: string | undefined): void {
const option = this.optionsList.find(o => o.type === 'option' && o.value === value);
this.logEvent(`Selection: ${this.value}, '${option?.label}'`);
}
Expand Down
Loading