/** * Dynamic form plugin * @author Nazar Lazorko * @requires $ jQuery 1.7.2+ * @requires _ Underscore 1.4.2+ */ (function ($) { "use strict"; /** * @example * $(form).dynamicForm({ * relations: [ * { * node: '', // {jQuery|HTMLElement|String} jQuery element or selector that match main selected element * group: undefined, // [{jQuery|HTMLElement|String}] optional jQuery element or selector that match entire checkbox or radio group * event: '', // {String} jQuery event name * rules: [ * { * value: '', // {String|Function} expected value or callback that checks value * nodes: [] // [{jQuery|HTMLElement|String}, ...] list of related noted * autoRebuildNodes: false, // {Boolean} determines the need to rebuild nodes before each activation/deactivation * subRelations: [], // [{Relation}, ...] optional list of sub relations * activateFn: undefined, // {Function} optional * deactivateFn: undefined, // {Function} optional * } * // ... * ] * } * // ... * ] * }); * @param {Object} options * @param {Array.} options.relations */ $.fn.dynamicForm = function (options) { return new DynamicForm(this, options); }; var DynamicForm = function () { this.init.apply(this, arguments); }; DynamicForm.prototype = { /** * @param {jQuery} plugin * @param {Object} options * @param {Array.} options.relations * @return {*} */ init: function (plugin, options) { this.plugin = plugin; this.options = options; /** @type {Array.} */ this.relations = []; this.setRelations(options.relations); this.refresh(); return this; }, setRelations: function (relations) { var relation, i, length; for (i = 0, length = relations.length; i < length; ++i) { relation = relations[i]; relation.plugin = this.plugin; this.relations.push((relation instanceof Relation) ? relation : new Relation(relation)); } return this; }, refresh: function () { var relations = this.relations, i, length; for (i = 0, length = relations.length; i < length; ++i) { relations[i].eventHandler(); } return this; } }; /** * * @param {Object} options * @param {jQuery} options.plugin * @param {jQuery|HTMLElement|String} options.node * @param {jQuery|HTMLElement|String} [options.group] * @param {String} options.event * @param {Array.} options.rules * @return {Relation} * @constructor */ var Relation = function (options) { /** @type {jQuery} */ this.plugin = options.plugin; /** @type {jQuery} */ this.node = (options.node instanceof jQuery) ? options.node : this.plugin.find(options.node); /** @type {jQuery} */ this.group = (null == options.group) ? this.node : ((options.group instanceof jQuery) ? options.group : this.plugin.find(options.group)); this.defaultValue = this.node.val(); /** @type {String} */ this.event = String(options.event); /** @type {Array.} */ this.rules = []; this.setRules(options.rules); this.group.on(this.event, _.bind(this.onEvent, this, this)); return this; }; Relation.prototype = { setRules: function (rules) { var rule, i, length; for (i = 0, length = rules.length; i < length; ++i) { rule = rules[i]; rule.plugin = this.plugin; this.rules.push((rule instanceof Rule) ? rule : new Rule(rule)); } return this; }, reset: function () { var node = this.node; if (null !== this.defaultValue) { node.val(this.defaultValue); } else { if (node.is(':checkbox, :radio')) { node.removeAttr('checked'); } else { node.val(''); } } node.change(); return this; }, /** * @param {Object} relation * @param {jQuery.Event} e */ onEvent: function (relation, e) { this.eventHandler($(e.currentTarget)); return true; }, /** * @param {jQuery} [target] * @return {*} */ eventHandler: function (target) { target = target || this.node; var rules = this.getMatchedRules(this.rules, target); if (rules && rules.length) { this.deactivateRules(_.difference(this.rules, rules)); this.activateRules(rules); } else { this.deactivateRules(this.rules); } return this; }, /** * Returns array of rules that match specified value * @param {Array.} rules * @param {jQuery} target * @return {Array} */ getMatchedRules: function (rules, target) { var result = [], match, i, length, targetValue = target.val(), ruleValue; for (i = 0, length = rules.length; i < length; ++i) { ruleValue = rules[i].value; if (this.match(ruleValue, target)) { result.push(rules[i]); } } return result; }, /** * Test if element value match with rule value * @param ruleValue * @param target */ match: function (ruleValue, target) { switch ($.type(ruleValue)) { case 'function': return ruleValue.call(target, target.val()); case 'array': return ruleValue.indexOf(target.val()) != -1; default: return (target.val() == ruleValue); } }, /** * @param {Array.} rules Non matched rules * @return {*} */ deactivateRules: function (rules) { var i, length; for (i = 0, length = rules.length; i < length; ++i) { rules[i].deactivate(); } return this; }, /** * @param {Array.} rules Matched rules * @return {*} */ activateRules: function (rules) { var i, length; for (i = 0, length = rules.length; i < length; ++i) { rules[i].activate(); } return this; } }; /** * @param {Object} options * @param {jQuery} options.plugin * @param {String} options.value * @param {Array.} [options.nodes] * @param {Boolean} [options.autoRebuildNodes=false] * @param {Array.} [options.subRelations] * @param {Function} [options.activateFn] * @param {Function} [options.deactivateFn] * @constructor */ var Rule = function (options) { /** @type {jQuery} */ this.plugin = options.plugin; /** @type {String}|{Array} */ this.value = options.value; /** @type {Array.} */ this.rawNodes = options.nodes; /** @type {jQuery} */ this.nodes = undefined; this.rebuildNodes(); /** @type {Boolean} */ this.autoRebuildNodes = !!options.autoRebuildNodes || false; /** @type {Array.} */ this.subRelations = (options.subRelations) ? options.subRelations : undefined; /** @type {Function} optional */ this.activateFn = options.activateFn; /** @type {Function} optional */ this.deactivateFn = options.deactivateFn; return this; }; Rule.prototype = { rebuildNodes: function () { this.nodes = (this.rawNodes) ? this.mergeNodes(this.rawNodes) : undefined; return this; }, activate: function () { if (this.autoRebuildNodes) { this.rebuildNodes(); } if (null == this.activateFn || false !== this.activateFn.call(this)) { if (this.nodes) { this.nodes.show(); } if (this.subRelations) { for (var i = 0, length = this.subRelations.length; i < length; ++i) { this.subRelations[i].reset(); } } } return this; }, deactivate: function () { if (this.autoRebuildNodes) { this.rebuildNodes(); } if (null == this.deactivateFn || false !== this.deactivateFn.call(this)) { if (this.nodes) { this.nodes.hide(); } if (this.subRelations) { for (var i = 0, length = this.subRelations.length; i < length; ++i) { this.subRelations[i].reset(); } } } return this; }, /** * * @param {Array} list [{jQuery|HTMLElement|String}, ...] * @return {jQuery} */ mergeNodes: function (list) { list = ('array' === $.type(list)) ? list : [list]; var result = [], i, c, item; for (i = 0, c = list.length; i < c; ++i) { item = (list[i] instanceof jQuery) ? list[i] : this.plugin.find(list[i]); result = result.concat(item.get()); } return $(result); } }; $.fn.dynamicForm.Relation = Relation; $.fn.dynamicForm.Rule = Rule; })(jQuery);