Skip to content

Latest commit

 

History

History
320 lines (252 loc) · 10.3 KB

File metadata and controls

320 lines (252 loc) · 10.3 KB

@dynamic-field-kit/angular

Angular adapter for @dynamic-field-kit/core.

This package provides Angular components and a convenience module that render field schemas defined with @dynamic-field-kit/core.

Live demo: https://vannt-dev.github.io/dynamic-field-kit/angular/

Install

npm install @dynamic-field-kit/core @dynamic-field-kit/angular

Note: @dynamic-field-kit/core, @angular/core, and @angular/common are peer dependencies — this adapter does not bundle or auto-install them, so add them to your app explicitly (as shown above). Keep a single @dynamic-field-kit/core version across all adapters so they share one registry.

If you need to pin versions explicitly:

npm install @dynamic-field-kit/core@^1.0.12 @dynamic-field-kit/angular@^1.2.3

What it exports

  • DynamicInput
  • FieldInput
  • MultiFieldInput
  • DynamicFieldKitModule
  • fieldRegistry
  • FieldRegistry (class, for scoped registries)
  • FIELD_REGISTRY (injection token)
  • validateField / validateFields / resolveDisabled / resolveReadOnly / ValidationResult
  • createDynamicFormStore (signal-based form state)
  • DynamicFormDevToolsComponent

Basic setup (Angular 19+)

  1. Import the component and register fields before bootstrap.
import 'zone.js';
import { bootstrapApplication } from '@angular/platform-browser';
import { fieldRegistry } from '@dynamic-field-kit/angular';
import { AppComponent } from './app/app.component';
import { TextFieldComponent } from './app/components/text-field.component';
import { NumberFieldComponent } from './app/components/number-field.component';

fieldRegistry.register('text', TextFieldComponent as any);
fieldRegistry.register('number', NumberFieldComponent as any);

bootstrapApplication(AppComponent, {
  providers: [],
}).catch((err) => console.error(err));
  1. Use the component in a standalone component.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FieldDescription } from '@dynamic-field-kit/core';
import { MultiFieldInput } from '@dynamic-field-kit/angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, MultiFieldInput],
  templateUrl: './app.component.html',
})
export class AppComponent {
  fields: FieldDescription[] = [
    { name: 'name', type: 'text', label: 'Name' },
    { name: 'age', type: 'number', label: 'Age' },
  ];

  data: any = {};

  onChange(data: any) {
    this.data = data;
  }
}
  1. Render your schema in a template.
<dfk-multi-field-input
  [fieldDescriptions]="fields"
  [properties]="data"
  (onChange)="onChange($event)"
></dfk-multi-field-input>

Form state (createDynamicFormStore)

A signal-based store — the Angular counterpart of React and Vue's useDynamicForm, with the same members. Read them as signals.

import {
  createDynamicFormStore,
  MultiFieldInput,
} from '@dynamic-field-kit/angular';

@Component({
  standalone: true,
  imports: [MultiFieldInput],
  template: `
    <form (ngSubmit)="onSubmit($event)">
      <dfk-multi-field-input
        [fieldDescriptions]="fields"
        [properties]="store.data()"
        (onChange)="store.handleChange($event)"
        (onBlurField)="store.handleBlur($event)"
      ></dfk-multi-field-input>
      <button [disabled]="store.isSubmitting()">Save</button>
    </form>
  `,
})
export class MyForm {
  fields = fields;
  store = createDynamicFormStore({
    fields,
    initialValues: { country: 'VN' },
    validateOnBlur: true, // default
  });

  // handleSubmit returns a handler, exactly like React and Vue.
  onSubmit = this.store.handleSubmit((data) => this.save(data));
}
Member Description
data() Current form data, with computeValue fields applied
errors() Record<string, string[]>, keyed like validateFields
isValid() / isDirty() No errors recorded / any value has changed
isSubmitting() / isSubmitted() In-flight submit / at least one submit attempted
touched() Fields that have been blurred
handleChange(data) Replace the whole form data — bind to (onChange)
setFieldValue(name, value) Change one field
handleBlur(name) Mark touched, and validate when validateOnBlur
setFieldTouched(name, value?) Set touched explicitly
validate() Validate now, returns a boolean
reset(values?) Back to initialValues (or the values given), clearing errors/touched/submission
handleSubmit(onValid, onInvalid?) Returns an async handler; calls preventDefault, validates, then dispatches

MultiFieldInput emits (onBlurField) with the field's name, driven by a focusout listener — so it works with any renderer, without the renderer needing a blur output of its own.

Default renderers

text · number · password · email · textarea · checkbox · select · radio · range · file · date · time · datetime-local · switch

Any type you have not registered falls back to one of these. file emits a File (or File[] when multiple is set), range and number emit numbers, checkbox / switch emit booleans; everything else emits strings.

DevTools

<dfk-dev-tools
  [data]="store.data()"
  [errors]="store.errors()"
  [touched]="store.touched()"
  [isDirty]="store.isDirty()"
  [fields]="fields"
></dfk-dev-tools>

Import DynamicFormDevToolsComponent. A floating overlay with data / errors / meta / fields tabs; the collapsed button carries a red badge with the number of fields in error.

Layouts

MultiFieldInput supports column, row, grid, and responsive (mobile/desktop with a custom breakpoint), matching the React and Vue adapters:

<dfk-multi-field-input
  [fieldDescriptions]="fields"
  [layout]="{ type: 'grid', columns: 3, gap: 16 }"
></dfk-multi-field-input>

<dfk-multi-field-input
  [fieldDescriptions]="fields"
  [layout]="{
    type: 'responsive',
    mobile: 'column',
    desktop: { type: 'grid', columns: 2, gap: 12 }
  }"
></dfk-multi-field-input>

Derived fields with computeValue

Give a field a computeValue to derive its value from the rest of the form data whenever any field changes:

fields: FieldDescription[] = [
  { name: 'firstName', type: 'text' },
  { name: 'lastName', type: 'text' },
  {
    name: 'fullName',
    type: 'text',
    computeValue: (data) => `${data.firstName ?? ''} ${data.lastName ?? ''}`.trim(),
  },
];

Validation & conditions

Declare a validate hook and dynamic disabledCondition/readOnlyCondition; your renderer component receives error, disabled, and readOnly inputs, and dfk-multi-field-input emits (validityChange):

<dfk-multi-field-input
  [fieldDescriptions]="fields"
  [properties]="data"
  (onChange)="onChange($event)"
  (validityChange)="canSubmit = $event.valid"
></dfk-multi-field-input>

For submit-time whole-form validation, call validateFields(fields, data).

Repeatable field groups

A field with fields renders as a repeatable group: data[name] becomes an array of items, each shaped by the nested fields, with "Add"/"Remove" controls rendered automatically.

fields: FieldDescription[] = [
  {
    name: 'contacts',
    type: 'group',
    label: 'Contacts',
    fields: [
      { name: 'email', type: 'text', label: 'Email' },
      { name: 'phone', type: 'text', label: 'Phone' },
    ],
    defaultItem: { email: '', phone: '' },
    keyField: 'id', // optional: stable trackBy key instead of the array index
    minItems: 1,
    maxItems: 5,
  },
];
<dfk-multi-field-input [fieldDescriptions]="fields"></dfk-multi-field-input>

Scoped registries

fieldRegistry is a process-wide singleton. To give a component or route its own renderers, provide the FIELD_REGISTRY token with an isolated FieldRegistry. Components without an override keep using the global singleton.

import { FieldRegistry, FIELD_REGISTRY } from '@dynamic-field-kit/angular';

const registry = new FieldRegistry();
registry.register('text', TextFieldComponent as any);

@Component({
  selector: 'app-scoped-form',
  standalone: true,
  imports: [MultiFieldInput],
  providers: [{ provide: FIELD_REGISTRY, useValue: registry }],
  template: `<dfk-multi-field-input [fieldDescriptions]="fields" />`,
})
export class ScopedFormComponent {}

Passing renderer-specific props

The generic adapter forwards only the shared FieldRendererProps. For inputs specific to one renderer (e.g. acceptFile, maxLength), pass them per field via FieldDescription.props; they are set on the renderer instance verbatim.

{ name: 'avatar', type: 'file', props: { acceptFile: 'image/*' } }

Legacy setup (Angular 14 and earlier with NgModule)

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { DynamicFieldKitModule } from '@dynamic-field-kit/angular';

@NgModule({
  imports: [BrowserModule, DynamicFieldKitModule],
})
export class AppModule {}

Type augmentation

import '@dynamic-field-kit/core';

declare module '@dynamic-field-kit/core' {
  interface FieldTypeMap {
    text: string;
    number: number;
  }
}

Notes

  • Register Angular component classes in fieldRegistry.
  • Do not register React or Vue renderers in the Angular adapter.
  • MultiFieldInput supports column, row, grid, and responsive layouts, and derives fields using computeValue.
  • Fields with fields render as repeatable groups instead of going through fieldRegistry.
  • The shared schema and field types still come from @dynamic-field-kit/core.

License

MIT