diff --git a/libs/angular-components/src/lib/components/base.component.ts b/libs/angular-components/src/lib/components/base.component.ts index 74b957aac..74768456f 100644 --- a/libs/angular-components/src/lib/components/base.component.ts +++ b/libs/angular-components/src/lib/components/base.component.ts @@ -1,6 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Spacing } from "@abgov/ui-components-common"; -import { booleanAttribute, Component, Input } from "@angular/core"; +import { + booleanAttribute, + Component, + Input, + ElementRef, + ViewChild, + Renderer2, +} from "@angular/core"; import { ControlValueAccessor } from "@angular/forms"; @Component({ @@ -28,9 +35,25 @@ export abstract class GoabBaseComponent { * - Supports `disabled="true"` and `error="true` attribute bindings for convenience. * - Handles form control value changes and touch events via `ControlValueAccessor` methods. * - Allows for flexible value types (`unknown`), making it suitable for various data types like integers, dates, or booleans. + * - Uses ViewChild to capture a reference to the native GOA web component element via `#goaComponentRef`. + * - Uses Renderer2 for safe DOM manipulation (compatible with SSR and security best practices). * * ## Usage - * Extend this class to create custom form controls. Implement additional functionality as needed for your specific use case. + * Extend this class to create custom form controls. Child components must: + * 1. Add `#goaComponentRef` template reference to their `goa-*` element in the template + * 2. Inject `Renderer2` in their constructor and pass it to `super(renderer)` + * + * ### Example: + * ```typescript + * @Component({ + * template: `` + * }) + * export class GoabInput extends GoabControlValueAccessor { + * constructor(private cdr: ChangeDetectorRef, renderer: Renderer2) { + * super(renderer); // Required: pass Renderer2 to base class + * } + * } + * ``` * * ## Properties * - `id?`: An optional identifier for the component. @@ -40,10 +63,11 @@ export abstract class GoabBaseComponent { * * ## Methods * - `markAsTouched()`: Marks the component as touched and triggers the `fcTouched` callback if defined. - * - `writeValue(value: unknown)`: Writes a new value to the form control. + * - `writeValue(value: unknown)`: Writes a new value to the form control (can be overridden for special behavior like checkbox). * - `registerOnChange(fn: any)`: Registers a function to handle changes in the form control value. * - `registerOnTouched(fn: any)`: Registers a function to handle touch events on the form control. * - `setDisabledState?(isDisabled: boolean)`: Sets the disabled state of the component. + * - `convertValueToString(value: unknown)`: Converts a value to a string for DOM attribute assignment (can be overridden). * * ## Callbacks * - `fcChange?`: A function to handle changes in the form control value. @@ -87,12 +111,42 @@ export abstract class GoabControlValueAccessor } } + /** + * Reference to the native GOA web component element. + * Child templates should declare `#goaComponentRef` on the `goa-*` element. + * The base class captures it here so children don't need their own ViewChild. + */ + @ViewChild("goaComponentRef", { static: false, read: ElementRef }) + protected goaComponentRef?: ElementRef; + + constructor(protected renderer: Renderer2) { + super(); + } + + /** + * Convert an arbitrary value into a string for DOM attribute assignment. + * Child classes can override when they need special formatting. + * @param value The value to convert + * @returns string representation or empty string for nullish/empty + */ + protected convertValueToString(value: unknown): string { + if (value === null || value === undefined || value === "") { + return ""; + } + return String(value); + } + /** * Writes a new value to the form control. * @param {unknown} value - The value to write. */ public writeValue(value: unknown): void { this.value = value; + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + const stringValue = this.convertValueToString(value); + this.renderer.setAttribute(el, "value", stringValue); + } } /** diff --git a/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts b/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts index db052a4db..c716ae827 100644 --- a/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts +++ b/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts @@ -8,6 +8,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -51,8 +52,11 @@ export class GoabCheckboxList extends GoabControlValueAccessor implements OnInit // Override value to handle string arrays consistently @Input() override value?: string[]; - constructor(private cdr: ChangeDetectorRef) { - super(); + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); } ngOnInit(): void { diff --git a/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts b/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts index fc7d6a82e..35e21f225 100644 --- a/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts +++ b/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts @@ -93,7 +93,7 @@ describe("GoabCheckbox", () => { expect(checkboxElement.getAttribute("maxwidth")).toBe("480px"); }); - it("should handle onChange event", fakeAsync(() => { + it("should handle onChange event", async () => { const onChange = jest.spyOn(component, "onChange"); const checkboxElement = fixture.debugElement.query( @@ -108,7 +108,50 @@ describe("GoabCheckbox", () => { ); expect(onChange).toHaveBeenCalled(); - })); + }); + + describe("writeValue", () => { + it("should set checked attribute to true when value is truthy", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + const checkboxElement = fixture.debugElement.query(By.css("goa-checkbox")).nativeElement; + + checkboxComponent.writeValue(true); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue("some value"); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue(1); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + }); + + it("should set checked attribute to false when value is falsy", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + const checkboxElement = fixture.debugElement.query(By.css("goa-checkbox")).nativeElement; + + checkboxComponent.writeValue(false); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(null); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(undefined); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(""); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + }); + + it("should update component value property", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + + checkboxComponent.writeValue(true); + expect(checkboxComponent.value).toBe(true); + + checkboxComponent.writeValue(null); + expect(checkboxComponent.value).toBe(null); + }); + }); }); @Component({ @@ -136,7 +179,7 @@ describe("Checkbox with description slot", () => { it("should render with slot description", fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabCheckbox, ReactiveFormsModule, TestCheckboxWithDescriptionSlotComponent], + imports: [TestCheckboxWithDescriptionSlotComponent, GoabCheckbox, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -155,7 +198,7 @@ describe("Checkbox with description slot", () => { @Component({ standalone: true, - imports: [GoabCheckbox, ReactiveFormsModule], + imports: [GoabCheckbox], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabCheckbox, ReactiveFormsModule, TestCheckboxWithRevealSlotComponent], + imports: [TestCheckboxWithRevealSlotComponent, GoabCheckbox, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); diff --git a/libs/angular-components/src/lib/components/checkbox/checkbox.ts b/libs/angular-components/src/lib/components/checkbox/checkbox.ts index 47dcd083f..4dc3ba957 100644 --- a/libs/angular-components/src/lib/components/checkbox/checkbox.ts +++ b/libs/angular-components/src/lib/components/checkbox/checkbox.ts @@ -10,6 +10,7 @@ import { booleanAttribute, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { NgTemplateOutlet, CommonModule } from "@angular/common"; @@ -19,34 +20,35 @@ import { GoabControlValueAccessor } from "../base.component"; standalone: true, selector: "goab-checkbox", template: ` - -
- -
-
- -
-
`, + #goaComponentRef + *ngIf="isReady" + [attr.name]="name" + [checked]="checked" + [disabled]="disabled" + [attr.indeterminate]="indeterminate ? 'true' : undefined" + [attr.error]="error" + [attr.text]="text" + [value]="value" + [attr.testid]="testId" + [attr.arialabel]="ariaLabel" + [attr.description]="getDescriptionAsString()" + [attr.revealarialabel]="revealArialLabel" + [id]="id" + [attr.maxwidth]="maxWidth" + [attr.mt]="mt" + [attr.mb]="mb" + [attr.ml]="ml" + [attr.mr]="mr" + (_change)="_onChange($event)" + > + +
+ +
+
+ +
+ `, schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ { @@ -60,8 +62,11 @@ import { GoabControlValueAccessor } from "../base.component"; export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { isReady = false; - constructor(private cdr: ChangeDetectorRef) { - super(); + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); } ngOnInit(): void { @@ -78,7 +83,7 @@ export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { @Input({ transform: booleanAttribute }) indeterminate?: boolean; @Input() text?: string; // ** NOTE: can we just use the base component for this? - @Input() override value?: string | number | boolean; + @Input() override value?: string | number | boolean | null; @Input() ariaLabel?: string; @Input() description!: string | TemplateRef; @Input() reveal?: TemplateRef; @@ -104,4 +109,15 @@ export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { this.markAsTouched(); this.fcChange?.(detail.binding === "check" ? detail.checked : detail.value || ""); } + + // Checkbox is a special case: it uses `checked` instead of `value`. + override writeValue(value: string | number | boolean | null): void { + this.value = value; + this.checked = !!value; + + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + this.renderer.setAttribute(el, "checked", this.checked ? "true" : "false"); + } + } } diff --git a/libs/angular-components/src/lib/components/date-picker/date-picker.ts b/libs/angular-components/src/lib/components/date-picker/date-picker.ts index dc9872faf..1e315a65e 100644 --- a/libs/angular-components/src/lib/components/date-picker/date-picker.ts +++ b/libs/angular-components/src/lib/components/date-picker/date-picker.ts @@ -1,4 +1,7 @@ -import { GoabDatePickerInputType, GoabDatePickerOnChangeDetail } from "@abgov/ui-components-common"; +import { + GoabDatePickerInputType, + GoabDatePickerOnChangeDetail, +} from "@abgov/ui-components-common"; import { CUSTOM_ELEMENTS_SCHEMA, Component, @@ -10,6 +13,7 @@ import { HostListener, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -20,6 +24,7 @@ import { GoabControlValueAccessor } from "../base.component"; selector: "goab-date-picker", imports: [CommonModule], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabDropdown, GoabDropdownItem, ReactiveFormsModule, TestDropdownComponent], + imports: [TestDropdownComponent, GoabDropdown, GoabDropdownItem, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -140,6 +141,8 @@ describe("GoABDropdown", () => { const onChangeMock = jest.spyOn(component, "onChange"); component.native = true; fixture.detectChanges(); + tick(); + fixture.detectChanges(); const el = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; expect(el).toBeTruthy(); @@ -152,4 +155,79 @@ describe("GoABDropdown", () => { ); expect(onChangeMock).toHaveBeenCalled(); })); -}); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called with a value", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + dropdownComponent.writeValue("blue"); + expect(dropdownElement.getAttribute("value")).toBe("blue"); + }); + + it("should set value attribute to empty string when writeValue is called with null", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + // First set a value + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + // Then clear it + dropdownComponent.writeValue(null); + expect(dropdownElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + + dropdownComponent.writeValue("yellow"); + expect(dropdownComponent.value).toBe("yellow"); + + dropdownComponent.writeValue(null); + expect(dropdownComponent.value).toBe(null); + }); + }); + + describe("_onChange", () => { + it("should update component value when user selects an option", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "yellow" }, + }), + ); + + expect(dropdownComponent.value).toBe("yellow"); + }); + + it("should update value to null when cleared", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + // Set initial value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "red" }, + }), + ); + expect(dropdownComponent.value).toBe("red"); + + // Clear value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "" }, + }), + ); + expect(dropdownComponent.value).toBe(null); + }); + }); +}); \ No newline at end of file diff --git a/libs/angular-components/src/lib/components/dropdown/dropdown.ts b/libs/angular-components/src/lib/components/dropdown/dropdown.ts index eb5ab04cc..5fa3dbc93 100644 --- a/libs/angular-components/src/lib/components/dropdown/dropdown.ts +++ b/libs/angular-components/src/lib/components/dropdown/dropdown.ts @@ -9,6 +9,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -21,6 +22,7 @@ import { GoabControlValueAccessor } from "../base.component"; imports: [CommonModule], template: ` ).detail; + // Keep local value in sync with emitted detail + this.value = detail.value || null; this.onChange.emit(detail); this.markAsTouched(); this.fcChange?.(detail.value || ""); } -} \ No newline at end of file +} diff --git a/libs/angular-components/src/lib/components/input/input.spec.ts b/libs/angular-components/src/lib/components/input/input.spec.ts index 27fd76e31..8069b1391 100644 --- a/libs/angular-components/src/lib/components/input/input.spec.ts +++ b/libs/angular-components/src/lib/components/input/input.spec.ts @@ -266,6 +266,52 @@ describe("GoABInput", () => { expect(trailingContent).toBeTruthy(); expect(trailingContent.textContent).toContain("Trailing Content"); }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + inputComponent.writeValue("new value"); + expect(inputElement.getAttribute("value")).toBe("new value"); + + inputComponent.writeValue("another value"); + expect(inputElement.getAttribute("value")).toBe("another value"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + // First set a value + inputComponent.writeValue("some value"); + expect(inputElement.getAttribute("value")).toBe("some value"); + + // Then clear it with null + inputComponent.writeValue(null); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + inputComponent.writeValue("test"); + inputComponent.writeValue(undefined); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + inputComponent.writeValue("test2"); + inputComponent.writeValue(""); + expect(inputElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + + inputComponent.writeValue("updated"); + expect(inputComponent.value).toBe("updated"); + + inputComponent.writeValue(null); + expect(inputComponent.value).toBe(null); + }); + }); }); @Component({ diff --git a/libs/angular-components/src/lib/components/input/input.ts b/libs/angular-components/src/lib/components/input/input.ts index f1d16292e..41fa0b8b3 100644 --- a/libs/angular-components/src/lib/components/input/input.ts +++ b/libs/angular-components/src/lib/components/input/input.ts @@ -19,6 +19,7 @@ import { numberAttribute, TemplateRef, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { GoabControlValueAccessor } from "../base.component"; @@ -34,6 +35,7 @@ export interface IgnoreMe { imports: [NgIf, NgTemplateOutlet, CommonModule], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabRadioGroup, GoabRadioItem, TestRadioGroupComponent], + imports: [TestRadioGroupComponent, GoabRadioGroup, GoabRadioItem], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -136,14 +137,12 @@ describe("GoABRadioGroup", () => { }); }); - it("should render description", fakeAsync(() => { + it("should render description", () => { component.options.forEach((option, index) => { component.options[index].description = `Description for ${component.options[index].text}`; }); component.options[0].isDescriptionSlot = true; fixture.detectChanges(); - tick(); - fixture.detectChanges(); const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); expect(radioGroup).toBeTruthy(); @@ -157,16 +156,62 @@ describe("GoABRadioGroup", () => { // attribute description expect(radioItems[1].getAttribute("description")).toBe(`Description for ${component.options[1].text}`); expect(radioItems[2].getAttribute("description")).toBe(`Description for ${component.options[2].text}`); - })); + }); it("should dispatch onChange", () => { const onChange = jest.spyOn(component, "onChange"); const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); fireEvent(radioGroup, new CustomEvent("_change", { - detail: { "name": component.name, value: component.options[0].value } + detail: {"name": component.name, value: component.options[0].value} })); - expect(onChange).toBeCalledWith({ name: component.name, value: component.options[0].value }); - }) + expect(onChange).toBeCalledWith({name: component.name, value: component.options[0].value}); + }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + radioGroupComponent.writeValue("apples"); + expect(radioGroupElement.getAttribute("value")).toBe("apples"); + + radioGroupComponent.writeValue("oranges"); + expect(radioGroupElement.getAttribute("value")).toBe("oranges"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + // First set a value + radioGroupComponent.writeValue("bananas"); + expect(radioGroupElement.getAttribute("value")).toBe("bananas"); + + // Then clear it with null + radioGroupComponent.writeValue(null); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + radioGroupComponent.writeValue("apples"); + radioGroupComponent.writeValue(undefined); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + radioGroupComponent.writeValue("oranges"); + radioGroupComponent.writeValue(""); + expect(radioGroupElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + + radioGroupComponent.writeValue("apples"); + expect(radioGroupComponent.value).toBe("apples"); + + radioGroupComponent.writeValue(null); + expect(radioGroupComponent.value).toBe(null); + }); + }); }); diff --git a/libs/angular-components/src/lib/components/radio-group/radio-group.ts b/libs/angular-components/src/lib/components/radio-group/radio-group.ts index a2284c612..56e614363 100644 --- a/libs/angular-components/src/lib/components/radio-group/radio-group.ts +++ b/libs/angular-components/src/lib/components/radio-group/radio-group.ts @@ -11,6 +11,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -21,6 +22,7 @@ import { GoabControlValueAccessor } from "../base.component"; selector: "goab-radio-group", template: ` { expect(onBlur).toBeCalledTimes(1); }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + textareaComponent.writeValue("new content"); + expect(textareaElement.getAttribute("value")).toBe("new content"); + + textareaComponent.writeValue("updated content"); + expect(textareaElement.getAttribute("value")).toBe("updated content"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + // First set a value + textareaComponent.writeValue("some content"); + expect(textareaElement.getAttribute("value")).toBe("some content"); + + // Then clear it with null + textareaComponent.writeValue(null); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + textareaComponent.writeValue("test content"); + textareaComponent.writeValue(undefined); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + textareaComponent.writeValue("more content"); + textareaComponent.writeValue(""); + expect(textareaElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + + textareaComponent.writeValue("updated value"); + expect(textareaComponent.value).toBe("updated value"); + + textareaComponent.writeValue(null); + expect(textareaComponent.value).toBe(null); + }); + }); }); diff --git a/libs/angular-components/src/lib/components/textarea/textarea.ts b/libs/angular-components/src/lib/components/textarea/textarea.ts index 453371b51..8887d2855 100644 --- a/libs/angular-components/src/lib/components/textarea/textarea.ts +++ b/libs/angular-components/src/lib/components/textarea/textarea.ts @@ -15,6 +15,7 @@ import { numberAttribute, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -26,6 +27,7 @@ import { GoabControlValueAccessor } from "../base.component"; imports: [CommonModule], template: ` element function setDisplayedValue() { - _inputEl.value = _selectedOption?.label || _selectedOption?.value || ""; + const newValue = _selectedOption?.label || _selectedOption?.value || ""; + _inputEl.value = newValue; } function dispatchValue(newValue?: string) { @@ -636,6 +641,11 @@ } onKeyUp(_: KeyboardEvent) { + // Clear selection and highlight if input becomes empty + if (this.input.value === "" && _selectedOption) { + _selectedOption = undefined; + _highlightedIndex = -1; + } showMenu(); }