Getting started
Install
npm install declarative-formsImport the stylesheet once, anywhere in your app:
import 'declarative-forms/styles.css';That is the default look, and it follows the operating system's light/dark setting on its own. The v1 look is still shipped as declarative-forms/classic.css.
The package contains an ESM build and a CJS build, both with TypeScript declarations, and it has no runtime dependencies.
No build step needed
You can also load the ESM build directly in a page with <script type="module">. The <dl-select> component adds its own structural CSS the first time it is used, so the stylesheet above is the only one you have to load.
Your first form
Read Asking for data first
It is two minutes, and it explains the one thing that makes this library different from every other form library: a form here is a function you call, not a component you mount. Most applications only ever need the nine-line ask helper on that page. This page is the object API underneath it.
A form is an array of field descriptors, plus what should happen when the form closes.
{
fields: [
{ name: 'name', displayName: 'Name' },
{
name: 'role',
kind: 'select',
displayName: 'Role',
options: ['Admin', 'Editor', 'Viewer'],
defaultValue: 'Editor',
},
],
onConfirm: (values) => console.log('submitted', values),
onCancel: () => console.log('cancelled'),
}Values
—
In your own code, you pass that object to the constructor:
import { DeclarativeForm } from 'declarative-forms';
const form = new DeclarativeForm({/* … */});
form.openInModal();Three things are worth noticing:
nameis the key. The value appears under this name ingetValues(), so it must be unique within one form.kindchooses the field type. Leave it out and you get a single-line text input. See Field kinds.onCanceldecides whether the dialog can be closed without finishing it. If you pass it, the dialog gets a close button and reacts to Escape. If you leave it out, the user can only complete the dialog, not abandon it.
Reading values
form.getValues();
// { name: 'Ada', role: 'Editor', activeTab: undefined }getValues() returns a plain object whose keys are the field names. Two rules to remember:
- A field hidden by
isActiveis left out completely. It is not set to an empty value. - The key
activeTabis always there. It holds the currently selected tab, orundefinedif the form has no tabs.
To follow the values as the user types, subscribe to them:
const unsubscribe = form.subscribeOnInput((values) => {
console.log(values);
});Waiting for setup
Options and default values may be loaded asynchronously, so the form is not completely filled in at the moment the constructor returns. whenReady() resolves once the first options have loaded and the default values have been applied:
const form = new DeclarativeForm({/* … */});
await form.whenReady();
form.getValues(); // defaults are in placeYou do not need this in order to show a form. You can call openInModal() straight away, and each field fills itself in as its data arrives. You need whenReady() when you want to read the values in code right after construction, or in a test.
Embedding instead of a modal
Not every form has to be a dialog. appendInElement renders the same form inside an element of your page:
form.appendInElement(document.querySelector('#panel'));The outer wrapper gets a noModalDialog class and there is no backdrop. Confirming the form runs your callback but does not remove the form from the page. See Modals & stacking.
Controlling a single field from code
Sometimes you need to control one field from outside the form: to set a value after a lookup, or to show a loading state while you fetch something.
const field = form.field('role');
field?.setValue('Admin');
field?.getValue();
field?.focus();
field?.setLoading(true);
field?.element; // the underlying <input> / <dl-select> / …form.field(name) returns undefined if no field has that name, so use optional chaining (?.) or check the result first.
TypeScript
The descriptor types form a discriminated union on kind. The compiler therefore knows which options each kind accepts, and rejects the others:
import type { FieldDescriptor } from 'declarative-forms';
const fields: FieldDescriptor[] = [
{ name: 'note', kind: 'textarea', allowNewlines: true },
// Error: 'allowNewlines' does not exist on a select field.
// { name: 'lang', kind: 'select', options: [], allowNewlines: true },
];Next
- Asking for data — wrap all of the above in one
await - Field kinds — what you can put in
fields - Reactivity — fields that depend on other fields
- Buttons — replacing the default OK button