/* DynamicInputList takes in a parent element, a row template (in html NOT dom), a callback func and options object. - The returned value is an array of objects, where each object's key is the name (or id) of the corresponding input/textarea/select element. -It takes complete control of all input, textarea and select elements in the template, if there's a select tag, it converts it to a DropDown object. - To pass previous options, pass {values: [{inputName: value}]} */ var DynamicInputList = (function(){ var buttonsTmpl = '
', forEach = Array.prototype.forEach, map = Array.prototype.map; function Row(events, id, values, tmpl, cb, req, nums, filterFn) { var row = document.createElement('row'), self = this, init = false; row.innerHTML = tmpl + buttonsTmpl; this.ele = row; var eles = row.querySelectorAll('input, textarea, select'), otherEles = [], addBtn = row.querySelector('.add'), removeBtn = row.querySelector('.remove'); forEach.call(eles, function(e) { if(!e.name.length && e.id.length) { e.name = e.id; } if(!e.name.length) return console.error("element doesn't have a name.", tmpl); if(e.getAttribute('required') != null) req[e.name] = true; if(e.getAttribute('number') != null) nums[e.name] = true; values[e.name] = values[e.name]; // this enforces the object to have the element name and sets older values. if(e.tagName === 'SELECT') { setupDropDown(e); } else if(e.tagName === 'INPUT') { switch(e.type.toLowerCase()) { case 'checkbox': setupCheckbox(e); break; case 'hidden': values[e.name] = getEleValue(e); break; default: events.add(e, 'change', updateValue); } } else { events.add(e, 'change', updateValue); } }); function setupCheckbox(e) { var p = e.parentNode, chk = new Checkbox(p, function(v) { return updateValue({target:{name: e.name, value: v ? getEleValue(e) : null}}); }, true); if(values[e.name] != null) chk.set(); p.removeChild(e); otherEles.push(chk); } function setupDropDown(e) { var dd = convertSelect(e, values[e.name], function(v) { return updateValue({target:{name: e.name, value: v}}); }); otherEles.push(dd); } events.add(addBtn, 'click', function(e) { if(!isValidRow(values, req) || (!!filterFn && !filterFn(values))) return; cb(e, 'add', values); }); events.add(removeBtn, 'click', function(e) { cb(e, 'remove', values); }); if(Object.keys(values).length) setValue(values); function updateValue(e) { values[e.target.name] = getEleValue(e.target); if(init) cb(e, "change", values, e.target.name, values[e.target.name]); } function setValue(newValues) { forEach.call(eles, function(e) { var k = e.name, v = newValues[k]; if(v != null) { e.value = v; values[k] = v; } }); } this.exit = function rowExit() { forEach.call(otherEles, function(e) { e.exit(); }); forEach.call(eles, function(e) { events.removeAll(e); }); events.removeAll(addBtn); events.removeAll(removeBtn); this.ele = row = addBtn = removeBtn = self = eles = otherEles = null; }; this.value = function() { return values; }; this.setValue = setValue; init = true; function getEleValue(e) { if(nums[e.name]) return parseFloat(e.value); return e.value; } } function DynamicInputList(ele, tmpl, cb, opts) { opts = opts || {}; var events = new EventManager(), allValues = [], req = {}, nums = {}, rows = [], active = 0, filterFn = typeof opts.filter === 'function' ? opts.filter : defFilter; this.value = value; this.setValue = setValue; this.exit = exit; ele.classList.add("dynamicInputList"); if(isArray(opts.values)) setValue(opts.values); addRow(); function addRow(values) { var id = rows.length, values = values || {}, row; active++; row = new Row(events, id, values, tmpl, function(e, action, values, name, val) { if(action === 'remove') { removeRow(id); if(typeof opts.onRemove === 'function') opts.onRemove(e, value(), values); } else if(action === 'add') { addRow(); } else if(action === 'change') { cb(e, value(), values, name, val); } }, req, nums, filterFn); allValues.push(values); rows.push(row); ele.appendChild(row.ele) } function removeRow(id) { active--; var row = rows[id]; ele.removeChild(row.ele); row.exit(); rows[id] = allValues[id] = null; if(active === 0) addRow(); } function setValue(vals) { if(!isArray(vals)) return console.error('setValue accepts an array.'); vals.map(addRow); } function value() { return allValues.filter(filterFn); } function exit() { if(!!events) events.reset(); if(!!rows) loop(rows, remRow); function remRow(k, v){ if(v && v.ele) removeChild(ele, v.ele); } ele = events = rows = allValues = null; } function defFilter(obj) { return isValidRow(obj, req) ? obj : null; } } function isValidRow(obj, req) { if(!isObject(obj)) return true; var keys = Object.keys(obj), len = keys.length, missing = 0; for(var k in obj) { var v = obj[k]; if(v == null || typeof v === 'string' && v.length === 0) { if(req[k]) missing++; }; } return missing === 0; } function convertSelect(e, val, cb) { var div = getElementFromString('
'), options = map.call(e.querySelectorAll('option'), function(o) { return {value: o.value || o.innerText, title: o.innerHTML}; }), dd = new Dropdown(div, { 'data' : options, 'initialValue': (val || options[0].value) + '', 'options' : { 'dropdownClass' : 'cool', 'menuItemClass' : 'cool' } }, cb); e.parentNode.replaceChild(div, e); return dd; } return DynamicInputList; })();