adaptform
v2.0.5
Published
Library of adaptive forms of validation
Maintainers
Readme
AdaptForm
AdaptForm is a TypeScript-first form modeling framework inspired by Django Forms and Pydantic.
Instead of describing validation schemas, you describe forms as classes with typed fields. Each field is responsible for its own state lifecycle, validation, transformation pipeline, and serialization logic.
AdaptForm provides:
- Typed form models
- Field-level validation
- Cross-field validation
- Input transformation plugins
- Nested forms and arrays
- Automatic TypeScript inference
- Framework-agnostic integration
Features
- Form-as-a-domain-model architecture
- Field-level state + validation ownership
- Cross-field validation support
- Plugin-based transformation pipeline
- Strong TypeScript inference
- Nested forms & collections
- Built-in serialization (JSON / FormData)
- Optional date engine (Luxon)
- Framework-agnostic design
- Zero-core-dependencies
Philosophy
Most form libraries treat forms as validation schemas:
const schema = z.object({
login: z.string(),
password: z.string(),
})AdaptForm treats forms as stateful domain models:
class LoginForm extends Form {
login = new Field(StringType)
password = new Field(StringType)
}field is not just a validator — it is a state container that owns:
- value
- validation
- errors
- transformation
- plugins
- type casting
The form itself becomes a single source of truth.
Simple Vue:
Without AdaptForm
A typical Vue form consists of multiple disconnected parts:
- reactive state
- validation schema
- error store
- input masks
- submission mapping
With AdaptForm
Everything is unified into a single form model:
class RegistrationForm extends Form {
phone = new Field(StringType)
}Installation
npm install adaptformOptional: Luxon integration
npm install luxonQuick Start
import {Form, Field, StringType, NumberType} from 'adaptform';
class LoginForm extends Form {
login = new Field(StringType, null, {maxLength: 50});
password = new Field(StringType, null, {minLength: 8});
remember = new Field(BooleanType, false);
}
const form = new LoginForm();
form.fields = {
login: 'john_doe',
password: 'securepass123'
};
console.log(form.isValid); // true
console.log(form.fieldsValue); // { login: 'john_doe', password: 'securepass123', remember: false }Usage Examples
Field Types
import {Field, StringType, NumberType, BooleanType, DecimalType, DateType} from 'adaptform';
// String with validation
const name = new Field(StringType, null, {
minLength: 2,
maxLength: 50,
pattern: /^[a-zA-Z]+$/
});
// Number with range
const age = new Field(NumberType, null, {
gt: 0, // greater than
lt: 120 // less than
});
// Boolean with requirement
const agreed = new Field(BooleanType, false, {
mustBeTrue: true,
messages: {
mustBeTrue: 'You must agree to the terms'
}
});Custom Validation
const email = new Field(StringType, null, {
validate: (value) => {
return value.includes('@') || 'Invalid email format';
}
});
// Async validation with form context
const passwordConfirmation = new Field(StringType, null, {
validate: (value, form) => {
return value === form?.password?.valueClear || 'Passwords do not match';
}
});Conditional Required Fields
class SignUpForm extends Form {
country = new Field(StringType, null);
state = new Field(StringType, null, {
isRequired: (form) => form?.country?.valueClear === 'USA',
messages: {
isRequired: 'State is required for USA residents'
}
});
}Input Masks
import {Field, StringType, MaskPlugin} from 'adaptform';
const phone = new Field(StringType, null, {
plugins: [
new MaskPlugin({
maskFormat: '+7 (___) ___-__-__',
maskPlaceholder: '_',
digitPattern: /\d/
})
]
});
phone.rawValue = '9991234567';
console.log(phone.valueClear); // '+7 (999) 123-45-67'Nested Forms
import {ArrayField, Field, NumberType} from 'adaptform';
const gameIds = new ArrayField(
(value?: number) => new Field(NumberType, value, {gt: 0}),
{minItems: 1}
);
gameIds.itemsValue = [1, 2, 3];
gameIds.add(4);
console.log(gameIds.itemsValue); // [1, 2, 3, 4]Luxon Date Integration
npm install luxonimport {DateLuxonType} from 'adaptform/luxon';
import {DateTime} from 'luxon';
const birthdate = new Field(DateLuxonType, null, {
isDatetime: false,
minDate: DateTime.fromISO('1900-01-01'),
maxDate: DateTime.now(),
inputFormat: 'dd.MM.yyyy',
messages: {
minDate: 'Date must be after 1900',
maxDate: 'Date cannot be in the future'
}
});
birthdate.rawValue = '15.06.1990';
console.log(birthdate.valueClear); // Luxon DateTime objectForm Submission
class ContactForm extends Form {
name = new Field(StringType, null, {maxLength: 100});
email = new Field(StringType, null, {
pattern: /^[^@]+@[^@.]+\.[^@.]+$/
});
message = new Field(StringType, null, {maxLength: 1000});
}
const form = new ContactForm();
form.fields = {
name: 'John',
email: '[email protected]',
message: 'Hello!'
};
if (form.checkValid()) {
// Submit form
const formData = form.toFormData();
const json = form.toJSON();
await fetch('/api/contact', {
method: 'POST',
body: formData
});
} else {
console.log(form.allErrors);
// {
// name: [],
// email: [],
// message: []
// }
}Global Errors
class LoginForm extends Form {
login = new Field(StringType, null);
password = new Field(StringType, null);
}
const form = new LoginForm();
// Add server-side errors
form.errors = {
login: 'Account not found',
password: 'Invalid password'
};
// Or set global error
form.setGlobalError('auth', 'Authentication failed');Plugin System
Built-in Plugins
MaskPlugin - Input masking (phone, date, credit card)
FieldPlugin - Base plugin for custom extensions
Creating Custom Plugins
import {BasePlugin} from 'adaptform';
import type {FieldPlugin} from 'adaptform';
class UppercasePlugin extends BasePlugin implements FieldPlugin<string> {
toRawValue(value: string): string {
return value?.toUpperCase() || '';
}
toValueClear(value: string): string {
return value?.toLowerCase() || '';
}
}
const field = new Field(StringType, null, {
plugins: [new UppercasePlugin()]
});API Reference
Form
checkValid(form?)- Validate all fieldsfieldsValue- Get all field valuesallErrors- Get all validation errorsglobalErrors- Get global errorstoFormData()- Convert to FormDatatoJSON()- Convert to JSONreset()- Reset all fields
Field
rawValue- Set raw input valuevalueClear- Get cleaned/validated valueisValid- Check if field is validerror- Get first error messageerrors- Get all error messagesisTouched- Check if field was modifiedisEmpty- Check if field is emptyreset()- Reset to default value
ArrayField
add(value?)- Add new fieldremove(index)- Remove field at indexclear()- Remove all fieldsitemsValue- Get array of valuesitems- Get array of Field instances
ArrayFormField
add(data?)- Add new formremove(index)- Remove form at indexclear()- Remove all formsforms- Get array of Form instancesformsFieldsValue- Get array of form values
TypeScript Support
import {Form, Field, StringType, NumberType} from 'adaptform';
class UserForm extends Form {
name = new Field(StringType, null);
age = new Field(NumberType, null);
}
const form = new UserForm();
// Full type inference
type UserData = typeof form.fieldsValue;
// { name: string | null; age: number | null; }License
MIT © Stanislav Orlov
Links
Support
If you find this project useful, please give it a star on GitHub!
