<form class="m-form" data-form="https://www.thecocktaildb.com/api/json/v1/1/search.php" action="" method="get">
    <meta name="form-validation" content="s=required,min:3|terms=required">

    <div role="alert" data-form-error class="m-alert m-alert--error is-hidden"></div>
    <div role="alert" data-form-success="drinks" class="m-alert m-alert--success is-hidden"></div>
    <fieldset>
        <div class="m-form-control">
            <div class="m-form__row u-m-b-2">
                <div class="field-group">
                    <input type="text" name="s" class="a-input" placeholder="Your favorite cocktail">
                </div>
            </div>
            <div class="m-form__row u-m-b-2">
                <div class="field-group">
                    <legend class="label-legend">Category</legend>
                    <div class="u-display-flex">
                        <div class="radio">
                            <input checked type="radio" id="c-cocktail" name="c" value="Cocktail" class="a-radio u-visuallyhidden">
                            <label for="c-cocktail">Cocktail</label>
                        </div>
                        <div class="radio">
                            <input type="radio" id="c-drink" name="c" value="Ordinary_Drink" class="a-radio u-visuallyhidden">
                            <label for="c-drink">Ordinary drink</label>
                        </div>
                    </div>
                </div>
            </div>
            <div class="m-form__row u-m-b-2">
                <div class="field-group">
                    <div class="checkbox">
                        <input id="newsletterTerms" type="checkbox" name="terms" value="1" class="a-checkbox u-visuallyhidden">
                        <label for="newsletterTerms"><span>Accept the terms</span></label>
                    </div>
                </div>
            </div>
            <div class="m-form__row u-m-b-2">
                <div class="field-group display-flex align-items-center">
                    <input type="file" id="file" class="a-file " data-multiple-caption="{count} filer valda">
                    <label for="file" class="a-button a-button--icon a-file-icon">
                        <span class="a-button__text a-file-text">Välj fil</span>
                        <svg class="icon a-button__icon">
                            <use xlink:href="#icon-upload"></use>
                        </svg>
                    </label>
                    <button type="button" class="a-button a-button--standalone-icon a-button--icon js-remove-file is-hidden">
                        <span class="a-button__text u-visuallyhidden">Ta bort</span>
                        <svg class="icon a-button__icon">
                            <use xlink:href="#icon-trash"></use>
                        </svg>
                    </button>

                </div>

            </div>
            <div class="m-form__row u-m-b-2">
                <style media="screen">
                    :root {
                        --m-modal-button-primary-color: ;
                        --m-modal-button-primary-hover-color: ;
                        --m-modal-button-primary-border-color: ;
                        --m-modal-button-primary-text-color: ;
                        --m-modal-button-secondary-color: ;
                        --m-modal-button-secondary-hover-color: ;
                        --m-modal-button-secondary-border-color: ;
                        --m-modal-button-secondary-text-color: ;
                    }
                </style>
                <div class="field-group disabled">
                    <label for="message">Meddelande</label>
                    <textarea rows="5" class="a-textarea" id="" placeholder="" autocomplete=""></textarea>
                </div>
            </div>
            <div class="m-form__row">
                <button type="submit" class="a-button" data-if="terms" data-if-effect="disable">
                    <span class="a-button__text">Fetch</span>
                </button>

                <button type="reset" class="a-button a-button--transparent">
                    <span class="a-button__text">Reset</span>
                </button>
            </div>
        </div>
    </fieldset>
    <script type="iis/template" id="drinks">
        <% if (drinks && drinks.length) { %>
		<% drinks.forEach(function (drink) { %><li><%- drink.strDrink %> (<%- drink.strAlcoholic %>)</li><% }); %>
		<% } else { %>
			Inga drinkar hittades. Testa en ny sökning.
			<% } %>
			</script>
</form>
<form class="m-form" data-form="https://www.thecocktaildb.com/api/json/v1/1/search.php" action="" method="get">
	<meta name="form-validation" content="s=required,min:3|terms=required">

	<div role="alert" data-form-error class="m-alert m-alert--error is-hidden"></div>
	<div role="alert" data-form-success="drinks" class="m-alert m-alert--success is-hidden"></div>
	<fieldset{{#if disabled}} disabled{{/if}}>
		<div class="m-form-control">
			<div class="m-form__row u-m-b-2">
				<div class="field-group">
					<input type="text" name="s" class="a-input" placeholder="Your favorite cocktail">
				</div>
			</div>
			<div class="m-form__row u-m-b-2">
				<div class="field-group">
					<legend class="label-legend">Category</legend>
					<div class="u-display-flex">
						<div class="radio">
							<input checked type="radio" id="c-cocktail" name="c" value="Cocktail" class="a-radio u-visuallyhidden">
							<label for="c-cocktail">Cocktail</label>
						</div>
						<div class="radio">
							<input type="radio" id="c-drink" name="c" value="Ordinary_Drink" class="a-radio u-visuallyhidden">
							<label for="c-drink">Ordinary drink</label>
						</div>
					</div>
				</div>
			</div>
			<div class="m-form__row u-m-b-2">
				<div class="field-group">
					<div class="checkbox">
						<input id="newsletterTerms" type="checkbox" name="terms" value="1" class="a-checkbox u-visuallyhidden">
						<label for="newsletterTerms"><span>Accept the terms</span></label>
					</div>
				</div>
			</div>
			<div class="m-form__row u-m-b-2">
				{{render '@file'}}
			</div>
			<div class="m-form__row u-m-b-2">
				{{> '@textarea--rich-text' disabled='true'}}
			</div>
			<div class="m-form__row">
				<button type="submit" class="a-button" data-if="terms" data-if-effect="disable">
					<span class="a-button__text">Fetch</span>
				</button>

				<button type="reset" class="a-button a-button--transparent">
					<span class="a-button__text">Reset</span>
				</button>
			</div>
		</div>
	</fieldset>
	<script type="iis/template" id="drinks">
	<% if (drinks && drinks.length) { %>
		<% drinks.forEach(function (drink) { %><li><%- drink.strDrink %> (<%- drink.strAlcoholic %>)</li><% }); %>
		<% } else { %>
			Inga drinkar hittades. Testa en ny sökning.
			<% } %>
			</script>
		</form>
{
  "disabled": false
}
  • Content:
    import template from 'lodash.template';
    import Events from '../../assets/js/Events';
    import Button from '../../atoms/button/Button';
    import className from '../../assets/js/className';
    import validationMessage from '../../assets/js/validationMessage';
    import request from '../../assets/js/request';
    
    export default class Form {
    	constructor(element, i18n = null) {
    		this.element = element;
    		this.submit = new Button(this.element.querySelector('button[type="submit"]'));
    		this.error = this.element.querySelector('[data-form-error]');
    		this.success = this.element.querySelector('[data-form-success]');
    		this.events = new Events();
    
    		if (this.success) {
    			const tpl = document.getElementById(this.success.getAttribute('data-form-success'));
    			this.successMessage = (tpl) ? tpl.innerHTML : '';
    		}
    
    		this.validation = this.element.querySelector('meta[name="form-validation"]');
    		this.i18n = i18n;
    
    		if (!this.i18n) {
    			this.i18n = (str) => str;
    		}
    
    		this.data = {};
    		this.errors = {};
    		this.validationRules = null;
    
    		if (this.validation) {
    			this.parseValidationRules();
    		}
    
    		this.collectInputs();
    		this.attach();
    	}
    
    	collectInputs() {
    		this.inputs = this.element.querySelectorAll('input');
    	}
    
    	parseValidationRules() {
    		const validationsStr = this.validation.getAttribute('content');
    		const validations = validationsStr.split('|');
    
    		if (!validations.length) {
    			return;
    		}
    
    		this.validationRules = {};
    
    		validations.forEach((validation) => {
    			const [name, rulesStr] = validation.split('=');
    
    			this.validationRules[name] = rulesStr.split(',');
    		});
    	}
    
    	attach() {
    		this.element.addEventListener('submit', this.onSubmit);
    		this.element.addEventListener('reset', this.reset);
    	}
    
    	updateData() {
    		const data = { ...this.data };
    
    		this.collectInputs();
    		this.inputs.forEach((input) => {
    			const name = input.getAttribute('name');
    			const { value } = input;
    			const type = input.getAttribute('type');
    
    			if ((type === 'radio' && input.checked) || (type === 'checkbox' && input.checked) || (type !== 'checkbox' && type !== 'radio' && value && value.length)) {
    				data[name] = value;
    			} else if (type === 'checkbox' && !input.checked) {
    				delete data[name];
    			}
    		});
    
    		this.data = data;
    	}
    
    	validateRule(value, rule, field) {
    		const [ruleName, ruleData] = rule.split(':');
    
    		switch (ruleName) {
    			case 'required': {
    				return field in this.data;
    			}
    			case 'min': {
    				return !value || value.length >= parseInt(ruleData, 10);
    			}
    			default: {
    				return true;
    			}
    		}
    	}
    
    	validate() {
    		this.errors = {};
    
    		if (!this.validationRules) {
    			return true;
    		}
    
    		Object.entries(this.validationRules).forEach(([field, rules]) => {
    			rules.forEach((rule) => {
    				if (!this.validateRule(this.data[field], rule, field)) {
    					if (!(field in this.errors)) {
    						this.errors[field] = [];
    					}
    
    					this.errors[field].push(rule);
    				}
    			});
    		});
    
    		return Object.keys(this.errors).length < 1;
    	}
    
    	onSubmit = (e) => {
    		e.preventDefault();
    
    		this.updateData();
    		this.clearFieldErrors();
    
    		if (this.validate()) {
    			this.send();
    		} else {
    			this.displayError({ message: this.i18n('Alla fält måste vara ifyllda') });
    		}
    	};
    
    	setLoading(loading) {
    		if (loading) {
    			this.inputs.forEach((input) => { input.disabled = true; });
    			this.submit.start();
    			this.element.classList.add('is-loading');
    		} else {
    			this.inputs.forEach((input) => { input.disabled = false; });
    			this.submit.stop();
    			this.element.classList.remove('is-loading');
    		}
    	}
    
    	displayError = (error) => {
    		this.events.emit('error', error);
    		this.setLoading(false);
    
    		if ('response' in error) {
    			const { response } = error;
    
    			if ('data' in response && response.data && response.data.errors) {
    				this.errors = response.data.errors;
    			}
    		}
    
    		if (Object.keys(this.errors).length) {
    			Object.entries(this.errors).forEach(this.displayFieldError);
    
    			return;
    		}
    
    		const message = ('response' in error) ? error.response.message : error.statusText;
    
    		this.error.classList.remove('is-hidden');
    		this.error.innerHTML = message || this.i18n('Något gick fel');
    	};
    
    	displayFieldError = ([name, error]) => {
    		const input = this.element.querySelector(`[name="${name}"]`);
    
    		if (!input) {
    			return;
    		}
    
    		let id = input.getAttribute('aria-describedby');
    
    		if (!id) {
    			id = `${name}-help`;
    			input.setAttribute('aria-describedby', id);
    		}
    
    		let help = document.getElementById(id);
    
    		if (!help) {
    			help = document.createElement('div');
    			help.id = id;
    			help.className = className('input-help');
    
    			const fieldGroup = input.closest('[class*="field-group"]');
    
    			if (fieldGroup) {
    				fieldGroup.appendChild(help);
    			}
    		}
    
    		help.innerHTML = error.map(validationMessage).join('<br>');
    
    		const fieldGroup = input.closest('[class*="field-group"]');
    
    		if (fieldGroup) {
    			fieldGroup.classList.add('is-invalid');
    		}
    	};
    
    	clearFieldErrors() {
    		this.inputs.forEach((input) => {
    			let id = input.getAttribute('aria-describedby');
    
    			if (!id) {
    				id = `${input.getAttribute('name')}-help`;
    				input.setAttribute('aria-describedby', id);
    			}
    
    			const help = document.getElementById(id);
    
    			if (help) {
    				help.innerHTML = '';
    			}
    
    			const fieldGroup = input.closest('[class*="field-group"]');
    
    			if (fieldGroup) {
    				fieldGroup.classList.remove('is-invalid');
    			}
    		});
    	}
    
    	hideMessages() {
    		this.success.classList.add('is-hidden');
    		this.error.classList.add('is-hidden');
    	}
    
    	send() {
    		this.hideMessages();
    		this.setLoading(true);
    
    		const data = { ...this.data };
    		const method = this.element.getAttribute('method').toUpperCase() || 'POST';
    		const url = this.element.getAttribute('data-form');
    
    		if (this.token) {
    			data.token = this.token;
    		}
    
    		request(url, data, method)
    			.then(this.onSuccess)
    			.catch(this.displayError);
    	}
    
    	onSuccess = (json) => {
    		this.setLoading(false);
    
    		const tmpl = template(this.successMessage);
    
    		this.success.classList.remove('is-hidden');
    		this.success.innerHTML = tmpl(json);
    
    		this.events.emit('success', json);
    	};
    
    	reset = () => {
    		this.element.reset();
    		this.hideMessages();
    
    		this.success.innerHTML = '';
    		this.error.innerHTML = '';
    	};
    }
    
  • URL: /components/raw/form/Form.js
  • Filesystem Path: src/molecules/form/Form.js
  • Size: 6.2 KB
  • Content:
    import Form from './Form';
    
    const elements = document.querySelectorAll('[data-form]');
    
    if (elements.length) {
    	elements.forEach((element) => {
    		element.form = new Form(element);
    	});
    }
    
  • URL: /components/raw/form/index.js
  • Filesystem Path: src/molecules/form/index.js
  • Size: 187 Bytes

Form

A simple ajax form component

Usage

Simply add the attribute data-form="/wp-json/..." to any form and it will automatically send the form data to the defined endpoint. With additional markup you can control error/success states and set validation rules.

Success

The success response is handled via an EJS template and a data-form-success element.

Like this:

<div role="alert" data-form-success="locations" class="m-alert m-alert--success is-hidden"></div>

<script type="iis/template" id="locations">
    <% locations.forEach(function (location) { %><li><%- location.city %> (<%- location.country %>)</li><% }); %>
</script>

Error

The error message is defined with the attribute data-form-error. The error message is set automatically depending on what happened. If the API returned an error message, that message will be used. If validation failed, relevant validation messages will be showed. Finally, if the component, for any reason failed to do what was expected, an exception will be displayed.

Validation

You can validate the input on the client side by adding a meta element with the name form-validation. The content is the rules and should look like this: input-name=rule:data. For example:

<meta name="form-validation" content="domain=required,min:3">

Available rules:

Name Data Description
required - The field must exist
min numeric Checks if the value of the field is at least N characters

Events

All form elements (data-form) has the actual form component attached to it. You can access it like this:

const form = document.querySelector('[data-form]').form;

The form component has a few events you can listen to:

Name Description
success The form was successfully submitted
error The form failed to submit

You can listen to these events like this:

form.events.on('success', (data) => {
    console.log('Form was successfully submitted', data);
});

This can be used for tracking and other things.