Last active
October 8, 2023 04:17
-
-
Save sstur/7379870 to your computer and use it in GitHub Desktop.
Stringify DOM nodes using JSON (and revive again)
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
function toJSON(node) { | |
let propFix = { for: 'htmlFor', class: 'className' }; | |
let specialGetters = { | |
style: (node) => node.style.cssText, | |
}; | |
let attrDefaultValues = { style: '' }; | |
let obj = { | |
nodeType: node.nodeType, | |
}; | |
if (node.tagName) { | |
obj.tagName = node.tagName.toLowerCase(); | |
} else if (node.nodeName) { | |
obj.nodeName = node.nodeName; | |
} | |
if (node.nodeValue) { | |
obj.nodeValue = node.nodeValue; | |
} | |
let attrs = node.attributes; | |
if (attrs) { | |
let defaultValues = new Map(); | |
for (let i = 0; i < attrs.length; i++) { | |
let name = attrs[i].nodeName; | |
defaultValues.set(name, attrDefaultValues[name]); | |
} | |
// Add some special cases that might not be included by enumerating | |
// attributes above. Note: this list is probably not exhaustive. | |
switch (obj.tagName) { | |
case 'input': { | |
if (node.type === 'checkbox' || node.type === 'radio') { | |
defaultValues.set('checked', false); | |
} else if (node.type !== 'file') { | |
// Don't store the value for a file input. | |
defaultValues.set('value', ''); | |
} | |
break; | |
} | |
case 'option': { | |
defaultValues.set('selected', false); | |
break; | |
} | |
case 'textarea': { | |
defaultValues.set('value', ''); | |
break; | |
} | |
} | |
let arr = []; | |
for (let [name, defaultValue] of defaultValues) { | |
let propName = propFix[name] || name; | |
let specialGetter = specialGetters[propName]; | |
let value = specialGetter ? specialGetter(node) : node[propName]; | |
if (value !== defaultValue) { | |
arr.push([name, value]); | |
} | |
} | |
if (arr.length) { | |
obj.attributes = arr; | |
} | |
} | |
let childNodes = node.childNodes; | |
// Don't process children for a textarea since we used `value` above. | |
if (obj.tagName !== 'textarea' && childNodes && childNodes.length) { | |
let arr = (obj.childNodes = []); | |
for (let i = 0; i < childNodes.length; i++) { | |
arr[i] = toJSON(childNodes[i]); | |
} | |
} | |
return obj; | |
} | |
function toDOM(input) { | |
let obj = typeof input === 'string' ? JSON.parse(input) : input; | |
let propFix = { for: 'htmlFor', class: 'className' }; | |
let node; | |
let nodeType = obj.nodeType; | |
switch (nodeType) { | |
// ELEMENT_NODE | |
case 1: { | |
node = document.createElement(obj.tagName); | |
if (obj.attributes) { | |
for (let [attrName, value] of obj.attributes) { | |
let propName = propFix[attrName] || attrName; | |
// Note: this will throw if setting the value of an input[type=file] | |
node[propName] = value; | |
} | |
} | |
break; | |
} | |
// TEXT_NODE | |
case 3: { | |
return document.createTextNode(obj.nodeValue); | |
} | |
// COMMENT_NODE | |
case 8: { | |
return document.createComment(obj.nodeValue); | |
} | |
// DOCUMENT_FRAGMENT_NODE | |
case 11: { | |
node = document.createDocumentFragment(); | |
break; | |
} | |
default: { | |
// Default to an empty fragment node. | |
return document.createDocumentFragment(); | |
} | |
} | |
if (obj.childNodes && obj.childNodes.length) { | |
for (let childNode of obj.childNodes) { | |
node.appendChild(toDOM(childNode)); | |
} | |
} | |
return node; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
True. The reason is that data attributes don't change the way the static HTML displays, so it doesn't make sense to capture them for the original goals of this script.
In most cases, the purpose of data attributes is for JavaScript to do something with the data, but toJSON() also doesn't capture JavaScript or JS-related attributes (like
onclick
).So I'd say data attributes are out of scope for this, but I'd be open to hear if there's a good use for adding support.
If you need HTML output, I'd suggest you first create DOM and then convert that to HTML using
innerHTML
or something similar.toDOM()
doesn't require a root node, instead it returns a node. You can attach that node to your document using something likedocument.body.appendChild(node)
.