<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
}
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 = '';
};
}
import Form from './Form';
const elements = document.querySelectorAll('[data-form]');
if (elements.length) {
elements.forEach((element) => {
element.form = new Form(element);
});
}
A simple ajax form component
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.
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>
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.
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 |
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.