<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 disabled>
        <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 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: #e0bff5;
                        --m-modal-button-primary-hover-color: #c27fec;
                        --m-modal-button-primary-border-color: #c27fec;
                        --m-modal-button-primary-text-color: #1f2a36;
                        --m-modal-button-secondary-color: #ff9fb4;
                        --m-modal-button-secondary-hover-color: #ff4069;
                        --m-modal-button-secondary-border-color: #ff4069;
                        --m-modal-button-secondary-text-color: #1f2a36;
                    }
                </style>
                <div class="field-group">
                    <label for="message">Meddelande</label>
                    <textarea rows="5" class="a-textarea" id="message" data-rich-text placeholder="Meddelande" autocomplete="">
			<p>Lorem <a href="https://internetstiftelsen.se" target="_blank">ipsum</a> <strong>dolor</strong> sit amet, consectetur adipiscing elit. Praesent mollis suscipit felis, eu ullamcorper quam. Nunc eleifend sapien velit, vitae <em>tincidunt</em> urna mollis ac.</p>
			<ul>
				<li>aliquet</li>
				<li>vehicula</li>
				<li>turpis</li>
			</ul>
		</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">
				{{render '@textarea--rich-text'}}
			</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": true
}
  • Content:
    import template from 'lodash.template';
    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]');
    
    		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;
    
    		this.recaptcha = this.element.getAttribute('data-recaptcha');
    
    		if (this.recaptcha) {
    			window.captchaCallback = this.captchaCallback;
    			this.renderCaptchaForm();
    		}
    
    		if (this.validation) {
    			this.parseValidationRules();
    		}
    
    		this.collectInputs();
    		this.attach();
    	}
    
    	collectInputs() {
    		this.inputs = this.element.querySelectorAll('input');
    	}
    
    	renderCaptchaForm() {
    		const s = document.createElement('script');
    		s.defer = true;
    		s.setAttribute('data-origin', this.recaptcha);
    		s.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=captchaCallback&render=6LdtNnkUAAAAACYo0vISI-z9tOyr3djjZore-6wY&hl=sv');
    		document.body.appendChild(s);
    	}
    
    	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()) {
    			if (this.recaptcha) {
    				this.captchaCallback();
    			}
    
    			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.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);
    	};
    
    	captchaCallback = () => {
    		if (typeof window.grecaptcha !== 'undefined' && 'CAPTCHA_KEY' in process.env) {
    			/* global grecaptcha */
    			grecaptcha.execute(process.env.CAPTCHA_KEY, { action: this.recaptcha })
    				.then((token) => {
    					this.token = token;
    				});
    		}
    	};
    
    	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.8 KB
  • Content:
    import Form from './Form';
    
    const elements = document.querySelectorAll('[data-form]');
    
    if (elements.length) {
    	elements.forEach((element) => new Form(element));
    }
    
  • URL: /components/raw/form/index.js
  • Filesystem Path: src/molecules/form/index.js
  • Size: 164 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 message is defined with the attribute data-form-success. How the message looks is completely up to you. You can use template tags that will be replaced with the data received from the API.

Like this:

<div data-form-success>
    The result was: {result}
</div>

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