Skip to content

Instantly share code, notes, and snippets.

@rodyhaddad
Last active May 5, 2017 14:50
Show Gist options
  • Save rodyhaddad/2e7464d1b9bc79d61a2345cba7020ebb to your computer and use it in GitHub Desktop.
Save rodyhaddad/2e7464d1b9bc79d61a2345cba7020ebb to your computer and use it in GitHub Desktop.
Library to add CSS classes to form inputs, describing their different states
window.formify = function formify(form, options) {
if (!(form instanceof HTMLFormElement)) {
throw new Error('Tried to Formify a non-form element: ' + form);
}
var opt = {
classPrefix: options.classPrefix || 'fm'
};
var validityKeys = [
'badInput',
'customError',
'patternMismatch',
'rangeOverflow',
'rangeUnderflow',
'stepMismatch',
'tooLong',
'tooShort',
'typeMismatch',
'valueMissing'
];
var inputChangeHandlers = [];
Array.from(form.elements).forEach(function(el) {
inputChangeHandlers.push({
el: el,
listener: setupFormElement(el)
});
});
listenInputChange(form, handleInputChange, true)
function handleInputChange(evt) {
inputChangeHandlers.forEach(function(handler) {
handler.listener(evt);
});
}
function setupFormElement(el) {
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLButtonElement)) {
throw new Error('Tried to setup a non-input-ish element: ' + el);
}
var state = {
dirty: false,
touched: false,
valid: true,
empty: true,
invalidKeys: [],
};
el.addEventListener('blur', changeStateFn('touched', true));
listenInputChange(el, changeStateFn('dirty', true));
var pTarget = el.getAttribute('data-formify-ptarget');
var targets = [el];
if (pTarget) {
var pIndex = parseInt(pTarget, 10);
var pEl = el;
for (var i = 0; i < pIndex; i++) {
pEl = el.parentNode;
}
targets.push(pEl);
}
updateValidity();
return updateValidity;
function changeStateFn(stateName, val) {
return function() {
if (state[stateName] !== val) {
state[stateName] = val;
updateClasses();
}
}
}
function updateValidity() {
var validity = el.validity;
state.empty = !el.value;
if (['checkbox', 'radio'].indexOf(el.type) !== -1) {
state.empty = !el.checked;
}
if (validity) {
state.valid = validity.valid;
state.invalidKeys = [];
validityKeys.forEach(function(key) {
if (validity[key]) {
state.invalidKeys.push(key);
}
});
}
updateClasses();
}
function updateClasses() {
targets.forEach(function(el) {
updateClass(state.dirty, '-dirty', '-pristine');
updateClass(state.touched, '-touched', '-untouched');
updateClass(state.valid, '-valid', '-invalid');
updateClass(state.empty, '-empty', '-not-empty');
function updateClass(val, trueClass, falseClass) {
if (val) {
el.classList.add(opt.classPrefix + trueClass);
el.classList.remove(opt.classPrefix + falseClass);
} else {
el.classList.add(opt.classPrefix + falseClass);
el.classList.remove(opt.classPrefix + trueClass);
}
}
validityKeys.forEach(function(key) {
if (state.invalidKeys.indexOf(key) === -1) {
el.classList.remove(opt.classPrefix + '-invalid-' + toKebabCase(key));
} else {
el.classList.add(opt.classPrefix + '-invalid-' + toKebabCase(key));
}
});
});
}
}
function listenInputChange(el, listener, useTarget) {
var handled = false;
var handleInputChange = function(evt) {
if (!handled) {
handled = true;
listener(evt);
setTimeout(function() {
handled = false;
});
}
};
if (useTarget) {
handled = [];
handleInputChange = function(evt) {
var target = evt.target;
if (handled.indexOf(target) === -1) {
handled.push(target);
listener(evt);
setTimeout(function() {
var index;
if ((index = handled.indexOf(target)) !== -1) {
handled.splice(index, 1);
}
});
}
};
}
el.addEventListener('input', handleInputChange);
el.addEventListener('change', handleInputChange);
}
function toKebabCase(str) {
return str.replace(/([A-Z])/g, function($1) {
return "-" + $1.toLowerCase();
});
}
}
@rodyhaddad
Copy link
Author

If you're interested in a demo & explanation for this: http://bit.ly/formify-demo2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment