Finstral Global

Structure of components

Elements

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

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

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

Templates are the overarching components that represent a complete page. They usually make only use of template components.

Form Validation

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.

Form Validation Pattern

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 the global.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.

Validation error messages

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 specific input 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 }}"
>

Field groups

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.

Error container

{% include "@elements/form-validation-errors/error-container/error-container.twig" with {
    id: "contact-support-subject-error",
} only %}

Updated input element

{% 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 %}

The JavaScript

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)
),

Validation message strings

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;

Colors

Fonts

Spacings