Fandry UI
Block

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.

Simulate server errors

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 Install

LWR / 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
PropTypeDefaultDescription
fieldsfield[][]What the form asks for, as plain data (see the field table below).
valuesobject{}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.
messagesobject{}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
KeyTypeDescription
namestringThe key in the form's values.
labelstringShown above the control, and beside the value in read mode.
typetext · email · number · tel · url · password · date · textarea · select · radio · checkbox · switchDefaults to text. number fields save as numbers; checkbox and switch as booleans.
options{ label, value }[]For select and radio.
requiredbooleanMust have a value. For a checkbox or switch, must be on.
readonlybooleanShown as text even while the form is being edited, and never validated.
helpTextstringShown under the control.
placeholderstringFor text inputs, textareas and selects.
elementPropsobjectSpread onto the native control ({ autocomplete: 'email', maxLength: 80 }).
validate(value, values) => string | voidReturn 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
EventdetailDescription
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.
cancelThe user cancelled; their edits are discarded.
resetEvery edit was discarded and the form is still being edited.
modechange{ mode }The form moved between reading and editing.

Slots

Link to section Slots
SlotDescription
actionsReplaces the default Edit / Save / Cancel / Reset buttons. Wire your own to edit(), save(), cancel() and reset().

Parts

Link to section Parts
PartDescription
fieldOne field of a form: its label, control, help text and error (a fandry-form-field's base).
revertThe button that puts back a changed field's saved value.
field changedA 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 yours

The 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