forms
Version:
An easy way to create, parse, and validate forms
327 lines (298 loc) • 10.3 kB
JavaScript
;
var is = require('is');
var reduce = require('reduce');
var some = require('array.prototype.some');
var tag = require('./tag');
var dataRegExp = /^data-[a-z]+([-][a-z]+)*$/;
var ariaRegExp = /^aria-[a-z]+$/;
var legalAttrs = [
'autocomplete', 'autocorrect', 'autofocus', 'autosuggest', 'checked', 'dirname', 'disabled', 'tabindex', 'list', 'max', 'maxlength', 'min', 'novalidate', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'step'
];
var ignoreAttrs = [
'id', 'name', 'class', 'classes', 'type', 'value', 'multiple'
];
var getUserAttrs = function (opt) {
return reduce(opt, function (attrs, option, k) {
if ((ignoreAttrs.indexOf(k) === -1 && legalAttrs.indexOf(k) > -1) || dataRegExp.test(k) || ariaRegExp.test(k)) {
// eslint-disable-next-line no-param-reassign
attrs[k] = option;
}
return attrs;
}, {});
};
// used to generate different input elements varying only by type attribute
var input = function (type) {
return function (opts) {
var opt = opts || {};
var userAttrs = getUserAttrs(opt);
var w = {
classes: opt.classes,
type: type,
formatValue: function (value) {
return value || value === 0 ? value : null;
}
};
w.toHTML = function (name, field) {
var f = field || {};
var attrs = {
type: type,
name: name,
id: f.id === false ? false : f.id || true,
classes: w.classes,
value: w.formatValue(f.value)
};
return tag('input', [
attrs, userAttrs, w.attrs || {}
]);
};
return w;
};
};
var choiceValueEquals = function (value1, value2) {
return !is.array(value1) && !is.array(value2) && String(value1) === String(value2);
};
var isSelected = function (value, choice) {
return value && (is.array(value)
? some(value, function (v) { return choiceValueEquals(v, choice); })
: choiceValueEquals(value, choice));
};
var renderChoices = function (choices, renderer) {
return reduce(choices, function (partialRendered, choice) {
var isNested = is.array(choice[1]);
var renderData = isNested
? {
isNested: true, label: choice[0], choices: choice[1]
}
: {
isNested: false, value: choice[0], label: choice[1]
};
return partialRendered + renderer(renderData);
}, '');
};
var isScalar = function (value) {
return !value || is.string(value) || is.number(value) || is.bool(value);
};
var unifyChoices = function (choices, nestingLevel) {
if (nestingLevel < 0) {
throw new RangeError('choices nested too deep');
}
var unifyChoiceArray = function (arrayChoices, currentLevel) {
return reduce(arrayChoices, function (result, choice) {
if (!is.array(choice) || choice.length !== 2) {
throw new TypeError('choice must be array with two elements');
}
if (isScalar(choice[0]) && isScalar(choice[1])) {
result.push(choice);
} else if (isScalar(choice[0]) && (is.array(choice[1]) || is.object(choice[1]))) {
result.push([choice[0], unifyChoices(choice[1], currentLevel - 1)]);
} else {
throw new TypeError('expected primitive value as first and primitive value, object, or array as second element');
}
return result;
}, []);
};
var unifyChoiceObject = function (objectChoices, currentLevel) {
return reduce(objectChoices, function (result, label, key) {
if (isScalar(label)) {
result.push([key, label]);
} else if (is.array(label) || is.object(label)) {
result.push([key, unifyChoices(label, currentLevel - 1)]);
} else {
throw new TypeError('expected primitive value, object, or array as object value');
}
return result;
}, []);
};
return is.array(choices) ? unifyChoiceArray(choices, nestingLevel) : unifyChoiceObject(choices, nestingLevel);
};
var select = function (isMultiple) {
return function (options) {
var opt = options || {};
var w = {
classes: opt.classes,
type: isMultiple ? 'multipleSelect' : 'select'
};
var userAttrs = getUserAttrs(opt);
w.toHTML = function (name, field) {
var f = field || {};
var choices = unifyChoices(f.choices, 1);
var optionsHTML = renderChoices(choices, function render(choice) {
if (choice.isNested) {
return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render), true);
} else {
return tag('option', { value: choice.value, selected: !!isSelected(f.value, choice.value) }, choice.label);
}
});
var attrs = {
name: name,
id: f.id === false ? false : f.id || true,
classes: w.classes
};
if (isMultiple) {
attrs.multiple = true;
}
return tag('select', [
attrs, userAttrs, w.attrs || {}
], optionsHTML, true);
};
return w;
};
};
exports.text = input('text');
exports.email = input('email');
exports.number = input('number');
exports.hidden = input('hidden');
exports.color = input('color');
exports.tel = input('tel');
var passwordWidget = input('password');
var passwordFormatValue = function () {
return null;
};
exports.password = function (opt) {
var w = passwordWidget(opt);
w.formatValue = passwordFormatValue;
return w;
};
var dateWidget = input('date');
exports.date = function (opt) {
var w = dateWidget(opt);
w.formatValue = function (value) {
if (!value) {
return null;
}
var date = is.date(value) ? value : new Date(value);
if (isNaN(date.getTime())) {
return null;
}
return date.toISOString().slice(0, 10);
};
return w;
};
exports.select = select(false);
exports.multipleSelect = select(true);
exports.checkbox = function (options) {
var opt = options || {};
var w = {
classes: opt.classes,
type: 'checkbox'
};
var userAttrs = getUserAttrs(opt);
w.toHTML = function (name, field) {
var f = field || {};
var attrs = {
type: 'checkbox',
name: name,
id: f.id === false ? false : f.id || true,
classes: w.classes,
checked: !!f.value,
value: 'on'
};
return tag('input', [
attrs, userAttrs, w.attrs || {}
]);
};
return w;
};
exports.textarea = function (options) {
var opt = options || {};
var w = {
classes: opt.classes,
type: 'textarea'
};
var userAttrs = getUserAttrs(opt);
w.toHTML = function (name, field) {
var f = field || {};
var attrs = {
name: name,
id: f.id === false ? false : f.id || true,
classes: w.classes,
rows: opt.rows || null,
cols: opt.cols || null
};
return tag('textarea', [
attrs, userAttrs, w.attrs || {}
], f.value || '');
};
return w;
};
exports.multipleCheckbox = function (options) {
var opt = options || {};
var w = {
classes: opt.classes,
labelClasses: opt.labelClasses,
type: 'multipleCheckbox'
};
var userAttrs = getUserAttrs(opt);
w.toHTML = function (name, field) {
var f = field || {};
var choices = unifyChoices(f.choices, 0);
return renderChoices(choices, function (choice) {
// input element
var id = f.id === false ? false : f.id ? f.id + '_' + choice.value : 'id_' + name + '_' + choice.value;
var checked = isSelected(f.value, choice.value);
var attrs = {
type: 'checkbox',
name: name,
id: id,
classes: w.classes,
value: choice.value,
checked: !!checked
};
var inputHTML = tag('input', [
attrs, userAttrs, w.attrs || {}
]);
// label element
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, choice.label);
return inputHTML + labelHTML;
});
};
return w;
};
exports.label = function (options) {
var opt = options || {};
var w = { classes: opt.classes || [] };
var userAttrs = getUserAttrs(opt);
w.toHTML = function (forID) {
var attrs = {
'for': forID,
classes: w.classes
};
return tag('label', [
attrs, userAttrs, w.attrs || {}
], opt.content);
};
return w;
};
exports.multipleRadio = function (options) {
var opt = options || {};
var w = {
classes: opt.classes,
labelClasses: opt.labelClasses,
type: 'multipleRadio'
};
var userAttrs = getUserAttrs(opt);
w.toHTML = function (name, field) {
var f = field || {};
var choices = unifyChoices(f.choices, 0);
return renderChoices(choices, function (choice) {
// input element
var id = f.id === false ? false : f.id ? f.id + '_' + choice.value : 'id_' + name + '_' + choice.value;
var checked = isSelected(f.value, choice.value);
var attrs = {
type: 'radio',
name: name,
id: id,
classes: w.classes,
value: choice.value,
checked: !!checked
};
var inputHTML = tag('input', [
attrs, userAttrs, w.attrs || {}
]);
// label element
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, choice.label);
return inputHTML + labelHTML;
});
};
return w;
};