diff --git a/apps/app/src/app/pages/(home)/components/three-hundred.component.ts b/apps/app/src/app/pages/(home)/components/three-hundred.component.ts
index 3efed8d7..a37b0fdf 100644
--- a/apps/app/src/app/pages/(home)/components/three-hundred.component.ts
+++ b/apps/app/src/app/pages/(home)/components/three-hundred.component.ts
@@ -10,10 +10,10 @@ import { ThreeHundredItemComponent } from './th-item.component';
class: 'grid gap-2 grid-cols-5 md:grid-cols-10',
},
template: `
- @for (contributor of _contributors; track contributor) {
+ @for (contributor of _contributors; track $index) {
{{ contributor }}
}
- @for (item of _rest; track item) {
+ @for (item of _rest; track $index) {
}
`,
diff --git a/libs/ui/select/brain/src/lib/brn-select-option.directive.ts b/libs/ui/select/brain/src/lib/brn-select-option.directive.ts
index 282d9d89..6468e3e7 100644
--- a/libs/ui/select/brain/src/lib/brn-select-option.directive.ts
+++ b/libs/ui/select/brain/src/lib/brn-select-option.directive.ts
@@ -31,7 +31,6 @@ export class BrnSelectOptionDirective implements FocusableOption, OnDestroy {
this._selectService.registerOption(this._cdkSelectOption);
toObservable(this._selectService.value)
- .pipe(takeUntilDestroyed())
.subscribe((selectedValues: string | string[]) => {
if (Array.isArray(selectedValues)) {
const itemFound = (selectedValues as Array).find((val) => val === this._cdkSelectOption.value);
diff --git a/libs/ui/select/brain/src/lib/brn-select.component.ts b/libs/ui/select/brain/src/lib/brn-select.component.ts
index 4dfb6c91..4795a3b9 100644
--- a/libs/ui/select/brain/src/lib/brn-select.component.ts
+++ b/libs/ui/select/brain/src/lib/brn-select.component.ts
@@ -198,11 +198,15 @@ export class BrnSelectComponent implements ControlValueAccessor, AfterContentIni
this.ngControl.valueAccessor = this;
}
- // Watch for Listbox Selection Changes to trigger Collapse
+ // Watch for Listbox Selection Changes to trigger Collapse and Value Change
this._selectService.listBoxValueChangeEvent$.pipe(takeUntilDestroyed()).subscribe(() => {
if (!this._multiple()) {
this.close();
}
+
+ // we set shouldEmitValueChange to true because we want to propagate the value change
+ // as a result of user interaction
+ this._shouldEmitValueChange.set(true);
});
/**
@@ -211,24 +215,16 @@ export class BrnSelectComponent implements ControlValueAccessor, AfterContentIni
* we dont propagate changes made from outside the component (ex. patch value or initial value from form control)
*/
toObservable(this._selectService.value)
- .pipe(
- filter(() => {
- const shouldEmitValueChange = this._shouldEmitValueChange();
- this._shouldEmitValueChange.set(true);
- return shouldEmitValueChange;
- }),
- takeUntilDestroyed(),
- )
- .subscribe((value) => this._onChange(value ?? null));
-
- toObservable(this.dir)
- .pipe(takeUntilDestroyed())
- .subscribe(() =>
- this._selectService.state.update((state) => ({
- ...state,
- dir: this.dir(),
- })),
- );
+ .subscribe((value) => {
+ if (this._shouldEmitValueChange()) {
+ this._onChange(value ?? null)
+ }
+ this._shouldEmitValueChange.set(true);
+ });
+
+ toObservable(this.dir).subscribe((dir) =>
+ this._selectService.state.update((state) => ({ ...state, dir })),
+ );
}
public ngAfterContentInit(): void {
@@ -291,7 +287,7 @@ export class BrnSelectComponent implements ControlValueAccessor, AfterContentIni
}
public writeValue(value: any): void {
- // 'shouldEmitValueChange' ensures we don't propagate changes when we recieve value from from form control
+ // 'shouldEmitValueChange' ensures we don't propagate changes when we receive value from form control
// set to false until next value change and then reset back to true
this._shouldEmitValueChange.set(false);
this._selectService.setInitialSelectedOptions(value);
diff --git a/libs/ui/select/brain/src/lib/tests/select-reactive-form.ts b/libs/ui/select/brain/src/lib/tests/select-reactive-form.ts
index a0c56f57..64610c99 100644
--- a/libs/ui/select/brain/src/lib/tests/select-reactive-form.ts
+++ b/libs/ui/select/brain/src/lib/tests/select-reactive-form.ts
@@ -96,6 +96,46 @@ export class SelectSingleValueWithInitialValueTestComponent {
form = new FormGroup({ fruit: new FormControl('apple') });
}
+@Component({
+ standalone: true,
+ imports: [CommonModule, FormsModule, ReactiveFormsModule, BrnSelectImports],
+ // eslint-disable-next-line @angular-eslint/component-selector
+ selector: 'select-reactive-field-fixture',
+ template: `
+
+ `,
+})
+export class SelectSingleValueWithInitialValueWithAsyncUpdateTestComponent {
+ form = new FormGroup({ fruit: new FormControl('apple') });
+
+ constructor() {
+ // queueMicrotask(() => {
+ // this.form.patchValue({ fruit: 'apple' });
+ // });
+ setTimeout(() => {
+ this.form.patchValue({ fruit: 'apple' });
+ });
+ }
+}
+
@Component({
standalone: true,
imports: [CommonModule, FormsModule, ReactiveFormsModule, BrnSelectImports],
diff --git a/libs/ui/select/brain/src/lib/tests/select-single-mode.spec.ts b/libs/ui/select/brain/src/lib/tests/select-single-mode.spec.ts
index f765a7ff..1078b9b8 100644
--- a/libs/ui/select/brain/src/lib/tests/select-single-mode.spec.ts
+++ b/libs/ui/select/brain/src/lib/tests/select-single-mode.spec.ts
@@ -1,7 +1,7 @@
import { Validators } from '@angular/forms';
import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
-import { SelectSingleValueTestComponent, SelectSingleValueWithInitialValueTestComponent } from './select-reactive-form';
+import { SelectSingleValueTestComponent, SelectSingleValueWithInitialValueTestComponent, SelectSingleValueWithInitialValueWithAsyncUpdateTestComponent } from './select-reactive-form';
import { getFormControlStatus, getFormValidationClasses } from './utils';
describe('Brn Select Component in single-mode', () => {
@@ -29,6 +29,17 @@ describe('Brn Select Component in single-mode', () => {
};
};
+
+ const setupWithFormValidationAndInitialValueAndAsyncUpdate = async () => {
+ const { fixture } = await render(SelectSingleValueWithInitialValueWithAsyncUpdateTestComponent);
+ return {
+ user: userEvent.setup(),
+ fixture,
+ trigger: screen.getByTestId('brn-select-trigger'),
+ value: screen.getByTestId('brn-select-value'),
+ };
+ };
+
describe('form validation - single mode', () => {
it('should reflect correct formcontrol status and value with no initial value', async () => {
const { fixture, trigger, value } = await setupWithFormValidation();
@@ -174,6 +185,61 @@ describe('Brn Select Component in single-mode', () => {
expect(getFormValidationClasses(trigger)).toStrictEqual(afterSelectionExpected);
expect(cmpInstance.form?.get('fruit')?.value).toEqual('banana');
+ expect(screen.getByTestId('brn-select-value').textContent?.trim()).toBe('Banana');
+ });
+
+ it('should reflect correct formcontrol status after first user selection with initial value and async update', async () => {
+ const { user, trigger, fixture, value } = await setupWithFormValidationAndInitialValueAndAsyncUpdate();
+ const cmpInstance = fixture.componentInstance as SelectSingleValueWithInitialValueTestComponent;
+
+ expect(value.textContent?.trim()).toBe(INITIAL_VALUE_TEXT);
+ expect(cmpInstance.form?.get('fruit')?.value).toEqual(INITIAL_VALUE);
+
+ const expected = {
+ untouched: true,
+ touched: false,
+ valid: true,
+ invalid: false,
+ pristine: true,
+ dirty: false,
+ };
+
+ expect(getFormControlStatus(cmpInstance.form?.get('fruit'))).toStrictEqual(expected);
+ expect(getFormValidationClasses(trigger)).toStrictEqual(expected);
+
+ // Open Select
+ await user.click(trigger);
+
+ const afterOpenExpected = {
+ untouched: true,
+ touched: false,
+ valid: true,
+ invalid: false,
+ pristine: true,
+ dirty: false,
+ };
+
+ expect(getFormControlStatus(cmpInstance.form?.get('fruit'))).toStrictEqual(afterOpenExpected);
+ expect(getFormValidationClasses(trigger)).toStrictEqual(afterOpenExpected);
+
+ // Select option
+ const options = await screen.getAllByRole('option');
+ await user.click(options[1]);
+
+ const afterSelectionExpected = {
+ untouched: false,
+ touched: true,
+ valid: true,
+ invalid: false,
+ pristine: false,
+ dirty: true,
+ };
+
+ expect(getFormControlStatus(cmpInstance.form?.get('fruit'))).toStrictEqual(afterSelectionExpected);
+ expect(getFormValidationClasses(trigger)).toStrictEqual(afterSelectionExpected);
+
+ expect(cmpInstance.form?.get('fruit')?.value).toEqual('banana');
+ expect(value.textContent?.trim()).toBe('Banana');
});
});