Skip to content

Vue FormsA lightweight, reactive data entry forms library for Vue.js

Manage form data and state without dictating your UI components

@dynamicforms/vue-forms

A lightweight, reactive data entry forms library for Vue.js that handles form state management without dictating your UI components.

Introduction

@dynamicforms/vue-forms manages form data, validation and state, and leaves rendering entirely to you. Every member of a field, group or list is a tracked read, so a template that reads field.value re-renders when something writes it — there is no ref to unwrap and no computed mirror to keep in sync.

What it models beyond state is the behaviour between fields. Visibility, enablement and values are declared as conditions over other fields, and every change travels through an action pipeline in which each handler decides whether to pass the event on and may reshape its result. Groups and lists compose recursively, so the same mechanism applies at every level of a nested form.

Every operation is a transaction

A change is announced once, when the operation carrying it finishes, over the net difference. Several writes wrapped in a transaction() announce as one, and a handler that throws rolls the whole operation back rather than leaving the form half-applied:

typescript
import { transaction } from '@dynamicforms/vue-forms';

transaction(() => {
  personForm.fields.firstName.value = 'Jane';
  personForm.fields.lastName.value = 'Novak';
});
// one ValueChangedAction on the form, not two — and none at all if either write throws

tx.rollback() unwinds a transaction without an error, which is what lets an edit be applied, measured and withdrawn. See Transactions.

Lists hold thousands of rows

Writing one field of one row costs that row and the depth of the nesting it sits in; push(), insert(), remove() and pop() cost one row; reading value or valid again with nothing changed in between costs nothing, because both are cached and the write itself invalidates them. See Scale.

One declaration, one rule, every row

The Group handed to new List(template) is not a row — it is the declaration every row is built from, and a row is a binding of it. A validator or a conditional action registered on the template is one instance serving every row, and each row answers for itself: two rows disagreeing about a condition show two different verdicts, and a CompareTo compares that row's own fields. See The model.

A field carries what your UI renders it with

The library models state and leaves rendering to you, so a field has room for what the rendering needs: a label, a hint, a column width. They are declared as a second type argument, checked by the compiler, and read through extra:

typescript
interface Presentation { label: string; hint?: string }

const firstName = new Field<string, Presentation>({ value: 'Ada', label: 'First name' });
firstName.extra.label;                                // 'First name'
firstName.setExtendedValues({ label: 'Given name' }); // hint stays as it was

Reading extra is tracked like any other member, so a component rendering off it follows a change. This is what a UI layer builds on, and what a form whose shape arrives from a server puts its labels in. See Extended properties.

Interactive Demo

Below is an interactive demo of a simple person form built with @dynamicforms/vue-forms and Vuetify. Try toggling the field states and see how the form output changes:

Person Form
Form Output
{
  "firstName": "John",
  "lastName": "Doe",
  "age": 30,
  "active": true
}

Basic Usage Example

Here's a simple example of how to create and use a form with fields and groups:

typescript
import { Field, Group } from '@dynamicforms/vue-forms';

// Create a form with fields
const personForm = new Group({
  firstName: new Field({ value: 'John' }),
  lastName: new Field({ value: 'Doe' }),
  age: new Field({ value: 30 }),
  active: new Field({ value: true })
});

// Access values
console.log(personForm.value);  // { firstName: 'John', lastName: 'Doe', age: 30, active: true }

// Update a field
personForm.fields.firstName.value = 'Jane';

// Disable a field
personForm.fields.age.enabled = false;

// Form serializes only enabled fields
console.log(personForm.value);  // { firstName: 'Jane', lastName: 'Doe', active: true }

Released under the MIT License.