DOM-JON, a JSON rule set for DOM creation
Summary
DOM-JON is for creating DOM fragments that can be appended to a live page from a JSON compliant structure. This is desirable as directly manipulating the DOM is faster, less prone to side effects, and often less bandwidth and memory inensive than methods like innerHTML
. It's even more secure since createTextNode for text content has ZERO risks of XSS exploitation, a common mistake people make with innerHTML
.
Whenever you use innerHTML
the browser has to get the parser involved to turn that new markup into a DOM structure, but also to make sure the new markup doesn't change the behavior of any surrounding markup. This long string processing can create vast overhead in the browser; an overhead you can't even measure from the JavaScript as MOST of it takes place AFTER scripting execution ends. It also often means the browser will create an all-new DOM and re-apply all existing rules to it, doubling down on the memory footprint and processing time.
BUT, if we go directly to the DOM by creating nodes using document.createElement
or document.createTextNode
and adding them using methods like Element.appendChild
or Element.insertBefore
we can bypass all that. Even if it ends up taking more JavaScript execution time, it is overall faster and cleaner.
Finally because it is just JSON it is surprisingly easy to use to create templates server-side for scripted generation WITHOUt getting the excessive overhead of complex string processing such as regex involved. More so it makes it easier to keep values separate from the markup itself -- a constant woe in the age of XSS exploits. ANY time content is added it client-side it should be created with document.createTextNode
meaning no arbitrary code in your content strings can EVER execute!
Overall Structure
A DOM-JON is an Array of values where each entry contains instructions for creating a new element. All array entries when added to the page -- such as with elementals.js' _.Node.write
or _.make
-- will be treated as siblings under any declared target.
If the entry is a string, it will be treated as a delimited selector string (see below). If it is an array, the first entry is your delimited selector string and the second entry is information about how to handle that new element based on the value type as follows:
- Array -- it is assumed to be another DOM-JON containing instructions for making even more elements which will be added to this one as children.
- Object -- it is assumed to be a list of attributes to be applied to the new element, as well as containing 'command attributes' for the creation of even more content, as well as appending the element directly to the DOM.
- String -- will be added as a textNode to this element.
Delimiters and Element Values
In DOM-JON you create elements with what we call a delimited selector string -- "DSS" for short. -- as it is separated by delimiters. It follows a similar notation to CSS selectors for tagname, id, and className, but there are two extra commands involved.
While all the delimiters are optional, they and their contents should appear in the following order when used:
tagName
- The tagname (if any)must be declared FIRST. If you omit a tagName during Element creation
DIV
is used as the default. unless creating a textNode, see the ~ fast text delimiter #
-- id- An optional ID assigned to the new element. If present it should be after the tagname (if any) and before any other delimiters.
.
-- className- A list of period separated classes can be chained to the selector. All will be applied to the new element.
@
-- Special Value Delimiter-
Tag/Atrributes for @ Special Value Delimiter tagName attribute a
href button
type img
src input
type option
value
script
src td
headers th
scope Some tags have attributes that it is handy to be able to declare on the fly, without getting the complex attributes object involved. The "Tag/Atrributes for @ Special Value Delimiter" table shows which tags accept this delimiter value and what attribute the value will be assigned to.
Using this delimiter on tags not on the table will have no effect.
Example:
var sampleDOMJON = ['script@library.js'];
Is roughly equivalent to this markup:
<script src="library.js"></script>
~
-- Fast Content-
Attributes for the "~" Fast Content Delimiter TagName Attribute For all other tags "fast-text" will be appended as a textNode
.img
alt
input
value
textaraea
value
Shorthand for adding text content or values to an element without getting an attribute object involved. In the majority of cases the specified text will be inserted into the new element as its content. Certain tags however will assign it to specific attributes as per the "Attributes for the "~" Fast Content Delimiter" table.
In addition if the tilde is the first character in a selector with no tagName specified, the entire remainder of the string will be created as a
textNode
instead of anElement
node; basically usingdocument.createTextNode
instead ofdocument.createElement
.Some typical usage examples would be: (combined with the ~ delimiter)
var sampleDOMJON = ['img@test.png~Test Image'];
Which results in a DOM similar to what this creates:
<img src="test.png" alt="Test Image">
Whist this:
var sampleDOMJON = ['span#wow.alpha.beta~Some Text'];
Gives us the same thing as:
<span id="wow" class="alpha beta">Some Text</span>
.. and of course this:
var sampleDOMJON = ['~Some Text'];
Would when run through a DOM-JON processor result in the same thing as:
document.createTextNode('Some Text');
- Array -- a DOM-JON that will be used to populate the element.
- Object -- assumed to be an instance of Element and will be appended to the newly created element.
- String -- it will be added to the element as its content as a textNode.
- after -- The new Element will be placed after the value as a sibling
- before -- The new Element will be placed before the value as a sibling
- first -- The new Element will be placed before the Element.firstChild of the value
- last -- The new Element will be placed after the Element.lastChild of the value
- replace -- The new Element will replace the Element stated as the value of this attribute.
Attributes and Commands
When an object is passed as the second index in a 'element' array (where the first index is a delimited selector string) it contains any attributes you wish to set on the element, as well as any commands you want to send. All DOMDocumentElement are valid when creating an Element.
A word about innerHTML
You can pass innerHTML to plug in content as well. Whilst normally writing to innerHTML is generally a bad idea and should be avoided. _.make()
ensures that if you use any of the _.nodeAdd
methods they are done LAST, so that things like innerHTML (and Element.type for IE) are applies AFTER all other attributes. This means innerHTML will be parsed into the element BEFORE it is added to the live document DOM. As a smaller sub-dom it parses faster and will not triggeer a reparse of the entire document, avoiding the headache/woes that normal innerHTML writes can create.
Still it is NOT a method that we recommeded and should be avoided when possible, reserving it for corner cases where you really have no other choice.
onevent
attributes
The elementals.js library will normalize onevent
attributes like onclick
to be applied via Element.addEventListener
with a fallback to element.attachEvent
that polyfills the callback using _.Event.add
. This results in the "event" being properly passed to the callback with Event.target and Event.currentTarget resolving as with modern browsers in legacy IE.
content Command
Primarily the content command exists to belt out large DOM models quickly and easily. It has three behaviors based on what you pass it for a variable type:
Placement Commands
These exist to allow elements from a DOM-JON to be added to the live DOM. Typically you would only use them on a parent-level Attribute object in elementals.js' _.make
or as a parameter for _.Node.write
. As such it is NOT recommended you place them into a static DOM-JON structure, and you REALLY shouldn't try to use them inside any DOM-JON that's nested inside another!
In all cases they expect to be passed a DOMDocumentElement as their value.
DOM-JON Examples
The following are implemented using elementals.js' _.make
and _.Node.write
methods.
Example #1 - A simple _.make
JavaScript
_.make('div#test~This is a test', { last : document.body });
Is roughly equivalent to:
JavaScript
document.body.innerHTML += '<div id="test">This is a test</div>';
Except that it uses the DOM to do it, bypassing the parsing engine.
Example #2, different placement
JavaScript
_.make('h2.scriptHeading', {
content : 'Test',
after : document.getElementsByTagName('h1')[0];
});
Inserts after the first h1
on the page the equivalent of <h2 class="scriptHeading">Test</h2>
, using the DOM instead of browser parsing.
Example #3, a Complex DOM-JON with _.make
JavaScript
_.make('div#test', {
content : [
[ 'h2~This is a DOM created subsection' ],
[ 'p', { innerHTML : 'A child paragraph <strong>that can use markup!</strong>' } ],
[ 'p', [
'A child paragraph',
[ 'strong', 'that directly assigns the <strong> tag.'],
[ '~ Note that markup is escaped when you pass a normal string!' ]
] ],
[ 'div.details' , { content : [
[ 'span' : '13 November 2017' ],
' Jason M. Knight'
] } ]
],
last : document.body
});
Basically creates the same thing on the DOM at the bottom of document.body as the following markup. (just without the whitespace I've added for clarity.)
HTML
<div id="test">
<h2>This is a DOM created subsection</h2>
<p>
A child paragraph <strong>that can use markup!</strong>
</p>
<p>
A child paragraph <strong>that directly assigns the <strong> tag.!</strong> Note that markup is escaped when you pass a normal string!
</p>
<div class="details">
<span>13 November 2017</span>
Jason M. Knight
</div>
</div>
Which is very handy when adding large sections of DOM elements and you want to avoid doing an innerHTML directly onto the live document. Quite often -- most of the time in fact -- writing to the live page with innerHTML can introduce large amounts of overhead in processing time and memory footprint as the entire document has to be reparsed as markup with an entirely new re-organized DOM created. Writing directly to the DOM with _.make bypasses this "problem".
Example #4, using _.Node.write
elementals.js' _.Node.write
method accepts a Element as its first parameter to which the write will occur, then either a string, object, or DOM-JON as its second parameter as per a DOM-JON sub-array's second index, and a 'placement command' string as it's final value. If you omit placement command 'last' is the default behavior.
HTML
<h1>Test</h1>
JavaScript
var
h1 = document.getElementsByTagName('h1')[0],
h2 = document.createElement('h2');
_.Node.write(h1, "\r\nAdded after the H1\r\n", 'after');
_.Node.write(h1, "Added before the H1\r\n", 'before');
_.Node.write(h1, "\r\nAdded before H1 content\r\n", 'first');
_.Node.write(h1, h2, 'after');
_.Node.write(h1, "\r\nAdded after H1 content\r\n", 'last');
_.Node.write(h2, 'Second Heading');
_.Node.write(h2, [
[ 'p~Dynamically added paragraph after the second heading' ],
[ 'p.test', [
content : [
'Another way of adding a paragraph, this time with a class ',
[ 'a', { content : 'and an anchor!', href : '/' } ]
]
] ]
], 'after');
RESULT HTML (equivalent)
Added before the h1
<h1>
Added before H1 content
Test
Added after H1 content
</h1>
Added after the H1
<h2>Second Heading</h2>
<p>Dynamically added paragraph after the second heading</p>
<p class="test">Another way of adding a paragraph, this time with a class <a href="/">and an anchor!</a></p>
In practice much of the above would actually be coded using _.make. In fact, several of _.make
's attributes Object properties call _.Node.write and vice-versa.