Form

Form usage

Title

if the form container has a title, the correct level of heading should be added to the form using the {‘title.heading_level’: ‘CORRECT_HEADING_LEVEL’} property, else this will default to a h2, which may not be correct in all scenarios

Lead text

If the requires leading text, the {‘form.lead’: ‘STRING_OF_LEAD_TEXT’} should contain the lead of text

Additional content

If the form requires text elements, lists, links or additional content to be placed before or after the form, the {‘form.additional_info_before’: ‘STRING_OF_RICH_TEXT_HTML’} or {‘form.additional_info_after’: ‘STRING_OF_RICH_TEXT_HTML’}, should be used. If the information is important, such as instructions, links to help pages or contacts etc, use the {‘form.additional_info_before’: ‘STRING_OF_RICH_TEXT_HTML’} value, as this will be discoverable for all users, if the content is inserted after, it appears after the submit button and some screen reader users may not be aware it is there. Only add the {‘form.additional_info_after’: ‘STRING_OF_RICH_TEXT_HTML’}, for search forms or forms with 1 or 2 inputs and if the content is not important.

Button position

If the form design has a button that is displayed adjacent to an input, the {‘form.button_inline’: true} would need to be set. This should only be used for single input forms, such as search forms

Form error

A form error is presented at the top of a form. It should be used in scenarios where a user’s input data passes validation constraints, but the data does not match records on the system. As an example, if a user attempts login and the credentials do not match, it may be helpful to direct them to some help pages or give them an email to contact the correct department e.g:

 'form': {
    'form_error': 'Your login credentails do not match, please email <a href="mailto:somebody@leeds.ac.uk">Somebody at Leeds</a>',
    'form_error_id': 'formErrorId',
    ...
 }

It is necessary to provide the {‘form_error_id’: ‘UNIQUE_ID_ON_THE_PAGE’} and when the page is sent back, the ID of the form error should be appended to the URL and the attribute tabindex=”-1” should be set to tabindex=”0”. this ensures that the message receives focus and can be consumed by the widest range of devices and assistive technologies.

Centering a form

Where a form needs centering within its container {‘form_centered’: true} should be set

Page title

When a form has errors, it is also best practice to prepend the page title with helpful error text, as an example

<title>Contact us | University of Leeds</title>
...

Would be more helpful to users if it were dynamically changed to

<title>3 errors on form submission - Contact us | University of Leeds</title>

This particularly benefits screen reader users, as the page title is the first thing read out on page load, it also benefits users that may become distracted, especially if they have multiple tabs open.

Form variants

On request specific variants can be added to our form component. These could just be configuration, but in some instances may require further development work.

Inline selects and search variant

This variant presents two drop down lists before a search box and button. On screen sizes over 768px the two dropdowns are presented horizontally alongside each other and stack to one column for smaller sizes.

This is achieved via presenting these in the config within a “form-group” and setting “inline-fields” to be true to accommodate the required layout.

An example config (with reduced data for select components) for this is presented below.


"form": {
  "heading_level": "h2",
  "form_centered": true,
  "action": "/example-form-action",
  "title": null,
  "lead": null,
  "overflow": true,
  "additional_info_before": null,
  "button": {
    "style": "primary",
    "type": "submit",
    "content": "Search"
  },

  "additional_info_after": "<p>Or <a href=\"#\">link to other site</a>.</p>",
  "form_group": {
    "inline_fields": true,
    "fields": [
      {
        'type': 'select',
        'label': 'Which subject matter does your event relate to?',
        'id': 'cheeseList',
        'name': 'selectName1',
        "hint": "Select one type",
        'options': [
          {"label": "Brie", "value": "BRI"},
          {"label": "Cashel Blue", "value": "CBL"},
        ],
      },
      {
        'type': 'select',
        'label': 'Which subject matter does your event relate to?',
        'id': 'cheeseList',
        'name': 'selectName1',
        "hint": "Select one type",
        'options': [
          {"label": "Brie", "value": "BRI"},
          {"label": "Cashel Blue", "value": "CBL"},
        ],
      },
    ],
  },
  "fields": [
    {
      "type": "search",
      "id": "inputId2",
      "name": "searchCourses2",
      "label": "Search by subject, course title or keyword",
      "invalid": "false",
      "autocomplete": "off",
      "has_icon": true
    },
  ],
  "button_inline": true
}
{% if form %}
   <div class="uol-form__container {{ 'uol-form-container--centered' if form.form_centered }} {{ 'uol-form__container--with-image' if form.img.src }} {{ 'uol-form-container--overflow' if form.overflow }}">

    <div class="uol-form__inner-wrapper">
      {% if form.title %}
        <{{ form.heading_level if form.heading_level else 'h2' }} class="uol-form__title">{{ form.title }}</{{ form.heading_level if form.heading_level else 'h2' }}>
      {% endif %}

      {% if form.lead %}
        <div class="uol-form__lead"><p>{{ form.lead | safe }}</p></div>
      {% endif %}

      {% if form.additional_info_before %}
        <div class="uol-rich-text">
          <div class="uol-form__additional-content uol-form__additional-content--before">
            {{ form.additional_info_before  | safe }}
          </div>
        </div>
      {% endif %}

      {% if form.form_error %}
          {% render '@uol-form-error-msg', { form_error: form.form_error, form_error_id: form.form_error_id } %}
      {% endif %}

      <form class="uol-form" action="{{ form.action }}"
        {% for field in form.fields %}
          {{ 'role=search' if field.type == 'search' }}
        {% endfor %}>

        <div class="uol-form__input-group  {{ 'uol-form__input-group--inline' if form.form_group.inline_fields else 'uol-form__input-group--block' }}">
          
          {% if form.form_group %}
                      
            {% for field in form.form_group.fields %}
              {% render '@uol-form-input', field %}
            {% endfor %}
          {% endif %}  
        </div>

        <div class="{{ 'uol-form--button-inline' if form.button_inline else 'uol-form--button-block' }}">

          <div class="uol-form__inputs-wrapper">

            {% block formContent %}
              {% for field in form.fields %}
                {% render '@uol-form-input', field %}
              {% endfor %}
            {% endblock %}

          </div>

          {% if form.additional_info_before_submit_button %}
            <div class="uol-rich-text">
              <div class="uol-form__additional-content">
                {{ form.additional_info_before_submit_button | safe }}
              </div>
            </div>
          {% endif %}

          {% if form.button %}
            <div class="uol-form__button-wrapper">
              {% render '@uol-button', form.button %}
            </div>
          {% endif %}
        
        </div>

      </form>

      {% if form.additional_info_after  %}
        <div class="uol-rich-text">
          <div class="uol-form__additional-content uol-form__additional-content--after">
            {{ form.additional_info_after | safe }}
          </div>
        </div>
      {% endif %}
    </div>

    {% if form.img.src %}
      <figure class="uol-form__img-wrapper">
        <img class="uol-form__img" src="{{ form.img.src }}" alt="{{ form.img.alt if form.img.alt else null }}">
      </figure>
    {% endif %}

  </div>
{% endif %}
<div class="uol-form__container   ">

    <div class="uol-form__inner-wrapper">

        <h2 class="uol-form__title">Form container</h2>

        <form class="uol-form" action="/example-form-action">

            <div class="uol-form__input-group  uol-form__input-group--block">

            </div>

            <div class="uol-form--button-block">

                <div class="uol-form__inputs-wrapper">

                </div>

            </div>

        </form>

    </div>

</div>
  • Content:
    .uol-form-container--centered {
      @extend .uol-col;
      @extend .uol-col-m-10;
      @extend .uol-col-xl-8;
    
      margin: 0 auto;
    }
    
    .uol-form__container {
      border: 1px solid $color-border--light;
      border-radius: 6px;
      margin-bottom: $spacing-6;
    
      &.uol-form-container--centered {
        padding: 0;
      }
    
      .uol-side-nav-container--populated + .uol-homepage-content & {
    
        .uol-form__inner-wrapper {
    
          @include media(">=uol-media-l") {
            flex-basis: 100%;
          }
    
          @include media(">=uol-media-xl") {
            flex-basis: 55.555%;
          }
        }
    
          .uol-form {
    
            @include media(">=uol-media-xl") {
              margin-right: $spacing-6;
            }
          }
    
        .uol-form__img-wrapper {
          display: none;
    
          @include media(">=uol-media-xl") {
            display: inline-flex;
            flex-basis: 44.444%;
          }
        }
      }
    }
    
    .uol-form__inner-wrapper {
      padding: $spacing-5 $spacing-4 $spacing-6;
      background-color: $color-grey--light;
    
      @include media(">=uol-media-l") {
        flex-basis: 58.333%;
        padding: 2.5rem $spacing-6;
      }
    
      @include media(">=uol-media-xl") {
        flex-basis: 50%;
      }
    
       /*
      Note:
      As element uses typography rich text, each paragraph element has spacing underneath
      Here, the element is at the bottom of the form so we force last paragraph element
      to have zero spacing
      */
      p:last-child {
        margin-bottom: 0 !important;
      }
    }
    
    /*
    Note:
    Fix so blue line is in correct place for form group inputs
    */
    .uol-form__input-group--inline {
      .uol-form__input-wrapper:before {
        bottom: 1px;
      }
    }
    
      .uol-form__title {
        color: $color-font;
        font-size: 2rem;
        line-height: 1.25;
        font-family: $font-family-serif;
        margin: 0;
        padding-bottom: $spacing-2;
    
        + .uol-form {
          padding-top: $spacing-2;
        }
    
        @include media(">=uol-media-m") {
          font-size: 2.25rem;
          line-height: 1.333;
        }
    
        @include media(">=uol-media-l") {
          font-size: 2.625rem;
          line-height: 1.238;
        }
      }
    
      .uol-form__lead {
        display: block;
        color: $color-font;
        font-size: 1.125rem;
        line-height: 1.556;
        font-family: $font-family-sans-serif;
        margin: 0 0 $spacing-6;
        font-weight: normal;
    
        // @include media(">=uol-media-s") {
        //   max-width: 31.5rem;
        // }
    
        @include media(">=uol-media-m") {
          max-width: 32rem;
        }
    
        @include media(">=uol-media-l") {
          font-size: 1.25rem;
          max-width: 41rem;
        }
      }
    
      .uol-form {
        flex-direction: row;
      
        .uol-form--button-inline {
          @include media(">=uol-media-m") {
            display: flex;
          }
        }
      
        .uol-form__input-group {
          display: flex;
          flex-wrap: wrap;
      
          @include media(">=uol-media-m") {
            column-gap: $spacing-4;
          }
      
          @include media(">=uol-media-l") {
            column-gap: $spacing-5;
          }
      
          @include media(">=uol-media-xl") {
            column-gap: $spacing-6;
          }
      
          .uol-form__input-container {
            width: 100%;
      
            @include media(">=uol-media-m") {
              width: calc(50% - #{$spacing-2});
            }
      
            @include media(">=uol-media-l") {
              width: calc(50% - #{$spacing-3});
            }
      
            @include media(">=uol-media-l") {
              width: calc(50% - #{$spacing-4});
            }
      
            .uol-form__input-wrapper {
              max-width: none;
            }
          }
      
        }
      
        .uol-form__input-group--inline {
          display: flex;
          flex-direction: column;
          
          @include media(">=uol-media-m") {
            flex-direction: row;
          }
        }
      }
    
    .uol-form__container--with-image {
    
      @include media(">=uol-media-l") {
        display: flex;
      }
    }
    
      .uol-form__img-wrapper {
        background-color: $color-grey--light;
        position: relative;
        display: none;
        z-index: -2;
        overflow: hidden;
    
        @include media(">=uol-media-l") {
          display: inline-flex;
          flex-basis: 41.666%;
        }
    
        @include media(">=uol-media-xl") {
          flex-basis: 50%;
        }
      }
    
        .uol-form__img {
          position: absolute;
          min-width: 100%;
          min-height: 100%;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          z-index: -1;
        }
    
        .uol-form--button-inline {
          .uol-form__inputs-wrapper {
            flex: 1;
          }
    
          .uol-form__input-container {
            margin-bottom: 0;
          }
    
          .uol-form__button-wrapper {
            align-self: flex-end;
    
            .uol-button {
              @include button_focus(-6px);
            }
    
            @include media(">=uol-media-m") {
              padding-left: $spacing-4;
            }
    
            @include media(">=uol-media-l") {
              padding-left: $spacing-5;
            }
    
            @include media(">=uol-media-xl") {
              padding-left: $spacing-6;
            }
    
            [class^="uol-button"] {
              width: 100%;
    
              @include media(">=uol-media-s") {
                width: inherit;
              }
              
              height: 3.125rem;
              line-height: 0.75;
            }
          }
        }
    
        .uol-form__button-wrapper {
          .uol-form--button-block & {
    
          @include media(">=uol-media-s") {
            display: inline-block;
            width: initial;
          }
    
            .uol-button {
              width: 100%;
            }
          }
        }
    
    .uol-form__additional-content {
      padding: 0;
      margin: 0;
    
      a {
        @include link_focus();
      }
    }
    
    .uol-form__additional-content--before {
      .uol-rich-text & {
        margin: $spacing-4 0;
    
        > * {
          margin-bottom: $spacing-4;
        }
    
        > *:last-child {
          margin-bottom: $spacing-6;
        }
      }
    }
    
    .uol-form__additional-content--after {
      .uol-rich-text & {
        margin: $spacing-6 0 0;
    
        > * {
          margin-bottom: $spacing-4;
        }
    
        > *:last-child {
          margin-bottom: 0;
        }
      }
    }
    
    // TODO: refactor this file
    
    .uol-form__inner-wrapper {
      
      .uol-form__custom-fieldset {
        // Fiddle to allow container for buttons to extend over search button
    
        @include media(">=uol-media-m") {
          width: calc(100% + 160px + 16px);
        }
    
        @include media(">=uol-media-l") {
          width: calc(100% + 160px + 24px);
        }
      }
    
      .uol-form__custom__legend {
        margin: 0 0 $spacing-3;
      }
    
      .uol-form__input-label {
        padding-bottom: $spacing-3;
      }
    
    }
  • URL: /components/raw/uol-form/_form.scss
  • Filesystem Path: src/library/02-components/form/_form.scss
  • Size: 6.2 KB
  • Content:
    import { mdiConsoleNetwork } from "@mdi/js";
    
    export const createAriaLiveRegion = () => {
      if (document.querySelector('.uol-form__container')) {
        const formContainer = document.querySelector('.uol-form__container');
    
        if (!formContainer.querySelector('.uol-form__announcement')) {
          const formAnnouncement = document.createElement('div');
    
          formAnnouncement.setAttribute('aria-live', 'polite')
          formAnnouncement.classList.add('uol-form__announcement')
          formAnnouncement.classList.add('hide-accessible')
          formContainer.appendChild(formAnnouncement);
        }
      }
    }
    
    export const preValidationChecks = () => {
      const submitBtn = document.querySelector('[type="submit"]');
      const passwordFields = document.querySelectorAll('.uol-form__input--password');
      const toggleBtn = document.querySelector('.uol-form__input--password-toggle');
      const checkboxGroups = document.querySelectorAll('[role="group"]');
    
      if (submitBtn) {
        submitBtn.addEventListener('click', () => {
    
          if (passwordFields && submitBtn) {
            passwordFields.forEach( (input) => {
              if (input.getAttribute('type') == 'text') {
                input.setAttribute('type', 'password');
                toggleBtn.setAttribute('data-password-visible', false)
              }
            })
          }
    
          if (checkboxGroups) {
            checkboxGroups.forEach( (group) => {
              if (group.hasAttribute('data-checkboxes-required')) {
                const numRequired = group.getAttribute('data-checkboxes-required');
                const totalChecked = group.querySelectorAll("input:checked").length;
    
                if (totalChecked >= numRequired) {
                  group.setAttribute('data-checkbox-group-invalid', false)
                } else {
                  group.setAttribute('data-checkbox-group-invalid', true)
                }
              }
            })
          }
        })
      }
    }
    
    /*
    This function changes the type of search input in a form based on radio button selection
    It initially loads both inputs and then either shows the first input (on load)
    Or changes the input type based on radio button selection
    */
    export const formButtonInputSwitch = () => {
    
      // Is iOS device check
    const isIOS = () => {
      return [
          'iPad Simulator',
          'iPhone Simulator',
          'iPod Simulator',
          'iPad',
          'iPhone',
          'iPod'
        ].includes(navigator.platform)
        // iPad on iOS 13 detection
        ||
        (navigator.userAgent.includes("Mac") && "ontouchend" in document)
    }
    
    const isAndroid = () => {
      const ua = navigator.userAgent.toLowerCase();
      return ua.indexOf("android") > -1;
    }
    
    const iOS = isIOS();
    const androidDevice = isAndroid();
    const firefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
    
      // If in form container
      if (document.querySelector('.uol-form__container')) {
        const customFieldSet = document.querySelector('.uol-form__custom-fieldset');
    
    
        // The following attribute set if config has "changeInputType": true
        if (customFieldSet && customFieldSet.hasAttribute("changeInputType")) {
    
          // Loop through each radio button
          document.querySelectorAll('.uol-form__input--radio').forEach((elem) => {
    
            // Add event listener on to each radio
            elem.addEventListener("change", function(event) {
    
              // Hide all inputs when a change made
              document.querySelectorAll('.uol-form__input-container--search').forEach((elem) => {
                elem.style.display = 'none';
              });
    
              /*
              in config, "changeInputTo" set to be "standard" or "singleTypeahead"
              This value set as attribute
              */
              const changeInputTo = event.target.getAttribute("changeInputTo");
              const searchLabel = event.target.getAttribute("searchlabel");
              let showInputId = event.target.getAttribute("showsearchid");
    
    
              /*
              Initial id of typeahead appended with -js-input-0
              We want to target these id and then hide the parent (closest) container
              Only run for browser which typeahead is available (not firefox android or IOS currently)
              */
              if (!iOS && !androidDevice && !firefox) {
                if (changeInputTo == "singleTypeahead") {
                  showInputId += "-js-input-0";
                }
    
    
                // Show parent container of input containing our id.
                const containerElement = document.getElementById(showInputId).closest('.uol-form__input-container--search');
                containerElement.style.display = "block";
              } else {
                // Always show search form for firefox, IOS and android
                document.querySelector('.uol-form__input-container--search').style.display = "block";
              }
    
    
              // const searchLabel = document.querySelector('.uol-form__input-label');
              // console.log("Changed" + searchLabel.innerHTML);
    
              const nodeList = document.querySelectorAll(".uol-form__input-label__text");
              for (let i = 0; i < nodeList.length; i++) {
                nodeList[i].innerHTML = searchLabel;
              }
    
    
            });
          });
    
          // initially hide all inputs apart from the first one
          document.querySelectorAll('.uol-form__input-container--search').forEach((elem, count) => {
            if (count > 0) elem.style.display = 'none';
          });
        }
      }
    }
    
  • URL: /components/raw/uol-form/form.module.js
  • Filesystem Path: src/library/02-components/form/form.module.js
  • Size: 5.3 KB
{
  "form": {
    "action": "/example-form-action",
    "title": "Form container",
    "heading_level": "h2"
  }
}