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();
}