Form
Fields, validation that speaks up at the right moment, and a save with feedback, all built from fandry-* primitives. A form can read first and edit on demand. It is a working screen, not a screenshot: what is below runs against a fake backend with real latency and real rules.
Try it: choose Edit, clear the name or break the email and leave the field, then Save. A field you change is highlighted and gets a revert button; Reset takes back all of them. Then use the username admin, a rule only the server can check.
Install
Link to section InstallLWR / LWC OSS
npm install fandryui <!-- nothing to copy: your bundler ships only what you use --> <fandry-form></fandry-form>
Salesforce DX
npx fandry add form # adds the block and everything it is built from # to your package directory, as source you own
Usage
Link to section Usage<fandry-form
fields={fields}
values={profile}
mode="read"
save-values={saveValues}
validate={validate}
onsave={handleSave}
></fandry-form>
A field is plain data. Its type picks the control, and its rules are functions:
fields = [
{ name: 'name', label: 'Name', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true,
validate: (value) => (/^\S+@\S+\.\S+$/.test(value) ? undefined : 'Enter a valid email address.') },
{ name: 'username', label: 'Username', required: true, helpText: 'At least 3 characters.',
validate: (value) => (value.length < 3 ? 'Use at least 3 characters.' : undefined) },
{ name: 'plan', label: 'Plan', type: 'radio', options: PLAN_OPTIONS },
{ name: 'bio', label: 'Bio', type: 'textarea' },
{ name: 'newsletter', label: 'Send me the newsletter', type: 'switch' },
{ name: 'memberSince', label: 'Member since', readonly: true }
];
The block owns the workflow (when errors show, the saving state, read and edit); you own the request and the data:
// Rules that need more than one field, or the server. Runs once every
// field's own rules pass; return { [field name]: message }.
validate = async (values) => {
const taken = await api.checkUsername(values.username);
return taken ? { username: taken } : undefined;
};
// The request. `changes` holds only what differs from `values`. Resolve with
// the saved values, or throw to stay in edit mode and show the message.
saveValues = (values, changes) => api.save(changes);
// The block never edits `values` itself -- put the result back.
handleSave(event) {
this.profile = event.detail.values;
}
Props
Link to section Props| Prop | Type | Default | Description |
|---|---|---|---|
fields | field[] | [] | What the form asks for, as plain data (see the field table below). |
values | object | {} | The saved values. Controlled: the block never changes it. What the user types is a draft over it. |
mode | 'read' | 'edit' | 'edit' | Whether the form shows controls or the values as text. Set it to choose where the form starts; the block keeps it current as the user moves between the two. |
save-values | (values, changes) => Promise | — | Persists the form. values is everything with the edits applied, changes only what differs. Resolve with the saved values or nothing; reject to stay in edit mode and show the message. |
validate | (values) => errors | Promise | — | Rules that span fields or need the server. Return { [field name]: message }. Runs on save, once every field's own rules pass. |
messages | object | {} | Everything the form says (buttons, statuses, "is required", the name of the Revert button, Yes / No in read mode), e.g. to translate it: any key of DEFAULT_FORM_MESSAGES (fandry/formState). Keys you leave out keep their English default. |
edit() · save() · cancel() · reset() | methods | — | What the default buttons call. Use them from your own buttons in the actions slot. reset() discards every edit and stays in edit mode; cancel() also goes back to reading. |
Field
Link to section Field| Key | Type | Description |
|---|---|---|
name | string | The key in the form's values. |
label | string | Shown above the control, and beside the value in read mode. |
type | text · email · number · tel · url · password · date · textarea · select · radio · checkbox · switch | Defaults to text. number fields save as numbers; checkbox and switch as booleans. |
options | { label, value }[] | For select and radio. |
required | boolean | Must have a value. For a checkbox or switch, must be on. |
readonly | boolean | Shown as text even while the form is being edited, and never validated. |
helpText | string | Shown under the control. |
placeholder | string | For text inputs, textareas and selects. |
elementProps | object | Spread onto the native control ({ autocomplete: 'email', maxLength: 80 }). |
validate | (value, values) => string | void | Return a message when the value is not acceptable. Runs after required, and not at all on an empty field that is not required. |
Events
Link to section Events| Event | detail | Description |
|---|---|---|
change | { name, value, values } | A field's value changed (on every keystroke), or its Revert button put the saved value back. value is in its saved shape: a number for a number field. |
save | { values, changes } | A save succeeded. Put values into your data. |
cancel | The user cancelled; their edits are discarded. | |
reset | Every edit was discarded and the form is still being edited. | |
modechange | { mode } | The form moved between reading and editing. |
Slots
Link to section Slots| Slot | Description |
|---|---|
actions | Replaces the default Edit / Save / Cancel / Reset buttons. Wire your own to edit(), save(), cancel() and reset(). |
Parts
Link to section Parts| Part | Description |
|---|---|
field | One field of a form: its label, control, help text and error (a fandry-form-field's base). |
revert | The button that puts back a changed field's saved value. |
field changed | A form field whose value differs from the saved one. The tinted panel behind it, with the bar at its start, is its ::before. |
/* Your own color for a changed field, and no bar */
fandry-form::part(field changed)::before {
background: hsl(199 89% 48% / 0.12);
border-inline-start-width: 0;
}
Make it yours
Link to section Make it yoursThe block is three pieces on purpose. fandry/formState has all the behavior and no template; fandry/form is that class plus the layout and actions you see here; fandry/formField is one field, which you can also use on its own. To change the markup, extend the state class and write your own template, or, on Salesforce, just edit the copy fandry add put in your project. Built from:inputtextareaselectradio-groupcheckboxswitchbuttoniconalertspinnertext