Skip to content

Instantly share code, notes, and snippets.

@Error601
Last active February 24, 2016 23:40
Show Gist options
  • Save Error601/3ed55f56f86d06ea4905 to your computer and use it in GitHub Desktop.
Save Error601/3ed55f56f86d06ea4905 to your computer and use it in GitHub Desktop.
Spawn DOM elements, with *optional* jQuery support.
/*!
* 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);
@Error601
Copy link
Author

Limitations of the spawn.lite() function:

  • At least one argument is required (tag name)
  • Will not generate document fragments
  • Does not directly support jQuery methods (it's easy enough to wrap spawned elements inside a jQuery object)
  • Does not support 'data' attributes (maybe it should?)
  • Does not support 'attr' property (maybe it should?)

Advantages of the spawn.lite() function:

  • Much faster (up to 3x faster, maybe faster than that)
  • Allows element methods in the 'fn' property
  • Has 'html' property (alias for innerHTML)
  • Has 'append' property that can accept an HTML string or 'appendable' nodes

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