@zellvora/ng-form
v0.1.2
Published
Schema-driven dynamic forms built on @zellvora/ng-input.
Readme
@zellvora/ng-form
Schema-driven dynamic forms built on @zellvora/ng-input. Define a form in JSON, get a fully functional form with validation, error handling, and responsive layout.
Features
- Schema-driven — Define forms declaratively in TypeScript or JSON
- 15 field types — Text, email, password, number, textarea, date, time, select, multiselect, checkbox, radio, switch, PIN, upload, and more
- Built-in validation — 30+ validators from ng-input, plus custom validators
- Conditional fields — Show/hide fields based on model state
- Responsive grid — 12-column grid layout with column spanning
- ControlValueAccessor — Works with
ngModel,formControlName, or Signal Forms - Type-safe — Fully typed schemas and models
Installation
npm install @zellvora/ng-input @zellvora/ng-formSetup
import { ApplicationConfig } from '@angular/core';
import { provideZvInput } from '@zellvora/ng-input';
export const appConfig: ApplicationConfig = {
providers: [
provideZvInput(),
],
};Quick Start
Basic Form
import { Component } from '@angular/core';
import { ZvDynamicForm, ZvFormSchema } from '@zellvora/ng-form';
@Component({
selector: 'app-contact-form',
standalone: true,
imports: [ZvDynamicForm],
template: `
<zv-dynamic-form
[schema]="schema"
[(ngModel)]="formData"
(ngModelChange)="onFormChange($event)"
></zv-dynamic-form>
`,
})
export class ContactFormComponent {
schema: ZvFormSchema = [
{ field: 'name', type: 'input', label: 'Full Name', required: true },
{ field: 'email', type: 'email', label: 'Email', required: true },
{ field: 'message', type: 'textarea', label: 'Message' },
];
formData = { name: '', email: '', message: '' };
onFormChange(data: any) {
console.log('Form data:', data);
}
}Schema Definition
ZvFormSchema Type
A form schema is an array of field definitions:
import { ZvFormSchema, ZvFieldSchema } from '@zellvora/ng-form';
interface ZvFieldSchema {
type: ZvFieldType; // Required: field type
field: string; // Required: field name
label?: string; // Display label
placeholder?: string; // Placeholder text
hint?: string; // Helper text below field
required?: boolean; // Is field required?
disabled?: boolean; // Is field disabled?
items?: unknown[]; // For select/multiselect/radio
validators?: string[]; // Built-in validator names
validatorFns?: ValidatorFn[]; // Custom sync validators
asyncValidatorFns?: AsyncValidatorFn[]; // Custom async validators
props?: Record<string, unknown>; // Extra inputs for the control
cols?: number; // Grid column span (1-12, default 12)
showWhen?: (model: any) => boolean; // Conditional visibility
}
type ZvFormSchema = ZvFieldSchema[];Field Types
All 15 ng-input controls are available:
Text Fields
const schema: ZvFormSchema = [
// Text input
{
field: 'name',
type: 'input',
label: 'Full Name',
placeholder: 'John Doe',
required: true,
hint: 'Your legal name',
cols: 12,
},
// Email
{
field: 'email',
type: 'email',
label: 'Email Address',
required: true,
cols: 6,
},
// Password
{
field: 'password',
type: 'password',
label: 'Password',
required: true,
validators: ['required'],
cols: 6,
},
// Number
{
field: 'age',
type: 'number',
label: 'Age',
required: true,
props: { min: 18, max: 120 },
cols: 6,
},
// Textarea
{
field: 'bio',
type: 'textarea',
label: 'Bio',
placeholder: 'Tell us about yourself',
props: { rows: 4 },
cols: 12,
},
];Date/Time
const schema: ZvFormSchema = [
{
field: 'birthDate',
type: 'datepicker',
label: 'Date of Birth',
required: true,
cols: 6,
},
{
field: 'meetingTime',
type: 'timepicker',
label: 'Meeting Time',
cols: 6,
},
];Select Fields
const countries = [
{ label: 'India', value: 'IN' },
{ label: 'USA', value: 'US' },
{ label: 'Canada', value: 'CA' },
];
const schema: ZvFormSchema = [
// Single select
{
field: 'country',
type: 'select',
label: 'Country',
items: countries,
required: true,
cols: 6,
},
// Multi-select
{
field: 'languages',
type: 'multiselect',
label: 'Languages',
items: [
{ label: 'English', value: 'EN' },
{ label: 'Hindi', value: 'HI' },
{ label: 'Spanish', value: 'ES' },
],
cols: 6,
},
// Radio buttons
{
field: 'gender',
type: 'radio',
label: 'Gender',
items: [
{ label: 'Male', value: 'M' },
{ label: 'Female', value: 'F' },
{ label: 'Other', value: 'O' },
],
cols: 12,
},
];Toggle/Switch
const schema: ZvFormSchema = [
// Checkbox
{
field: 'agreeTerms',
type: 'checkbox',
label: 'I agree to terms and conditions',
required: true,
},
// Switch/Toggle
{
field: 'newsletter',
type: 'switch',
label: 'Subscribe to newsletter',
},
];Specialized
const schema: ZvFormSchema = [
// PIN input (OTP)
{
field: 'otp',
type: 'pin',
label: 'Enter OTP',
props: { length: 6 },
},
// File upload
{
field: 'document',
type: 'upload',
label: 'Upload Document',
props: { accept: '.pdf,.doc,.docx', multiple: false },
},
];Validation
Built-in Validators
Use validator names as strings in the validators array:
const schema: ZvFormSchema = [
{
field: 'email',
type: 'email',
label: 'Email',
validators: ['required', 'email'],
},
{
field: 'age',
type: 'number',
label: 'Age',
validators: ['required', 'min:18', 'max:100'],
},
{
field: 'phone',
type: 'input',
label: 'Phone',
validators: ['phone'],
},
{
field: 'pan',
type: 'input',
label: 'PAN',
validators: ['pan'],
},
{
field: 'name',
type: 'input',
label: 'Name',
validators: ['required', 'minLength:3', 'maxLength:50'],
},
];Available built-in validators:
required - Value is required
email - Valid email format
phone - Valid phone number
mobile - Valid mobile (India)
url - Valid URL
password - Strong password (8+ chars, mixed case, numbers, symbols)
pan - Valid PAN (India)
gst - Valid GST (India)
aadhaar - Valid Aadhaar (India)
passport - Valid passport number
drivingLicense - Valid driving license
pincode - Valid pincode (India)
ifsc - Valid IFSC (India)
cvv - Valid CVV
swift - Valid SWIFT
iban - Valid IBAN
time - Valid time format (HH:MM)
date - Valid date
pattern:regex - Matches regex pattern
min:number - Minimum numeric value
max:number - Maximum numeric value
minLength:number - Minimum string length
maxLength:number - Maximum string lengthCustom Validators
import { ValidatorFn } from '@angular/forms';
import { ZvFormSchema } from '@zellvora/ng-form';
const noSpacesValidator: ValidatorFn = (control) => {
if (!control.value) return null;
const hasSpaces = /\s/.test(control.value);
return hasSpaces ? { noSpaces: true } : null;
};
const schema: ZvFormSchema = [
{
field: 'username',
type: 'input',
label: 'Username',
required: true,
validators: ['required'],
validatorFns: [noSpacesValidator],
},
];Async Validators
import { AsyncValidatorFn } from '@angular/forms';
const emailAvailabilityValidator: AsyncValidatorFn = (control) => {
if (!control.value) {
return Promise.resolve(null);
}
return checkEmailAvailability(control.value)
.then(available => available ? null : { emailTaken: true });
};
const schema: ZvFormSchema = [
{
field: 'email',
type: 'email',
label: 'Email',
required: true,
asyncValidatorFns: [emailAvailabilityValidator],
},
];Conditional Fields
Show or hide fields based on the form model state:
const schema: ZvFormSchema = [
{
field: 'country',
type: 'select',
label: 'Country',
items: countries,
required: true,
},
{
field: 'state',
type: 'select',
label: 'State',
items: states,
required: true,
// Only show if country is 'IN'
showWhen: (model) => model.country === 'IN',
},
{
field: 'province',
type: 'select',
label: 'Province',
items: provinces,
// Only show if country is 'CA'
showWhen: (model) => model.country === 'CA',
},
{
field: 'terms',
type: 'checkbox',
label: 'I agree to the terms',
required: true,
},
{
field: 'referralCode',
type: 'input',
label: 'Referral Code (optional)',
// Only show if terms are agreed
showWhen: (model) => model.terms === true,
},
];Responsive Layout
Fields are arranged in a 12-column grid. Use cols to control width:
const schema: ZvFormSchema = [
// Full width (default)
{
field: 'name',
type: 'input',
label: 'Full Name',
cols: 12, // 100% width
},
// Half width
{
field: 'email',
type: 'email',
label: 'Email',
cols: 6, // 50% width
},
{
field: 'phone',
type: 'input',
label: 'Phone',
cols: 6, // 50% width
},
// One-third width
{
field: 'city',
type: 'input',
label: 'City',
cols: 4, // 33% width
},
{
field: 'state',
type: 'input',
label: 'State',
cols: 4,
},
{
field: 'pincode',
type: 'input',
label: 'Pincode',
cols: 4,
},
];Responsive behavior: On mobile (< 640px), all fields automatically span full width (cols: 12).
Complete Example
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ZvDynamicForm, ZvFormSchema } from '@zellvora/ng-form';
interface RegistrationForm {
firstName: string;
lastName: string;
email: string;
country: string;
state?: string;
agreeTerms: boolean;
newsletter: boolean;
birthDate: Date | null;
phone: string;
}
@Component({
selector: 'app-registration',
standalone: true,
imports: [CommonModule, FormsModule, ZvDynamicForm],
template: `
<div class="form-container">
<h1>User Registration</h1>
<zv-dynamic-form
[schema]="schema"
[(ngModel)]="formData"
></zv-dynamic-form>
<div class="form-actions">
<button
(click)="submit()"
[disabled]="!isFormValid()"
class="btn btn-primary"
>
Register
</button>
<button (click)="reset()" class="btn btn-secondary">
Clear
</button>
</div>
<pre *ngIf="submitted">{{ formData | json }}</pre>
</div>
`,
styles: [`
.form-container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-actions {
margin-top: 2rem;
display: flex;
gap: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: #2563eb;
color: white;
}
.btn-primary:disabled {
background: #d1d5db;
cursor: not-allowed;
}
.btn-secondary {
background: #e5e7eb;
color: #333;
}
`],
})
export class RegistrationComponent {
countries = [
{ label: 'India', value: 'IN' },
{ label: 'USA', value: 'US' },
{ label: 'Canada', value: 'CA' },
];
indianStates = [
{ label: 'Delhi', value: 'DL' },
{ label: 'Maharashtra', value: 'MH' },
{ label: 'Tamil Nadu', value: 'TN' },
];
schema: ZvFormSchema = [
{
field: 'firstName',
type: 'input',
label: 'First Name',
placeholder: 'John',
required: true,
cols: 6,
validators: ['required', 'minLength:2'],
},
{
field: 'lastName',
type: 'input',
label: 'Last Name',
placeholder: 'Doe',
required: true,
cols: 6,
validators: ['required', 'minLength:2'],
},
{
field: 'email',
type: 'email',
label: 'Email Address',
required: true,
cols: 12,
validators: ['required', 'email'],
},
{
field: 'phone',
type: 'input',
label: 'Phone Number',
cols: 6,
validators: ['phone'],
},
{
field: 'birthDate',
type: 'datepicker',
label: 'Date of Birth',
cols: 6,
},
{
field: 'country',
type: 'select',
label: 'Country',
items: this.countries,
required: true,
cols: 6,
},
{
field: 'state',
type: 'select',
label: 'State',
items: this.indianStates,
cols: 6,
showWhen: (model: any) => model.country === 'IN',
},
{
field: 'agreeTerms',
type: 'checkbox',
label: 'I agree to terms and conditions',
required: true,
cols: 12,
},
{
field: 'newsletter',
type: 'switch',
label: 'Subscribe to newsletter',
cols: 12,
},
];
formData: Partial<RegistrationForm> = {
firstName: '',
lastName: '',
email: '',
country: '',
agreeTerms: false,
newsletter: false,
phone: '',
birthDate: null,
};
submitted = signal(false);
isFormValid(): boolean {
// Add validation logic
return !!(
this.formData.firstName &&
this.formData.lastName &&
this.formData.email &&
this.formData.country &&
this.formData.agreeTerms
);
}
submit() {
if (this.isFormValid()) {
console.log('Submitting:', this.formData);
this.submitted.set(true);
}
}
reset() {
this.formData = {
firstName: '',
lastName: '',
email: '',
country: '',
agreeTerms: false,
newsletter: false,
phone: '',
birthDate: null,
};
this.submitted.set(false);
}
}Usage with Reactive Forms
ZvDynamicForm implements ControlValueAccessor, so it works with reactive forms:
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ZvDynamicForm, ZvFormSchema } from '@zellvora/ng-form';
@Component({
selector: 'app-reactive-dynamic',
standalone: true,
imports: [ReactiveFormsModule, ZvDynamicForm],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<zv-dynamic-form
[schema]="schema"
formControlName="userData"
></zv-dynamic-form>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`,
})
export class ReactiveFormComponent {
schema: ZvFormSchema = [
{ field: 'name', type: 'input', label: 'Name', required: true },
{ field: 'email', type: 'email', label: 'Email', required: true },
];
form = this.fb.group({
userData: [{ name: '', email: '' }],
});
constructor(private fb: FormBuilder) {}
submit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}API Reference
ZvDynamicForm Component
@Component({
selector: 'zv-dynamic-form',
// ...
})
export class ZvDynamicForm implements ControlValueAccessor {
@Input() schema: ZvFormSchema = [];
// Implements ControlValueAccessor
// Use with: [(ngModel)], formControlName, or ngModel binding
}ZvFormSchema
type ZvFormSchema = ZvFieldSchema[];
interface ZvFieldSchema {
type: ZvFieldType;
field: string;
label?: string;
placeholder?: string;
hint?: string;
required?: boolean;
disabled?: boolean;
items?: unknown[];
validators?: string[];
validatorFns?: ValidatorFn[];
asyncValidatorFns?: AsyncValidatorFn[];
props?: Record<string, unknown>;
cols?: number;
showWhen?: (model: any) => boolean;
}
type ZvFieldType =
| 'input' | 'email' | 'password' | 'number' | 'textarea'
| 'datepicker' | 'timepicker' | 'select' | 'multiselect'
| 'checkbox' | 'radio' | 'switch' | 'pin' | 'upload';Best Practices
- Type your form model — Define an interface for your form data.
- Organize schema logically — Group related fields together.
- Use conditional visibility — Hide fields instead of removing them from the schema.
- Validate early — Use built-in validators, not custom form validation.
- Set reasonable defaults — Initialize
formDatawith sensible defaults. - Use responsive cols — Design for mobile first (cols: 12), then wider screens.
- Handle errors gracefully — Show validation feedback immediately.
Styling
The form uses a 12-column grid. Customize via CSS:
:root {
--zv-primary: #2563eb;
--zv-border: #d1d5db;
--zv-bg: #ffffff;
--zv-error: #dc2626;
--zv-radius: 0.5rem;
}
.zv-dyn-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
@media (max-width: 640px) {
.zv-dyn-cell {
grid-column: span 12 !important;
}
}Browser Support
- Angular 21+
- Chrome, Firefox, Safari, Edge (latest 2 versions)
License
MIT
