Last active
August 29, 2019 17:12
-
-
Save Quacky2200/725254c52dfe24a18a87a7e25677acea to your computer and use it in GitHub Desktop.
Simple JavaScript template/view/HTML building function.
This file contains hidden or 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
/** | |
* Tags by Argument | |
* | |
* Creates a HTML string from the element name, attributes and content given | |
* as part of the arguments provided. | |
* | |
* The name argument can be null, which will produce a standard div element. | |
* If the name is prefixed with a hash, and an id is not provided as an | |
* attribute, then the id attribute will be added. Classes can be described | |
* by using dot notation and will get prefixed to the class attribute. This | |
* familiar syntax follows CSS selector syntax as seen with querySelector | |
* and the jQuery framework. | |
* | |
* Examples of name formats: | |
* - (null|undefined|0) | |
* - div | |
* - div.class#id | |
* - div#id | |
* - .class.another.class#id2 | |
* - .container > .inner > h1 | |
* | |
* The attributes must be given as KeyValuePairs (an object), or a string | |
* (see below), otherwise the provided attributes will not be used. | |
* Examples: | |
* {class: 'alert', style: 'color: red', 'data-msg': 'Example Alert'} | |
* 'class="alert" style="color: red" data-msg="Example Alert"' | |
* | |
* If only two parameters are submitted and the value is either a string or | |
* an array, then an assumption will be made that it is content rather than | |
* an argument. This means that string element arguments can only be given | |
* when ALL 3 arguments are present. | |
* | |
* This allows a lot of flexibility: | |
* tag('p > a.test.example#unique', {href: '/'}, 'hello world') | |
* <p><a class="test example" id="unique" href="/">hello world</a></p> | |
* | |
* tag('.container > .inner > header', 'Header 1') | |
* <div class="container"> | |
* <div class="inner"> | |
* <header>Header 1</header> | |
* </div> | |
* </div> | |
* | |
* tag('.container', [ | |
* '<p>Hello World</p>', | |
* tag('a > span', 'An empty link') | |
* ]) | |
* <div class="container"> | |
* <p>Hello World</p> | |
* <a><span>An empty link</span></a> | |
* </div> | |
* | |
* tag('.container', {class: 'inner', style: 'background: red'}) | |
* <div class="container inner" style="background: red"></div> | |
* | |
* @param {string} name The name of the element | |
* @param {object} attr Object containing attributes (KVPs) | |
* @param {string} content Embedded content | |
* @returns {string} The built HTML | |
*/ | |
const tag = function tag(name, attr, content) { | |
// These tags must be closed automatically with HTML4 standards | |
var self_closing = [ | |
'area', | |
'base', | |
'br', | |
'col', | |
'embed', | |
'hr', | |
'img', | |
'input', | |
'link', | |
'meta', | |
'param', | |
'source', | |
'track', | |
'wbr', | |
]; | |
// Use the name or default to a div tag | |
name = (name || 'div'); | |
// Check whether the input contains invalid characters | |
if (name.match(/(?:([\w\d\-\.\#]+)(\(.*\))?[\s>]*)/g, '').join('') !== name) { | |
console.warn('Invalid characters present in element syntax:', name); | |
} | |
// Allow shortened arguments, only allow element arguments if an object | |
// is sent, otherwise expect it as content | |
if (arguments.length === 2 && | |
attr && attr !== null && | |
((typeof(attr) === 'object' && attr.constructor.name === 'Array') || | |
typeof(attr) === 'string')) { | |
content = attr; | |
attr = false; | |
} | |
// Allow CSS syntax to provide surrounded elements, this helps with | |
// library 'exhaustion' but can only be used to surround elements which | |
// can later carry many elements. | |
var surrounds = name.split(/(\s*>\s*)/g); | |
if (surrounds.length > 1) { | |
name = surrounds.pop(); | |
surrounds = surrounds.filter(e => e.indexOf('>') < 0); | |
} else { | |
surrounds = false; | |
} | |
/** | |
* Parse Attributes. | |
* | |
* Parses attributes in string format into an object. | |
* | |
* @param {string} str attributes in string format | |
*/ | |
var parse_attributes = function(str) { | |
var result = {}; | |
if (str && typeof(str) === 'string') { | |
var r = /(?:(?<key>[\w_\-]+)=(?<value>"(?:[^"]+)"|'(?:[^']+)'|(?:[\w\d ]+))(?:\s*$)?)/g; | |
var match = str.matchAll(r); | |
while ((pair = match.next()) && !pair.done && (pair = pair.value)) { | |
result[pair.groups.key] = pair.groups.value.replace(/(^["']|['"]$)/g, ''); | |
} | |
} | |
return result; | |
}; | |
// Check attribute arguments and only allow object/strings to be given | |
if (!(attr && typeof(attr) === 'object' && attr.constructor.name === 'Object')) { | |
// If we're provided with a string then try to interpret all of the | |
// attributes into an object. This might feel painful but allows us | |
// to easily append classes/ids from the selector and gives us a | |
// standard format. For speed, the developer should avoid using | |
// strings as we have to manually fetch them. They should ideally | |
// prefer using objects in this scenario too. | |
attr = parse_attributes(attr); | |
} | |
if ((match = name.matchAll(/\((.*)\)/g).next().value) && match) { | |
// Attributes were passed in the tag | |
name = name.replace(match[0], ''); | |
attr = Object.assign(parse_attributes(match[1]), attr || {}); | |
} | |
var split = name.split(/(#|\.)/); | |
if (split.length > 1) { | |
name = name.replace(/(#|\.)[\w\d\-]+/g, '').trim() || 'div'; | |
var prefixed_class = ''; | |
// Multiple classes can be specified | |
while ((_i = split.indexOf('.')) && _i > -1) { | |
prefixed_class += ' ' + (split[_i + 1] || ''); | |
delete split[_i]; | |
} | |
// Prefix the classes | |
if (prefixed_class) { | |
attr['class'] = (prefixed_class + ' ' + (attr['class'] || '')).trim(); | |
} | |
// Add an ID if not present (attribute takes precedence) | |
if ((_id = split.indexOf('#')) && _id > 0 && _id < split.length - 1) { | |
if (!attr['id']) { | |
attr['id'] = split[_id + 1]; | |
} | |
} | |
} | |
// Join all attributes together. | |
attr = Object.keys(attr).map(function(e) { | |
var value = JSON.stringify(attr[e]); | |
return e + '=' + (value[0] !== '"' ? JSON.stringify(value) : value); | |
}).join(' '); | |
attr = attr || ''; | |
// Allow content to be a list which can be joined | |
if (content && typeof(content) === 'object' && content.constructor.name === 'Array') { | |
content = content.join(''); | |
} | |
content = content || ''; | |
// Finally build the element we require | |
var result = '<' + name + (attr ? ' ' + attr : ''); | |
result += ( | |
self_closing.indexOf(name) > -1 ? | |
'/>' : | |
'>' + content + '</' + name + '>' | |
); | |
// However, surround the element when a CSS syntax heirarchy exists | |
if (surrounds) { | |
while ((surround = surrounds.pop()) && surround) { | |
result = tag(surround, null, result); | |
} | |
} | |
return result; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment