Blog

Maintaining user-interactions

Written by Loris , December 13, 2019

Forms are potentially the most UI-sensitive part of any application. They are essentially the only way users are interacting with our technology yet they are often taken for granted.

Caring about the design, the ease-of-use and browser-accessibility of your forms is important but what developers often forget is to create an environment that facilitates maintaining these forms.

Forms are everywhere — in your business workflows, in your authentication gateway, in your confirmation modals, everywhere. Thus, If you don’t make it easy for yourself to maintain them, the next time you realise something is not working for your users, it will take you a significant amount of time to ship a different solution. Users might not have the patience to wait that long.

Therefore, to keep your users happy and increase the response time of your tech force, let’s have a look at two approaches we use in EquipmentConnect to facilitate the management of our user interactions.

Base components

The first one is meant to provide a layer of abstraction on how all the fields look and interact with the user.

Instead of using plain HTML and CSS in your forms that gets copy/pasted everywhere, we use our JavaScript framework (Vue.js) to create one component for each field (e.g. short inputs, big text inputs, checkboxes, dropdown selections, file uploads, etc.).

Let’s have a look at an example. The following code represents a typical “input” field that gathers a relatively short amount of text (e.g. your email on a registration form).

<div class="form-group row">
    <label class="col-md-4 col-form-label">Trading Name</label>
    <div class="col-md-8">
        <input type="text" name="companyName" class="form-control">
        <div class="error-hint">
            Trading name is required
        </div>
    <div>
</div>

This is what that field looks like on the browser:

Now let’s encapsulate this in a new BaseInput.vue component.

<template>
    <div class="form-group row">
        <label class="col-md-4 col-form-label" v-text="label"></label>
        <div class="col-md-8">
            <input :type="type" :name="name" class="form-control">
            <div v-if="error" class="error-hint" :error="error"></div>
        <div>
    </div>
</template>

<script>
export default {
    props: {
        label: String,
        name: String,
        error: String,
        type: {
            type: String,
            default: 'text'
        }
    }
}
</script>

With that component created, we can now create a new input field very easily like this:

<base-input
    label="Trading Name"
    name="companyName"
    error="Trading name is required"
></base-input>

Note that it is a known convention to use the prefix “Base” for base components to prevent any conflict with native HTML tags like input.

Whilst this was a simple example, the possibilities related to this encapsulation are endless. Let say you’ve realised that your users would like to be guided when filling forms by having a hover-able question mark next to some field that provides some helping text. Something like that:

All you have to do is open your BaseInput component and update the HTML/CSS so that a hover-able question mark is displayed if and only if the help attribute was provided.

Next update the fields that need guidance like so:

<base-input
    label="Companies House Name"
    name="companiesHouseName"
    error="Companies House Name is required"
    help="Enter the name of your company as registered on companies house"
></base-input>

Now make sure you do this with every single type of field present in your application and you’ll have a very solid base to maintain your forms.

Renderless Form wrapper

Now that we’ve encapsulated the look-and-feel of our forms, let’s have a go at encapsulating the logic of its execution.

At EquipmentConnect, when a user submits a form:

  • It starts by making sure the data provided is valid and shows an error next to the relevant field if it’s not. (Client-side validation)
  • It then makes an asynchronous request to our API which shows a “loading” state to let the users know the form is being processed.
  • If that API returns some errors, we again make sure to display those errors next to the relevant fields. (Server-side validation)
  • It removes the loading process and sends a successful message if everything went through properly.

Now, if you had to copy/paste that complex logic for every form, it would be an absolute nightmare to manage. So let’s see how we can create a new layer of abstraction to make it easy for ourselves.

Let’s create a new “renderless” component called FormWrapper. It’s called renderless because it will not display any HTML/CSS but only provide logic to its child components.

That means this…

<form-wrapper>
    <form>
        <base-input v-model="firstName"></base-input>
        <base-input v-model="lastName"></base-input>
        <base-button @click="updateProfile">Update</base-button>
    </form>
</form-wrapper>

… is visually identical to this:

<form>
    <base-input v-model="firstName"></base-input>
    <base-input v-model="lastName"></base-input>
    <base-button @click="updateProfile">Update</base-button>
</form>

However, the FormWrapper component will host all of that complex logic we mentioned earlier: loading state, data validation, etc.

You can see the FormWrapper as some kind of function that takes some input and provide output.

Input

As an input, it will take a variable called fields that defines the scheme of our fields for that particular form. For example, let’s say the user must enter its last name and can optionally enter its first name but if it does it must be at least 2 characters long. What we’ve described defines the following fields variable:

{
    firstName: {
        default: '',
        rules: [{
            name: 'minLength'
            arguments: [2],
            error: 'Your first name must be at least 2 characters long.'
        }]
    },
    lastName: {
        default: '',
        rules: [{
            name: 'required'
            error: 'You must enter your last name.'
        }]
    }
}

Output

With that fields variable, our complex logic has enough data to succeed. It knows which fields to initialise, which validation rules to check for and which errors to display for each of them.

At last, we can use our encapsulated form logic like so:

<form-wrapper 
    :fields="fields" 
    @submit="updateProfile" 
    v-slot="{ formData, formErrrors, submit }"
>
    <form>
        <base-input
            v-model="formData.firstName"
            :error="formErrrors.firstName"
        ></base-input>
        <base-input 
            v-model="formData.lastName"
            :error="formErrrors.lastName"
        ></base-input>
        <base-button @click="submit">Update</base-button>
    </form>
</form-wrapper>

Conclusion

The important thing to understand here is how we’ve managed to abstract both the design and the logic of our forms in various components that can easily interact with each other whilst having their own separate responsibilities.

How exactly that logic behaves in the FormWrapper component will vary based on your application and deserves a tutorial on its own but creating Single-Point of Failures (SPOF) when managing software applications (like we have done today for forms) is crucial to its long-term survival.

Pin It on Pinterest