From 6752812be57e089874c3a4f7a62c69c08b9c4abe Mon Sep 17 00:00:00 2001 From: zvo-bla Date: Mon, 31 Mar 2025 13:08:40 +0200 Subject: [PATCH 1/6] form-field: Use signals for prefix/suffix (#25) --- .../components/form-field/src/form-field.component.html | 4 ++-- projects/components/form-field/src/form-field.component.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/projects/components/form-field/src/form-field.component.html b/projects/components/form-field/src/form-field.component.html index 291a0a9..3883ccc 100644 --- a/projects/components/form-field/src/form-field.component.html +++ b/projects/components/form-field/src/form-field.component.html @@ -16,13 +16,13 @@ {{ calculatedLabel }} } - @if (_prefixChildren.length) { + @if (_prefixChildren().length) { } - @if (_suffixChildren.length) { + @if (_suffixChildren().length) { diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index d68cfc5..44c0245 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -1,11 +1,9 @@ import { AsyncPipe, isPlatformServer } from '@angular/common'; -import type { QueryList } from '@angular/core'; import { AfterContentChecked, ChangeDetectionStrategy, Component, ContentChild, - ContentChildren, ElementRef, HostBinding, InjectionToken, @@ -16,6 +14,7 @@ import { SimpleChanges, ViewChild, ViewEncapsulation, + contentChildren, inject, } from '@angular/core'; import { FormControl, NgControl } from '@angular/forms'; @@ -96,8 +95,8 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { } public _labelChild: MatLabel | null = null; - @ContentChildren(MatPrefix) public _prefixChildren!: QueryList; - @ContentChildren(MatSuffix) public _suffixChildren!: QueryList; + public readonly _prefixChildren = contentChildren(MatPrefix); + public readonly _suffixChildren = contentChildren(MatSuffix); @HostBinding('class.zv-form-field--subscript-resize') public get autoResizeHintError() { return this.subscriptType === 'resize'; From 68fd849595fadb9e279d59be79fbf0e4d471f25c Mon Sep 17 00:00:00 2001 From: saithis <1547453+saithis@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:52:14 +0200 Subject: [PATCH 2/6] form-field: partial update to use signals --- .../form-field/src/form-field.component.html | 11 +- .../src/form-field.component.spec.ts | 8 +- .../form-field/src/form-field.component.ts | 126 +++++++++--------- 3 files changed, 71 insertions(+), 74 deletions(-) diff --git a/projects/components/form-field/src/form-field.component.html b/projects/components/form-field/src/form-field.component.html index 3883ccc..3fa1984 100644 --- a/projects/components/form-field/src/form-field.component.html +++ b/projects/components/form-field/src/form-field.component.html @@ -3,17 +3,16 @@ style="width: 100%" [class.mat-form-field--emulated]="emulated" [class.mat-form-field--no-underline]="noUnderline" - [floatLabel]="floatLabel" + [floatLabel]="floatLabel()" [hintLabel]="hintText" > - @if (_labelChild) { + @if (_labelChild()) { - } - @if (!_labelChild && calculatedLabel) { + } @else if (calculatedLabel()) { - {{ calculatedLabel }} + {{ calculatedLabel() }} } @if (_prefixChildren().length) { @@ -27,7 +26,7 @@ } - @if (showHintToggle) { + @if (showHintToggle()) { diff --git a/projects/components/form-field/src/form-field.component.spec.ts b/projects/components/form-field/src/form-field.component.spec.ts index 1d1ef58..a0cf6c8 100644 --- a/projects/components/form-field/src/form-field.component.spec.ts +++ b/projects/components/form-field/src/form-field.component.spec.ts @@ -195,8 +195,8 @@ describe('ZvFormField', () => { component.formControl.markAsTouched(); fixture.detectChanges(); - expect(component.formField._ngControl.invalid).toBe(true); - expect(component.formField._matFormField._control.errorState).toBe(true); + expect(component.formField._ngControl().invalid).toBe(true); + expect(component.formField._matFormField()._control.errorState).toBe(true); let errorsChecked = false; component.formField.errors$.subscribe((e) => { @@ -350,7 +350,7 @@ describe('ZvFormField', () => { const component = fixture.componentInstance; expect(component).toBeDefined(); - expect(component.formField.floatLabel).toEqual('auto'); + expect(component.formField.floatLabel()).toEqual('auto'); })); it('should priorize MAT_FORM_FIELD_DEFAULT_OPTIONS over its own settings', waitForAsync(() => { @@ -368,7 +368,7 @@ describe('ZvFormField', () => { const fixture = TestBed.createComponent(TestFormComponent); const component = fixture.componentInstance; expect(component).toBeDefined(); - expect(component.formField.floatLabel).toEqual('always'); + expect(component.formField.floatLabel()).toEqual('always'); })); }); diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index 44c0245..2a6941a 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -3,19 +3,21 @@ import { AfterContentChecked, ChangeDetectionStrategy, Component, - ContentChild, ElementRef, - HostBinding, InjectionToken, - Input, - OnChanges, OnDestroy, PLATFORM_ID, - SimpleChanges, - ViewChild, ViewEncapsulation, contentChildren, inject, + input, + model, + viewChild, + contentChild, + effect, + computed, + signal, + linkedSignal, } from '@angular/core'; import { FormControl, NgControl } from '@angular/forms'; import { MatIconButton } from '@angular/material/button'; @@ -25,6 +27,7 @@ import { MatError, MatFormField, MatFormFieldControl, + MatFormFieldDefaultOptions, MatLabel, MatPrefix, MatSuffix, @@ -44,15 +47,20 @@ export interface ZvFormFieldConfig { export const ZV_FORM_FIELD_CONFIG = new InjectionToken('ZV_FORM_FIELD_CONFIG'); -function applyConfigDefaults(config: ZvFormFieldConfig | null): { +function applyConfigDefaults( + config: ZvFormFieldConfig | null, + matConfig: MatFormFieldDefaultOptions | null +): { subscriptType: ZvFormFieldSubscriptType; hintToggle: boolean; requiredText: string | null; + floatLabel: FloatLabelType; } { return { hintToggle: config?.hintToggle ?? false, subscriptType: config?.subscriptType ?? 'resize', requiredText: config?.requiredText ?? null, + floatLabel: matConfig?.floatLabel ?? 'auto', }; } @@ -60,76 +68,66 @@ function applyConfigDefaults(config: ZvFormFieldConfig | null): { selector: 'zv-form-field', templateUrl: './form-field.component.html', styleUrls: ['./form-field.component.scss'], + host: { + '[class.zv-form-field--subscript-resize]': 'subscriptType() === "resize"', + }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [MatFormField, MatLabel, MatPrefix, MatSuffix, MatIconButton, MatIcon, MatError, AsyncPipe], }) -export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { +export class ZvFormField implements AfterContentChecked, OnDestroy { + private isServer = isPlatformServer(inject(PLATFORM_ID)); private _elementRef = inject(ElementRef); private formsService = inject(ZvFormService); - private defaults = applyConfigDefaults(inject(ZV_FORM_FIELD_CONFIG, { optional: true })); - private matDefaults = inject(MAT_FORM_FIELD_DEFAULT_OPTIONS, { optional: true }); + private defaults = applyConfigDefaults( + inject(ZV_FORM_FIELD_CONFIG, { optional: true }), + inject(MAT_FORM_FIELD_DEFAULT_OPTIONS, { optional: true }) + ); - @Input() public createLabel = true; - @Input() public hint = ''; - @Input() public floatLabel: FloatLabelType = this.matDefaults?.floatLabel || 'auto'; - @Input() public subscriptType: ZvFormFieldSubscriptType = (this.defaults ? this.defaults.subscriptType : null) ?? 'resize'; - @Input() public hintToggle: boolean | null = null; + public readonly createLabel = input(true); + public readonly hint = input(''); + public readonly floatLabel = model(this.defaults.floatLabel); + public readonly subscriptType = input(this.defaults.subscriptType); + public readonly hintToggle = input(this.defaults.hintToggle); - @ViewChild(MatFormField, { static: true }) public _matFormField!: MatFormField; + readonly _matFormField = viewChild.required(MatFormField); /** We can get the FromControl from this */ - @ContentChild(NgControl) public _ngControl: NgControl | null = null; + readonly _ngControl = contentChild(NgControl); /** The MatFormFieldControl or null, if it is no MatFormFieldControl */ - @ContentChild(MatFormFieldControl) public _control: MatFormFieldControl | null = null; + readonly _control = contentChild(MatFormFieldControl); /** The MatLabel, if it is set or null */ - @ContentChild(MatLabel) public set labelChild(value: MatLabel) { - this._labelChild = value; - this.updateLabel(); - if (this._matFormField) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - (this._matFormField as any)._changeDetectorRef.markForCheck(); - } - } - public _labelChild: MatLabel | null = null; + readonly _labelChild = contentChild(MatLabel); public readonly _prefixChildren = contentChildren(MatPrefix); public readonly _suffixChildren = contentChildren(MatSuffix); - @HostBinding('class.zv-form-field--subscript-resize') public get autoResizeHintError() { - return this.subscriptType === 'resize'; - } - // mat-form-field childs, that we dont support: // @ContentChild(MatPlaceholder) _placeholderChild: MatPlaceholder; // Deprecated, placeholder attribute of the form field control should be used instead! // @ContentChildren(MatError) public _errorChildren: QueryList; // Will be created automatically // @ContentChildren(MatHint) public _hintChildren: QueryList; // No idea how to make this work... - public get hintToggleOptionActive(): boolean { - return typeof this.hintToggle === 'boolean' ? this.hintToggle : this.defaults.hintToggle; - } - - public get showHintToggle(): boolean { - return !!this.hint && this.hintToggleOptionActive; - } + public readonly showHintToggle = computed(() => !!this.hint() && this.hintToggle()); + // No computed, because it wouldn't detect control.required/disabled changes anymore public get hintText(): string { - const hintShouldBeShown = this.showHint || !this.hintToggleOptionActive; + const hintShouldBeShown = this.showHint() || !this.hintToggle(); if (!hintShouldBeShown) { return ''; } - const isRequired = this._control?.required; - const isDisabled = this._control?.disabled; + const _control = this._control(); + const isRequired = _control?.required; + const isDisabled = _control?.disabled; if (!isRequired || isDisabled) { - return this.hint; + return this.hint(); } - const requiredText = this.defaults?.requiredText; - return [requiredText, this.hint].filter((s) => !!s).join('. '); + const requiredText = this.defaults.requiredText; + return [requiredText, this.hint()].filter((s) => !!s).join('. '); } /** The error messages to show */ @@ -140,8 +138,8 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { /** Hide the underline for the control */ public noUnderline = false; - public showHint = false; - public calculatedLabel: string | null = null; + public readonly showHint = linkedSignal(() => !this.hintToggle()); + public readonly calculatedLabel = signal(null); private formControl: FormControl | null = null; @@ -158,19 +156,19 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { private initialized = false; - private isServer = isPlatformServer(inject(PLATFORM_ID)); - - public ngOnChanges(changes: SimpleChanges) { - if (changes.hintToggle) { - this.showHint = !this.hintToggleOptionActive; - } + constructor() { + effect(() => { + this._labelChild(); // to trigger the effect + this.updateLabel(); + }); } public ngAfterContentChecked(): void { if (this.initialized) { return; } - this.formControl = this._ngControl && (this._ngControl.control as FormControl); + const _ngControl = this._ngControl(); + this.formControl = _ngControl ? (_ngControl.control as FormControl) : null; // Slider is not initialized the first time we enter this method, therefore we need to check if it got initialized already or not if (this.formControl) { this.initialized = true; @@ -179,8 +177,8 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { if (this.matFormFieldControl instanceof DummyMatFormFieldControl) { this.matFormFieldControl.ngOnDestroy(); } - this.matFormFieldControl = this._control || new DummyMatFormFieldControl(this._ngControl, this.formControl); - this._matFormField._control = this.matFormFieldControl; + this.matFormFieldControl = this._control() || new DummyMatFormFieldControl(this._ngControl() ?? null, this.formControl); + this._matFormField()._control = this.matFormFieldControl; this.emulated = this.matFormFieldControl instanceof DummyMatFormFieldControl; // This tells the mat-input that it is inside a mat-form-field // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -188,14 +186,14 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (this.matFormFieldControl as any)._isInFormField = true; } - this.realFormControl = getRealFormControl(this._ngControl, this.matFormFieldControl); + this.realFormControl = getRealFormControl(this._ngControl(), this.matFormFieldControl); this.controlType = this.formsService.getControlType(this.realFormControl) || 'unknown'; // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access this._elementRef.nativeElement.classList.add(`zv-form-field-type-${this.controlType}`); this.noUnderline = this.emulated || !!this.realFormControl.noUnderline; - if (this.floatLabel === 'auto' && (this.emulated || this.realFormControl.shouldLabelFloat === undefined)) { - this.floatLabel = 'always'; + if (this.floatLabel() === 'auto' && (this.emulated || this.realFormControl.shouldLabelFloat === undefined)) { + this.floatLabel.set('always'); } if (this.formControl) { @@ -221,7 +219,7 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { } public toggleHint(event: MouseEvent) { - this.showHint = !this.showHint; + this.showHint.set(!this.showHint()); event.stopPropagation(); } @@ -229,8 +227,8 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { if (this.isServer) { return; } - this.calculatedLabel = null; - if (!this.createLabel || this._labelChild || !this.formControl) { + this.calculatedLabel.set(null); + if (!this.createLabel() || this._labelChild() || !this.formControl) { return; } @@ -258,19 +256,19 @@ export class ZvFormField implements OnChanges, AfterContentChecked, OnDestroy { } } } else { - this.calculatedLabel = label; + this.calculatedLabel.set(label); } // when only our own component is marked for check, then the label will not be shown // when labelText$ didn't run synchronously // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - (this._matFormField as any)._changeDetectorRef.markForCheck(); + (this._matFormField() as any)._changeDetectorRef.markForCheck(); }); } } function getRealFormControl( - ngControl: NgControl | null, + ngControl: NgControl | null | undefined, matFormFieldControl: MatFormFieldControl ): { noUnderline?: boolean; shouldLabelFloat?: boolean } { if (!(matFormFieldControl instanceof DummyMatFormFieldControl) || !ngControl) { From d2e530833a2ec654e47749efb23585cd97590f64 Mon Sep 17 00:00:00 2001 From: saithis <1547453+saithis@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:25:16 +0200 Subject: [PATCH 3/6] More form-field signal updates --- .../src/dummy-mat-form-field-control.ts | 15 ++++++-- .../form-field/src/form-field.component.ts | 37 +++++++++++-------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/projects/components/form-field/src/dummy-mat-form-field-control.ts b/projects/components/form-field/src/dummy-mat-form-field-control.ts index 3169676..31e2c6e 100644 --- a/projects/components/form-field/src/dummy-mat-form-field-control.ts +++ b/projects/components/form-field/src/dummy-mat-form-field-control.ts @@ -52,16 +52,23 @@ export class DummyMatFormFieldControl implements MatFormFieldControl, On public autofilled?: boolean; + public ngControl: NgControl | null = null; + private _value: string | null = null; private _required = false; private _disabled = false; private _valueSubscription: Subscription | null = null; private _statusSubscription: Subscription | null = null; - constructor( - public ngControl: NgControl | null, - formControl: AbstractControl | null - ) { + constructor(ngControl: NgControl | null, formControl: AbstractControl | null) { + this.init(ngControl, formControl); + } + + public init(ngControl: NgControl | null, formControl: AbstractControl | null) { + this.ngControl = ngControl; + + this._valueSubscription?.unsubscribe(); + this._statusSubscription?.unsubscribe(); if (formControl) { this._valueSubscription = formControl.valueChanges.pipe(startWith(formControl.value)).subscribe((value) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index 2a6941a..820ed46 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -18,6 +18,7 @@ import { computed, signal, linkedSignal, + untracked, } from '@angular/core'; import { FormControl, NgControl } from '@angular/forms'; import { MatIconButton } from '@angular/material/button'; @@ -134,14 +135,16 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { public errors$: Observable = of([]); /** Indicates if the control is no real MatFormFieldControl */ - public emulated = false; + public get emulated() { + return this.matFormFieldControl instanceof DummyMatFormFieldControl; + } /** Hide the underline for the control */ public noUnderline = false; public readonly showHint = linkedSignal(() => !this.hintToggle()); public readonly calculatedLabel = signal(null); - private formControl: FormControl | null = null; + private readonly formControl = computed(() => (this._ngControl()?.control as FormControl) ?? null); /** Either the MatFormFieldControl or a DummyMatFormFieldControl */ private matFormFieldControl!: MatFormFieldControl; @@ -159,7 +162,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { constructor() { effect(() => { this._labelChild(); // to trigger the effect - this.updateLabel(); + untracked(() => this.updateLabel()); }); } @@ -167,25 +170,27 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { if (this.initialized) { return; } - const _ngControl = this._ngControl(); - this.formControl = _ngControl ? (_ngControl.control as FormControl) : null; // Slider is not initialized the first time we enter this method, therefore we need to check if it got initialized already or not - if (this.formControl) { + const formControl = this.formControl(); + if (formControl) { this.initialized = true; } - // We hope noone subscribed matFormFieldControl.stateChanges already - 🤞 + if (!this.matFormFieldControl) { + this.matFormFieldControl = this._control() || new DummyMatFormFieldControl(null, null); + } + if (this.matFormFieldControl instanceof DummyMatFormFieldControl) { - this.matFormFieldControl.ngOnDestroy(); + this.matFormFieldControl.init(this._ngControl() ?? null, formControl); } - this.matFormFieldControl = this._control() || new DummyMatFormFieldControl(this._ngControl() ?? null, this.formControl); this._matFormField()._control = this.matFormFieldControl; - this.emulated = this.matFormFieldControl instanceof DummyMatFormFieldControl; + // This tells the mat-input that it is inside a mat-form-field // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if ((this.matFormFieldControl as any)._isInFormField !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (this.matFormFieldControl as any)._isInFormField = true; } + this.realFormControl = getRealFormControl(this._ngControl(), this.matFormFieldControl); this.controlType = this.formsService.getControlType(this.realFormControl) || 'unknown'; // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access @@ -196,13 +201,13 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { this.floatLabel.set('always'); } - if (this.formControl) { + if (formControl) { if (this.formsService.tryDetectRequired) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (this.matFormFieldControl as any).required = hasRequiredField(this.formControl); + (this.matFormFieldControl as any).required = hasRequiredField(formControl); } - this.errors$ = this.formsService.getControlErrors(this.formControl); + this.errors$ = this.formsService.getControlErrors(formControl); this.updateLabel(); } @@ -224,15 +229,15 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { } private updateLabel() { - if (this.isServer) { + if (this.isServer || !this.initialized) { return; } this.calculatedLabel.set(null); - if (!this.createLabel() || this._labelChild() || !this.formControl) { + if (!this.createLabel() || this._labelChild() || !this.formControl()) { return; } - const labelText$ = this.formsService.getLabel(this.formControl); + const labelText$ = this.formsService.getLabel(this.formControl()!); if (!labelText$) { return; } From d2752092e899cbc94ffd02c770e85f82c15532bf Mon Sep 17 00:00:00 2001 From: saithis <1547453+saithis@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:39:27 +0200 Subject: [PATCH 4/6] form-field: make matFormFieldControl computed --- .../form-field/src/form-field.component.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index 820ed46..86cfecf 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -147,7 +147,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { private readonly formControl = computed(() => (this._ngControl()?.control as FormControl) ?? null); /** Either the MatFormFieldControl or a DummyMatFormFieldControl */ - private matFormFieldControl!: MatFormFieldControl; + private matFormFieldControl = computed>(() => this._control() || this.#lazyDummyMatformFieldControl.val); /** The real control instance (MatSlider, MatSelect, MatCheckbox, ...) */ private realFormControl!: { noUnderline?: boolean; shouldLabelFloat?: boolean }; @@ -166,6 +166,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { }); } + #lazyDummyMatformFieldControl = new Lazy(() => new DummyMatFormFieldControl(null, null)); public ngAfterContentChecked(): void { if (this.initialized) { return; @@ -175,23 +176,21 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { if (formControl) { this.initialized = true; } - if (!this.matFormFieldControl) { - this.matFormFieldControl = this._control() || new DummyMatFormFieldControl(null, null); - } - if (this.matFormFieldControl instanceof DummyMatFormFieldControl) { - this.matFormFieldControl.init(this._ngControl() ?? null, formControl); + const matFormFieldControl = this.matFormFieldControl(); + if (matFormFieldControl instanceof DummyMatFormFieldControl) { + matFormFieldControl.init(this._ngControl() ?? null, formControl); } - this._matFormField()._control = this.matFormFieldControl; + this._matFormField()._control = matFormFieldControl; // This tells the mat-input that it is inside a mat-form-field // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if ((this.matFormFieldControl as any)._isInFormField !== undefined) { + if ((matFormFieldControl as any)._isInFormField !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (this.matFormFieldControl as any)._isInFormField = true; + (matFormFieldControl as any)._isInFormField = true; } - this.realFormControl = getRealFormControl(this._ngControl(), this.matFormFieldControl); + this.realFormControl = getRealFormControl(this._ngControl(), matFormFieldControl); this.controlType = this.formsService.getControlType(this.realFormControl) || 'unknown'; // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access this._elementRef.nativeElement.classList.add(`zv-form-field-type-${this.controlType}`); @@ -204,7 +203,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { if (formControl) { if (this.formsService.tryDetectRequired) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (this.matFormFieldControl as any).required = hasRequiredField(formControl); + (matFormFieldControl as any).required = hasRequiredField(formControl); } this.errors$ = this.formsService.getControlErrors(formControl); @@ -214,8 +213,9 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { } public ngOnDestroy(): void { - if (this.matFormFieldControl instanceof DummyMatFormFieldControl) { - this.matFormFieldControl.ngOnDestroy(); + const matFormFieldControl = this.matFormFieldControl(); + if (matFormFieldControl instanceof DummyMatFormFieldControl) { + matFormFieldControl.ngOnDestroy(); } if (this.labelTextSubscription) { @@ -281,3 +281,11 @@ function getRealFormControl( } return ngControl.valueAccessor as unknown as { noUnderline?: boolean; shouldLabelFloat?: boolean }; } + +class Lazy { + #instance: T | undefined; + get val(): T { + return this.#instance ?? (this.#instance = this.creator()); + } + constructor(private creator: () => T) {} +} From 19abec212f3d496d98881620aa6c28ea1f451b0d Mon Sep 17 00:00:00 2001 From: saithis <1547453+saithis@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:48:01 +0200 Subject: [PATCH 5/6] form-field: Make noUnderline getter property --- .../components/form-field/src/form-field.component.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index 86cfecf..57663e4 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -37,6 +37,7 @@ import { MatIcon } from '@angular/material/icon'; import { IZvFormError, ZvFormService, hasRequiredField } from '@zvoove/components/form-base'; import { Observable, Subscription, of } from 'rxjs'; import { DummyMatFormFieldControl } from './dummy-mat-form-field-control'; +import { type MatInput } from '@angular/material/input'; export declare type ZvFormFieldSubscriptType = 'resize' | 'single-line'; @@ -140,7 +141,9 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { } /** Hide the underline for the control */ - public noUnderline = false; + public get noUnderline() { + return this.emulated || !!this.realFormControl?.noUnderline || false; + } public readonly showHint = linkedSignal(() => !this.hintToggle()); public readonly calculatedLabel = signal(null); @@ -184,8 +187,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { this._matFormField()._control = matFormFieldControl; // This tells the mat-input that it is inside a mat-form-field - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if ((matFormFieldControl as any)._isInFormField !== undefined) { + if ((matFormFieldControl as MatInput)._isInFormField !== undefined) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (matFormFieldControl as any)._isInFormField = true; } @@ -195,7 +197,6 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access this._elementRef.nativeElement.classList.add(`zv-form-field-type-${this.controlType}`); - this.noUnderline = this.emulated || !!this.realFormControl.noUnderline; if (this.floatLabel() === 'auto' && (this.emulated || this.realFormControl.shouldLabelFloat === undefined)) { this.floatLabel.set('always'); } From 63a9b65063c4401f3d482d284f1deb2a9e2bbeb3 Mon Sep 17 00:00:00 2001 From: saithis <1547453+saithis@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:08:05 +0200 Subject: [PATCH 6/6] fix form-field border not hidden for checkbox, radio, etc. --- .../form-field/src/form-field.component.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/projects/components/form-field/src/form-field.component.ts b/projects/components/form-field/src/form-field.component.ts index 57663e4..d5dd231 100644 --- a/projects/components/form-field/src/form-field.component.ts +++ b/projects/components/form-field/src/form-field.component.ts @@ -8,17 +8,17 @@ import { OnDestroy, PLATFORM_ID, ViewEncapsulation, + computed, + contentChild, contentChildren, + effect, inject, input, + linkedSignal, model, - viewChild, - contentChild, - effect, - computed, signal, - linkedSignal, untracked, + viewChild, } from '@angular/core'; import { FormControl, NgControl } from '@angular/forms'; import { MatIconButton } from '@angular/material/button'; @@ -34,10 +34,10 @@ import { MatSuffix, } from '@angular/material/form-field'; import { MatIcon } from '@angular/material/icon'; +import { type MatInput } from '@angular/material/input'; import { IZvFormError, ZvFormService, hasRequiredField } from '@zvoove/components/form-base'; import { Observable, Subscription, of } from 'rxjs'; import { DummyMatFormFieldControl } from './dummy-mat-form-field-control'; -import { type MatInput } from '@angular/material/input'; export declare type ZvFormFieldSubscriptType = 'resize' | 'single-line'; @@ -137,7 +137,7 @@ export class ZvFormField implements AfterContentChecked, OnDestroy { /** Indicates if the control is no real MatFormFieldControl */ public get emulated() { - return this.matFormFieldControl instanceof DummyMatFormFieldControl; + return this.matFormFieldControl() instanceof DummyMatFormFieldControl; } /** Hide the underline for the control */