Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

GUIDE

Forms & validation

The Dojo NG form controls are form-associated custom elements. Put them in a native <form> and they behave like built-in inputs: they submit their values, they block submission while they are invalid, and the browser shows its own messages. You do not need a form library.

I accept the terms

Submitted values

Nothing yet. Try submitting the empty form first.
A native form with live components. Submit it empty, then fill it in. The submit handler prints the form's FormData.

Use a native form

Give each control a name, and put the controls in a <form>. On submit, read the values with FormData, as you would for built-in inputs. Without preventDefault(), the browser posts the form to its action as usual.

<form id="signup">
  <dj-text-input name="name" label="Name" required></dj-text-input>
  <dj-email-input name="email" label="Email" required></dj-email-input>
  <dj-checkbox name="terms" required>I accept the terms</dj-checkbox>
  <button type="submit">Sign up</button>
</form>

<script type="module">
  import "@dojo-ng/text-input";
  import "@dojo-ng/email-input";
  import "@dojo-ng/checkbox";

  document.getElementById("signup").addEventListener("submit", (event) => {
    event.preventDefault();   // remove this line to let the browser post the form
    const data = Object.fromEntries(new FormData(event.target));
    console.log(data);        // { name: "Ada", email: "ada@example.com", terms: "on" }
  });
</script>
  • Each control's <input> is inside its shadow root, so the control itself takes part in the form. A control with no name submits nothing.
  • A <fieldset disabled> disables the controls inside it, and disabled controls submit nothing.
  • A form reset returns a text input or text area to its value attribute, and a checkbox, switch, or radio to its checked state from the HTML.
  • After back and forward navigation, and after autofill, the browser restores each control's value.
  • For file uploads, set enctype="multipart/form-data" on the form, as for a native file input.

What each control submits

Each control adds entries to the form's FormData under its name:

ControlsWhat they submit
Text inputs, text areaThe value as a string. An empty field submits an empty string.
Select, native select, typeahead, date input, time picker, calendar, listThe value as a string. Nothing when no value is chosen.
Number input, slider, rateThe number as a string, such as "30".
Checkbox, switchTheir value (default "on") when checked. Nothing when not checked.
Radio groupThe value of the selected radio. Nothing when none is selected.
Checkbox group, chip typeaheadOne entry for each selected value, all under the same name. Read them with formData.getAll(name).
Range sliderTwo entries: name_min and name_max.
File inputThe selected File. With multiple, one entry for each file.
Color pickerThe color string, in its format.
Rich textThe document, in its format (HTML by default).

The search box is not form-associated, because a search usually runs without submitting a form. Read its query property instead.

Built-in rules

The text inputs accept the same rules as a native <input>: required, pattern, min, max, step, minlength, and maxlength. type="email" and type="url" check the format. dj-email-input, dj-number-input, and dj-password-input are text inputs with the type already set.

<dj-text-input name="zip" label="ZIP code" required pattern="[0-9]{5}"></dj-text-input>
<dj-number-input name="seats" label="Seats" min="1" max="8"></dj-number-input>
<dj-password-input name="password" label="Password" required minlength="12"></dj-password-input>
<dj-text-input name="site" label="Website" type="url"></dj-text-input>
  • required also works on the checkbox, checkbox group, radio group, select, native select, typeahead, date input, time picker, file input, and text area. A required checkbox group needs at least one option checked.
  • The browser checks minlength and maxlength only after the user edits the field. A value set from code is not checked against them.
  • An invalid control blocks the form's submit event, so your handler only runs with valid values.
  • The messages come from the browser, in the browser's language, not the page's. For your own wording, use a custom rule.

Custom rules

dj-constrained-input is a text input with a validator property. The function receives the value and returns an error message, or undefined when the value is valid. The message takes part in form validation like a built-in rule, so an invalid value blocks the submit.

<dj-constrained-input id="code" name="code" label="Team code"
  helper-text="Three capital letters, such as ABC."></dj-constrained-input>

<script type="module">
  import "@dojo-ng/constrained-input";

  // Return a message when the value breaks the rule. Return undefined when it is valid.
  document.getElementById("code").validator = (value) =>
    value && !/^[A-Z]{3}$/.test(value) ? "Use three capital letters." : undefined;
</script>
  • The rule runs each time the value changes, and the message replaces the helper text while the field is invalid.
  • Set validator from JavaScript. A function cannot be an attribute.
  • The function runs on every keystroke, so keep it fast. Check slow rules, such as "is this name taken?", on the server after submit.
  • dj-password-input is built on the same element, so it also accepts a validator.

For an error that only the server can find, call setCustomValidity() on any text input or text area, as on a native input. A non-empty message makes the field invalid until you clear it with an empty string:

const user = form.elements.namedItem("user");

// The server says the name is taken: show it on the field.
user.setCustomValidity("That name is taken.");
user.reportValidity();

// Clear it when the user changes the value.
user.addEventListener("input", () => user.setCustomValidity(""), { once: true });

When errors show

A field that the user has not touched is not marked as an error. A control shows its invalid state in one of two cases:

  • The user edits the field and leaves it with an invalid value. A text input then turns red and shows the message under the field.
  • The user submits the form. The browser shows its message on the first invalid control, and every invalid control is marked as user-invalid. A text input or text area also turns red and shows its message.

A form reset clears the user-invalid marks.

Style the invalid state

Page CSS cannot reach the <input> inside a shadow root, so :invalid does not work on these controls. Each control mirrors its state onto its own element instead, as attributes: data-dj-required, data-dj-valid, data-dj-invalid, data-dj-user-valid, and data-dj-user-invalid. Style the user- pair, so that untouched fields stay quiet.

/* A red outline on any control the user has left invalid */
form [data-dj-user-invalid] {
  outline: 2px solid var(--dj-color-danger-600);
  outline-offset: 2px;
  border-radius: 4px;
}

/* The same with custom states, in browsers that support them */
dj-checkbox:state(user-invalid) {
  color: var(--dj-color-danger-600);
}

The theming guide shows more, such as a hint that appears next to an invalid field.

Check validity from code

The controls have the same validity API as a native input, and the form's methods include them:

const form = document.getElementById("signup");

form.checkValidity();     // false while any control is invalid; marks them as user-invalid
form.reportValidity();    // the same, and the browser shows its message on the first one

const email = form.elements.namedItem("email");
email.validity.valueMissing;   // true when the required field is empty
email.validationMessage;       // the browser's message, in the browser's language

Labels, help text, and required marks

  • label sets the visible label and the accessible name. With label-hidden, the label is hidden on screen and still read by screen readers.
  • helper-text shows a hint under a text input. While the field shows as invalid, the error message takes its place.
  • When it is required, a text input, text area, select, native select, checkbox, checkbox group, or radio group marks its label with an asterisk.
  • For the checkbox, switch, and radio, the text in the default slot is the label.

dj-form

dj-form lays out its fields in a row, or in a column with column. Its submit() method emits dj-submit with a { name: value } object, and Enter in a field submits too.

  • submit() checks the fields first. While one is invalid, the browser shows its message, dj-submit is not emitted, and submit() returns false. Set novalidate to skip the check.
  • The values follow the same rules as a native form: an unchecked checkbox or switch and disabled controls are left out, and a name used by several checked controls gives an array.
  • Enter in a text area adds a new line. It does not submit.
  • Use a native form instead when you need to post to a URL or want FormData. A native form can sit inside dj-form for its layout.

These rules need @dojo-ng/form 0.1.2 or later.

<dj-form column>
  <dj-text-input name="first" label="First name"></dj-text-input>
  <dj-text-input name="last" label="Last name" required></dj-text-input>
</dj-form>

<script type="module">
  import "@dojo-ng/form";
  import "@dojo-ng/text-input";

  document.querySelector("dj-form").addEventListener("dj-submit", (event) => {
    console.log(event.detail.data);   // { first: "…", last: "…" }, only when valid
  });
</script>
Next: The form controls in the API reference Every property, event, and part of each form control.