diff --git a/apps/example/src/app/app.component.ts b/apps/example/src/app/app.component.ts index 37a726b..4edcf1f 100644 --- a/apps/example/src/app/app.component.ts +++ b/apps/example/src/app/app.component.ts @@ -16,6 +16,9 @@ import { RouterLink, RouterOutlet } from '@angular/router';
  • Multipage form
  • +
  • + Form with CVA +
  • diff --git a/apps/example/src/app/app.routes.ts b/apps/example/src/app/app.routes.ts index fd753da..95b975c 100644 --- a/apps/example/src/app/app.routes.ts +++ b/apps/example/src/app/app.routes.ts @@ -18,4 +18,8 @@ export const routes: Routes = [ path: 'multi-page-form', loadChildren: () => import('./multi-page-form/multi-page-form.routes'), }, + { + path: 'form-with-cva', + loadComponent: () => import('./form-with-cva/form-with-cva.component'), + }, ]; diff --git a/apps/example/src/app/form-with-cva/address/address.component.ts b/apps/example/src/app/form-with-cva/address/address.component.ts new file mode 100644 index 0000000..3d7e69f --- /dev/null +++ b/apps/example/src/app/form-with-cva/address/address.component.ts @@ -0,0 +1,79 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + forwardRef, effect +} from '@angular/core'; +import { + ControlValueAccessor, + FormsModule, + NG_VALUE_ACCESSOR +} from '@angular/forms'; +import { createFormField, createFormGroup, SignalInputDirective, V } from '@ng-signal-forms'; +import { Address } from './address'; + +@Component({ + selector: 'app-address', + standalone: true, + imports: [CommonModule, SignalInputDirective, FormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AddressComponent), + multi: true + } + ], + template: ` +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AddressComponent + implements ControlValueAccessor { + onChange: any; + onTouched: any; + formGroup = createFormGroup({ + street: createFormField(undefined as undefined | string, { validators: [V.minLength(3)] }), + zip: createFormField(undefined as undefined | string, { validators: [V.minLength(3)] }), + city: createFormField(undefined as undefined | string, { validators: [V.minLength(3)] }), + country: createFormField(undefined as undefined | string, { validators: [V.minLength(3)] }) + }); + + change = effect(() => { + this.onChange(this.formGroup.value()); + }, { allowSignalWrites: true }); + + registerOnChange(onChange: any): void { + this.onChange = onChange; + } + + registerOnTouched(onTouched: any): void { + this.onTouched = onTouched; + } + + writeValue(obj: Address): void { + // TODO: obj should receive values from parent + if (obj) { + this.formGroup.controls.street.value.set(obj.street); + this.formGroup.controls.zip.value.set(obj.zip); + this.formGroup.controls.city.value.set(obj.city); + this.formGroup.controls.country.value.set(obj.country); + } + } +} diff --git a/apps/example/src/app/form-with-cva/address/address.ts b/apps/example/src/app/form-with-cva/address/address.ts new file mode 100644 index 0000000..091faec --- /dev/null +++ b/apps/example/src/app/form-with-cva/address/address.ts @@ -0,0 +1,6 @@ +export interface Address { + street?: string; + zip?: string; + city?: string; + country?: string; +} diff --git a/apps/example/src/app/form-with-cva/form-with-cva.component.ts b/apps/example/src/app/form-with-cva/form-with-cva.component.ts new file mode 100644 index 0000000..da6ae5e --- /dev/null +++ b/apps/example/src/app/form-with-cva/form-with-cva.component.ts @@ -0,0 +1,94 @@ +import { JsonPipe, NgFor, NgIf } from '@angular/common'; +import { Component, effect } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + createFormGroup, + SignalInputDebounceDirective, + SignalInputDirective, + SignalInputErrorDirective, + withErrorComponent +} from '@ng-signal-forms'; +import { CustomErrorComponent } from '../custom-input-error.component'; +import { AddressComponent } from './address/address.component'; + +@Component({ + selector: 'app-basic-form', + template: ` +
    +
    +
    + + +
    + +
    + + +
    + +
    + +
    +
    + +
    + + + +

    States

    +
    {{
    +            {
    +              state: form.state(),
    +              dirtyState: form.dirtyState(),
    +              touchedState: form.touchedState(),
    +              valid: form.valid()
    +            } | json
    +          }}
    +    
    + +

    Value

    +
    {{ form.value() | json }}
    + +

    Errors

    +
    {{ form.errorsArray() | json }}
    +
    +
    + `, + standalone: true, + imports: [ + JsonPipe, + FormsModule, + SignalInputDirective, + SignalInputErrorDirective, + NgIf, + NgFor, + SignalInputDebounceDirective, + AddressComponent, + ], + providers: [withErrorComponent(CustomErrorComponent)], +}) +export default class FormWithCvaComponent { + // TODO: type of address should be Address | null + form = createFormGroup({ + name: 'Alice', + age: null as number | null, + // TODO: this should a form group so the initial value will be set in the form + // but if we make this a group now, then the user input is not emitted back to the form + address: { city: 'Vienna'} as any + }); + + formChanged = effect(() => { + console.log('form changed:', this.form.value()); + }); + + reset() { + this.form.reset(); + } + + prefill() { + // TODO: improve this API to set form groups + this.form.controls.age.value.set(42); + this.form.controls.name.value.set('Bob'); + } +} diff --git a/packages/platform/src/lib/signal-input.directive.ts b/packages/platform/src/lib/signal-input.directive.ts index 344f428..db9f320 100644 --- a/packages/platform/src/lib/signal-input.directive.ts +++ b/packages/platform/src/lib/signal-input.directive.ts @@ -16,8 +16,8 @@ import { SIGNAL_INPUT_MODIFIER, SignalInputModifier } from "./signal-input-modif '[class.ng-dirty]': 'this.formField?.dirtyState() === "DIRTY"', '[class.ng-touched]': 'this.formField?.touchedState() === "TOUCHED"', '[class.ng-untouched]': 'this.formField?.touchedState() === "UNTOUCHED"', - '[attr.disabled]': '!propagateState ? undefined : this.formField?.disabled() ? true : undefined', - '[attr.readonly]': '!propagateState ? undefined : this.formField?.readOnly() ? true : undefined', + '[attr.disabled]': '!propagateState ? undefined : this.formField?.disabled?.() ? true : undefined', + '[attr.readonly]': '!propagateState ? undefined : this.formField?.readOnly?.() ? true : undefined', }, }) export class SignalInputDirective implements OnInit { @@ -30,7 +30,7 @@ export class SignalInputDirective implements OnInit { onModelChange(value: unknown) { if (this.modifiers && this.modifiers.length === 1) { this.modifiers[0].onModelChange(value); - } else if (this.formField) { + } else if (this.formField && this.formField.value.set) { this.formField.value.set(value); } } @@ -67,6 +67,6 @@ export class SignalInputDirective implements OnInit { emitModelToViewChange: true, })); - this.formField?.registerOnReset(value => this.model.control.setValue(value)) + this.formField?.registerOnReset?.(value => this.model.control.setValue(value)) } }