Last active
February 24, 2016 23:40
-
-
Save Error601/3ed55f56f86d06ea4905 to your computer and use it in GitHub Desktop.
Spawn DOM elements, with *optional* jQuery support.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* DOM element spawner with *optional* jQuery functionality | |
* | |
* EXAMPLES: | |
* var p1 = spawn('p|id:p1', 'Text for paragraph 1.'); | |
* var div2 = spawn('div|class=div2', ['Text for div2.', p1]) // inserts text and puts p1 inside div2 | |
* var ul1 = spawn('ul', [['li', 'Content for <li> 1.'], ['li', 'Content for the next <li>.']]); | |
* div2.appendChild(ul1); // add ul1 to div2 | |
*/ | |
(function(window, doc){ | |
var undefined, | |
UNDEFINED = 'undefined'; | |
// The Spawner | |
function spawn(tag, opts, inner){ | |
var el, parts, attrs, use$, $el, children, | |
DIV = doc.createElement('div'), | |
classArray = [], | |
contents = '', | |
$opts = {}, | |
toDelete = ['tag', 'tagName']; | |
if (typeof tag == UNDEFINED) { | |
return doc.createDocumentFragment(); | |
} | |
// handle cases where 'tag' is already an element | |
if (isElement(tag) || isFragment(tag)) { | |
//return tag; | |
el = tag; | |
tag = el.tagName; // will this create a new element? | |
} | |
tag = typeof tag == 'string' ? tag.trim() : tag; | |
if (arguments.length === 1) { | |
if (Array.isArray(tag)) { | |
children = tag; | |
tag = '#html'; | |
} | |
else if (tag === '!') { | |
return doc.createDocumentFragment(); | |
} | |
else if (typeof tag == 'string' && tag !== '' && !(/^(#text|#html|!)|\|/gi.test(tag))) { | |
return doc.createElement(tag || 'span') | |
} | |
} | |
// make sure opts is defined | |
//opts = opts || {}; | |
if (arguments.length === 3) { | |
contents = inner; | |
} | |
if (Array.isArray(opts) || typeof opts == 'string' || typeof opts == 'function') { | |
contents = opts; | |
} | |
else { | |
opts = opts || {}; | |
} | |
if (isPlainObject(tag)) { | |
opts = tag; | |
tag = opts.tag || opts.tagName || '#html'; | |
} | |
// NOW make sure opts is an Object | |
opts = getObject(opts); | |
if (typeof contents == 'number') { | |
contents = contents + ''; | |
} | |
if (typeof contents == 'function'){ | |
try { | |
contents = contents(); | |
} | |
catch(e){ | |
if (console && console.log) console.log(e); | |
contents = []; | |
} | |
} | |
// combine 'content', 'contents', and 'children' respectively | |
contents = [].concat(contents||[], opts.content||[], opts.contents||[], opts.children||[], children||[]); | |
// add contents/children properties to list of properties to be deleted | |
toDelete.push('content', 'contents', 'children'); | |
// trim outer white space and remove any trailing | |
// semicolons or commas from 'tag' | |
// (shortcut for adding attributes) | |
parts = tag.trim().replace(/(;|,)$/, '').split('|'); | |
tag = parts[0].trim(); | |
if (el && (isElement(el) || isFragment(el))) { | |
// don't do anything if | |
// el is already an element | |
} | |
else { | |
// pass '!' as first argument to create fragment | |
//if (tag === '!'){ | |
// el = doc.createDocumentFragment(); | |
// //el.appendChild(contents); | |
// if (!contents.length){ | |
// return el; | |
// } | |
//} | |
// pass empty string '', '#text', or '#html' as first argument | |
// to create a textNode | |
if (tag === '' || /^(#text|#html|!)|\|/gi.test(tag)) { | |
el = doc.createDocumentFragment(); | |
//el.appendChild(doc.createTextNode(contents)); | |
//return el; | |
} | |
else { | |
try { | |
el = doc.createElement(tag || 'span'); | |
} | |
catch(e) { | |
if (console && console.log) console.log(e); | |
el = doc.createDocumentFragment(); | |
el.appendChild(doc.createTextNode(tag || '')); | |
} | |
} | |
} | |
// pass element attributes in 'tag' string, like: | |
// spawn('a|id="foo-link";href="foo";class="bar"'); | |
// or (colons for separators, commas for delimeters, no quotes),: | |
// spawn('input|type:checkbox,id:foo-ckbx'); | |
// allow ';' or ',' for attribute delimeter | |
attrs = parts[1] ? parts[1].split(/;|,/) || [] : []; | |
forEach(attrs, function(att){ | |
if (!att) return; | |
var sep = /:|=/; // allow ':' or '=' for key/value separator | |
var quotes = /^('|")|('|")$/g; | |
var key = att.split(sep)[0].trim(); | |
var val = (att.split(sep)[1] || '').trim().replace(quotes, '') || key; | |
// add each attribute/property directly to DOM element | |
//el[key] = val; | |
el.setAttribute(key, val); | |
}); | |
// 'attr' property (object) to EXPLICITLY set attribute=value | |
opts.attr = opts.attr || opts.attrs || opts.attributes; | |
if (opts.attr) { | |
forOwn(opts.attr, function(name, val){ | |
// if a 'data' object snuck in 'attr' | |
if (name.data) { | |
opts.data = name.data; | |
delete name.data; | |
return; | |
} | |
el.setAttribute(name, val); | |
}); | |
} | |
// any 'data-' attributes? | |
if (opts.data) { | |
forOwn(opts.data, function(name, val){ | |
setElementData(el, name, val); | |
}); | |
} | |
toDelete.push('data', 'attr'); | |
//opts = isPlainObject(opts) ? opts : {}; | |
// Are we using jQuery later? | |
// jQuery stuff needs to be in a property named $, jq, jQuery, or jquery | |
opts.$ = opts.$ || opts.jq || opts.jQuery || opts.jquery; | |
use$ = isDefined(opts.$ || undefined); | |
if (use$) { | |
// copy to new object so we can delete from {opts} | |
forOwn(opts.$, function(method, args){ | |
$opts[method] = args; | |
}); | |
} | |
// delete these before adding stuff to the element | |
toDelete.push('$', 'jq', 'jQuery', 'jquery'); | |
// allow use of 'classes', 'classNames', 'className', and 'addClass' | |
// as a space-separated string or array of strings | |
opts.className = [].concat(opts.classes||[], opts.classNames||[], opts.className||[], opts.addClass||[]); | |
// delete bogus 'class' properties later | |
toDelete.push('classes', 'classNames', 'addClass'); | |
forEach(opts.className.join(' ').split(/\s+/), function(name){ | |
if (classArray.indexOf(name) === -1) { | |
classArray.push(name) | |
} | |
}); | |
// apply sanitized className string back to opts.className | |
opts.className = classArray.join(' ').trim(); | |
// if no className, delete property | |
if (!opts.className) toDelete.push('className'); | |
// contents MUST be an array before being processed later | |
// add 'prepend' and 'append' properties | |
contents = [].concat(opts.prepend||[], contents, opts.append||[]); | |
toDelete.push('prepend', 'append'); | |
// DELETE PROPERTIES THAT AREN'T VALID *ELEMENT* ATTRIBUTES OR PROPERTIES | |
forEach(toDelete, function(prop){ | |
delete opts[prop]; | |
}); | |
// add remaining properties and attributes to element | |
// (there should only be legal attributes left) | |
if (isPlainObject(opts)) { | |
forOwn(opts, function(attr, val){ | |
el[attr] = val; | |
}); | |
} | |
forEach(contents, function(part){ | |
try { | |
if (typeof part == 'string') { | |
DIV = doc.createElement('div'); | |
DIV.innerHTML = part; | |
while (DIV.firstChild){ | |
el.appendChild(DIV.firstChild); | |
} | |
} | |
else if (isElement(part) || isFragment(part)) { | |
el.appendChild(part); | |
} | |
else { | |
el.appendChild(spawn.apply(null, [].concat(part))) | |
} | |
} | |
catch(e) { | |
if (console && console.log) console.log(e); | |
} | |
}); | |
// that's it... 'contents' HAS to be one of the following | |
// - text or HTML string | |
// - array of spawn() compatible arrays: ['div', '{divOpts}'] | |
// - element or fragment | |
// OPTIONALLY do some jQuery stuff, if specified (and available) | |
if (use$ && isDefined(window.jQuery || undefined)) { | |
$el = window.jQuery(el); | |
forOwn($opts, function(method, args){ | |
method = method.toLowerCase(); | |
// accept on/off event handlers with varying | |
// number of arguments | |
if (/^(on|off)$/.test(method)) { | |
forOwn(args, function(evt, fn){ | |
try { | |
$el[method].apply($el, [].concat(evt, fn)); | |
} | |
catch(e) { | |
if (console && console.log) console.log(e); | |
} | |
}); | |
return; | |
} | |
$el[method].apply($el, [].concat(args)) | |
}); | |
//return $el; | |
} | |
return el; | |
} | |
/** | |
* Leaner, faster element spawner. | |
* @param tag {String} element's tagName | |
* @param [opts] {Object|Array|String} element | |
* properties/attributes -or- array of | |
* children -or- HTML string | |
* @param [children] {Array|String} | |
* array of child element 'spawn' arg arrays | |
* or elements or HTML string | |
* @returns {Element} | |
*/ | |
spawn.lite = function(tag, opts, children){ | |
var el = doc.createElement(tag||'div'), | |
skip = [], // properties to skip later | |
errors = []; // collect errors | |
if (!opts && !children){ | |
// return early for basic element creation | |
return el; | |
} | |
opts = opts || {}; | |
children = children || null; | |
// if 'opts' is a string, | |
// set el's innerHTML and | |
// make 'opts' an object | |
if (typeof opts == 'string'){ | |
el.innerHTML += opts; | |
//opts = {}; | |
return el; | |
} | |
// if 'children' arg is not present | |
// and 'opts' is really an array | |
if (!children && Array.isArray(opts)){ | |
children = opts; | |
opts = {}; | |
} | |
// or if 'children' is a string | |
// set THAT to the innerHTML | |
else if (typeof children == 'string'){ | |
el.innerHTML += children; | |
children = null; | |
} | |
// add innerHTML now, if present | |
el.innerHTML += (opts.innerHTML||opts.html||''); | |
// append any spawned children | |
if (children && Array.isArray(children)){ | |
children.forEach(function(child){ | |
// each 'child' can be an array of | |
// spawn arrays... | |
if (Array.isArray(child)){ | |
child = spawn.lite.apply(el, child); | |
} | |
// ...or 'appendable' nodes | |
try { | |
el.appendChild(child); | |
} | |
catch(e){ | |
// fail silently | |
errors.push('Error processing children: ' + e); | |
} | |
}); | |
} | |
// special handling of 'append' property | |
if (opts.append){ | |
// a string should be HTML | |
if (typeof opts.append == 'string'){ | |
el.innerHTML += opts.append; | |
} | |
// otherwise an 'appendable' node | |
else { | |
try { | |
el.appendChild(opts.append); | |
} | |
catch(e){ | |
errors.push('Error appending: ' + e); | |
} | |
} | |
} | |
// attach object or array of methods | |
// to 'fn' property - this can be an | |
// array in case you want to run the | |
// same method(s) more than once | |
var fns = opts.fn || null; | |
// DO NOT ADD THESE DIRECTLY TO 'el' | |
skip.push('innerHTML', 'html', 'append', 'fn'); | |
// add attributes and properties to element | |
forOwn(opts, function(prop, val){ | |
// only add if NOT in 'skip' array | |
if (skip.indexOf(prop) === -1){ | |
el[prop] = val; | |
} | |
}); | |
// execute element methods last | |
if (fns){ | |
[].concat(fns).forEach(function(fn){ | |
forOwn(fn, function(f, args){ | |
el[f].apply(el, [].concat(args)); | |
}); | |
}); | |
} | |
if (errors.length){ | |
if (console && console.log) console.log(errors) | |
} | |
return el; | |
}; | |
spawn.trial = function(tag, count){ | |
tag = tag || 'div'; | |
count = count || 1000; | |
var i = -1, | |
time = Date.now(), | |
span = spawn('span'); | |
while (++i < count){ | |
span.appendChild(spawn.apply(null, [].concat(tag))); | |
} | |
console.log('time: ' + ((Date.now() - time) / 1000 ) + 's'); | |
return span.childNodes; | |
}; | |
// export to the global window object | |
window.spawn = spawn; | |
// | |
// utility functions: | |
// | |
function isElement(it){ | |
return it.nodeType && it.nodeType === 1; | |
} | |
function isFragment(it){ | |
return it.nodeType && it.nodeType === 11; | |
} | |
function isDefined(it){ | |
return typeof it != 'undefined'; | |
} | |
// returns first defined argument | |
// useful for retrieving 'falsey' values | |
function firstDefined(){ | |
var undefined, i = -1; | |
while (++i < arguments.length) { | |
if (arguments[i] !== undefined) { | |
return arguments[i]; | |
} | |
} | |
return undefined; | |
} | |
function isPlainObject(obj){ | |
return Object.prototype.toString.call(obj) === '[object Object]'; | |
} | |
function getObject(obj){ | |
return isPlainObject(obj) ? obj : {}; | |
} | |
function forEach(arr, fn){ | |
if (!arr) return; | |
var i = -1, len = arr.length; | |
while (++i < len) { | |
fn(arr[i], i); | |
} | |
} | |
function forOwn(obj, fn){ | |
if (!obj) return; | |
var keys = [], | |
key; | |
for (key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
keys.push(key); | |
if (typeof fn != 'function') continue; | |
fn(key, obj[key]); | |
} | |
} | |
return keys; | |
} | |
function setElementData(element, name, val){ | |
if (document.head && document.head.dataset) { | |
name = camelize(name); | |
element.dataset[name] = val; | |
} | |
else { | |
name = hyphenize(name); | |
element.setAttribute('data-' + name, val); | |
} | |
} | |
function getElementData(element, name){ | |
if (document.head && document.head.dataset) { | |
name = camelize(name); | |
return realValue(element.dataset[name]); | |
} | |
else { | |
name = hyphenize(name); | |
return realValue(element.getAttribute('data-' + name)); | |
} | |
} | |
// returns real boolean for boolean string | |
// returns real number for numeric string | |
// returns null and undefined for those strings | |
// (or returns original value if none of those) | |
// useful for pulling 'real' values from | |
// a string used in [data-] attributes | |
function realValue(val, bool){ | |
var undefined; | |
// only evaluate strings | |
if (!isString(val)) return val; | |
if (bool) { | |
if (val === '0') { | |
return false; | |
} | |
if (val === '1') { | |
return true; | |
} | |
} | |
if (isNumeric(val)) { | |
return +val; | |
} | |
switch(val) { | |
case 'true': | |
return true; | |
case 'false': | |
return false; | |
case 'undefined': | |
return undefined; | |
case 'null': | |
return null; | |
default: | |
return val; | |
} | |
} | |
function hyphenize(name){ | |
return name.replace(/([A-Z])/g, function(u){ | |
return '-' + u.toLowerCase(); | |
}); | |
} | |
// set 'forceLower' === true (or omit argument) | |
// to ensure *only* 'cameled' letters are uppercase | |
function camelize(name, forceLower){ | |
if (firstDefined(forceLower, false)) { | |
name = name.toLowerCase(); | |
} | |
return name.replace(/\-./g, function(u){ | |
return u.substr(1).toUpperCase(); | |
}); | |
} | |
})(this, document); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Limitations of the spawn.lite() function:
Advantages of the spawn.lite() function: