Elements are small components that are used by multiple higher order components. They are not meant to be used directly to author the content of a page.
Patterns are components that can be used by content editors or template developers to author the content of a page. They usually make use of elements.
Template components are the main parts of a template, but not usable by content editors. Instead they are used by template developers to construct templates.
Templates are the overarching components that represent a complete page. They usually make only use of template components.
When doing form validation there are a couple of useful components and patterns that can be used to keep the form validation logic clean and reusable.
When you will be validating more than a single field, the first component you must add inside your form
element is the FormValidationErrors component:
{% include "@elements/form-validation-errors/form-validation-errors.twig" %}
This reusable web component provides a way to display all the errors encountered during form validation. Each error message will also link to the field that is in error making it easy for a user to scan over all the errors and jump to the field or fieldss that needs to be corrected.
Next, consider the types of validation you will be doing to get a sense of the error message you will need when providing feedback to the user. Once you have a sense of what you will need, you can find several pre-existing localised message in web/modules/custom/finstral_string/finstral_string.string.yml
under global.form_errors
.
NOTE: Should none of the available options suit your needs, you can add a new message to the
finstral_string.string.yml
file. Consider whether this new entry could be reused. If so, consider adding it to theglobal.form_errors
section. If not, please add it to a section that is scoped to your particular component, for example:contact_form.form_errors
. Always keep the long term manitanability of this file in mind before adding a new entry and prefer reuse.
Instead of repeating the same message keys across inputs, we expose all the messages we will require using data attributes on the parent web component.
NOTE: In very rare occasions exposing these on the
form
element or a specificinput
element is acceptable, but should generally be avoided.
<finstral-create-user
class="CreateUserForm"
data-field-required="{{ "global.form_errors.js.field_required"|tc }}"
data-email-invalid="{{ "global.form_errors.email_invalid"|tc }}"
data-require-option="{{ "global.form_errors.js.require_option"|tc }}"
>
Styling for error states can be inherited by wrapping a group of elements in a Form-fieldGroup
class.
@see
web/themes/custom/finstral_global/src/css/common/utils/forms.css
<div class="Form-fieldGroup">
{% include "@elements/form-element/label/label.twig" with {
for: "contact-support-subject",
required: true,
title: "form_contact_support.subject_label"|tc
} only %}
{% include "@elements/form-element/input/input.twig" with {
id: "contact-support-subject",
name: "contact-support-subject",
required: true,
type: "text"
} only %}
</div>
Lastly you need to include the error output component and update the form element to associate the element with the message container. This ensures that the error message will be announced to screen reader users.
{% include "@elements/form-validation-errors/error-container/error-container.twig" with {
id: "contact-support-subject-error",
} only %}
{% include "@elements/form-element/input/input.twig" with {
additional_attributes: [
["aria-describedby", "contact-support-subject-error"],
],
id: "contact-support-subject",
name: "contact-support-subject",
required: true,
type: "text"
} only %}
To the #selectors
object in your JavaScript file, add the following:
formValidationErrors: "finstral-form-validation-errors",
Then, add the following to your #getElements
function:
formValidationErrors: /** @type {HTMLElement} */ (
this.querySelector(FormContactSupport.#selectors.formValidationErrors)
),
Next, set up the references to the error messages. In the base class
add the following private variable:
#messages;
Then inside the constructor
function, add the following:
this.#messages = {
fieldRequired: this.dataset.fieldRequired,
emailInvalid: this.dataset.emailInvalid,
requireOption: this.dataset.requireOption,
};
You now have references to the form validation errors component and error messages. In your validate
function add:
const { formValidationErrors } = this.#elements;
const { fieldRequired, emailInvalid, requireOption } = this.#messages;
The FormValidationErrors
component exposes a function that accepts an array of error messages so we will also need an array to store any error messages.
const fieldErrors = [];
Next, write your validation and populate the fieldErrors
array with any errors as appropriate. For example:
if (!firstNameField.checkValidity()) {
this.#updateFormErrorState(firstNameField, this.#messages.fieldRequired);
}
You can use the following handy function to do all of the heavy lifting:
/**
* Updates the form error state by setting the aria-invalid attribute on the
* specified field and adding the error message to error container.
* @param {HTMLElement} field - The form field element that has the error.
* @param {string} message - The error message.
* @private
* @returns {void}
*/
#updateFormErrorState(field, message) {
const fieldName = this.querySelector(`[for=${field.id}]`).textContent; // get the text of the label which will be translated
field.setAttribute("aria-invalid", true);
field.nextElementSibling.querySelector("span").textContent =
message.replace("{{fieldname}}", fieldName);
this.#state.fieldErrors.push({
fieldName,
id: field.id,
message,
});
}
Once all of the validation logic has been completed, we call the setValidationErrors
function of the FormValidationErrors
component. This function will update the component with the new error messages and ensure that the component is visible. It will also clear out the error messages once you pass an empty array.
formValidationErrors.setValidationErrors({
errors: this.#state.fieldErrors,
});
Lastly, we return a boolean value to indicate whether the form is valid or not.
return this.#state.fieldErrors.length ? false : true;