-
-
Save sstur/7379870 to your computer and use it in GitHub Desktop.
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; | |
} |
Mine too! I am reading another site's window with jsdom and sending it to the client as an object passed through toJSON
.
Nice one.
amazing!
thanks! really cool!
Thanks!!!
Example, sorting https://jsfiddle.net/chefk6uu/
increase, decrease and revert.
But, I found simple solution: https://jsfiddle.net/m6365sc6/
You need a few more steps to map shadow dom (open) and iframes (non-sandboxed) for serialization but the rehydration gets trickier since those can have state, templates, etc.
Thanks 👍
So helpful !!!
Thank you a LOT.
Thank you for sharing this--super neat!
@sstur I seem to be having trouble with it saving and restoring radio values. The DOM elements are being saved but when restored, the item that was selected no longer has the checked attribute.
Thanks for the feedback!
Actually, this code is so old, over 6 years now, and hasn't been updated or tested in a a long time, that it doesn't surprise me if there are some edge cases that aren't handled properly. But I'll see if I can repro the issue you mentioned and maybe there's a simple fix.
@sstur Thanks, so much, for getting back to me!
Actually, I figured out a work around--maybe not even really a work around based on how radios work? When the radio option is clicked, its checked property is set. That is fine if the form is submitted without going through toJSON and then toDOM. After looking at your code a bit I added a small function (to mine) that adds/removes the attribute "checked" as appropriate and that did the trick!
Again, thank you, for sharing your code. The project I'm working on is enabling an app to be offline and the functions you wrote are a godsend as I really wanted to serialize the form and deserialize the form, not the form values.
I've updated it to better handle form elements and runtime state (such as checkboxes and radio items) and it's now using more modern JS syntax. Hope this helps!
@sstur, wow! Sweet! Thanks, Dude. I'll give the new version a go!
This has been really useful to me over the last few months. I've had to change some things, maybe in doing so I've made errors. But I had to pay attention to the size of the JSON file. Has anyone else noticed that all the empty fields on the inline style attribute of the DOM node are saved to json? I changed it to this and it works better.
if (attrs) {
let attrNames = new Map();
for (let i = 0; i < attrs.length; i++) {
attrNames.set(attrs[i].nodeName, undefined);
}
let arr = [];
for (let [name, defaultValue] of attrNames) {
let value;
if (name === 'style') {
//if we query the node with the attribute getter [], we get a CSS style declaration, not a string.
//it is possible to get the cssText string though
value = node.style.cssText;
//then we can just push that to the array
arr.push(['style', value]);
} else {
value = node[name];
if (value !== defaultValue) { arr.push([name, value]); }
}
}
if (arr.length) { obj.attributes = arr; }
}
This is a great point @tomgallagher! I'll update the code above to better handle style properties.
Edit: Done.
For those using Typescript, or want the consts
that can be lets
be converted:
export function toJson(node: Node & Record<string, unknown>) {
const propFix = { for: 'htmlFor', class: 'className' };
const specialGetters = {
style: (node: HTMLElement) => node.style.cssText,
};
const attrDefaultValues = { style: '' };
const obj: any = {
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;
}
const attrs = node.attributes;
if (attrs) {
const defaultValues = new Map();
for (let i = 0; i < attrs.length; i++) {
const 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;
}
}
const arr = [];
for (const [name, defaultValue] of defaultValues) {
const propName = propFix[name] || name;
const specialGetter = specialGetters[propName];
const value = specialGetter ? specialGetter(node) : node[propName];
if (value !== defaultValue) {
arr.push([name, value]);
}
}
if (arr.length) {
obj.attributes = arr;
}
}
const childNodes = node.childNodes;
// Don't process children for a textarea since we used `value` above.
if (obj.tagName !== 'textarea' && childNodes && childNodes.length) {
const arr = (obj.childNodes = []);
for (let i = 0; i < childNodes.length; i++) {
arr[i] = toJson(childNodes[i] as any);
}
}
return obj;
}
export function toDom(input: Node) {
const obj = typeof input === 'string' ? JSON.parse(input) : input;
const propFix = { for: 'htmlFor', class: 'className' };
let node: Node;
const nodeType = obj.nodeType;
switch (nodeType) {
// ELEMENT_NODE
case 1: {
node = document.createElement(obj.tagName);
if (obj.attributes) {
for (const [attrName, value] of obj.attributes) {
const 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 (const childNode of obj.childNodes) {
node.appendChild(toDom(childNode));
}
}
return node;
}
@sstur could you add a license?
Licenced as ISC. License follows:
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
HI, I want to convert Html to Json using TOJSON() function but it doesn't capture special attributes like data-glide_el.
<div class="glide" id="glide_1" >
<div class="glide__track" data-glide-el="track">
<div class="hero__center">
<div class="hero__left">
<span class="">New Inspiration 2020</span>
</div>
<div class="hero__right">
<div class="hero__img-container">
<img class="banner_01" src="./images/banner_01.jpg" alt="banner2" />
</div>
</div>
</div>
</div>
</div>
In the same manner, I am don't know how to generate the same HTML from JSON already created in the above step.
where can I provide my root node in toDOM() function so that it can add the generated code to it?
it doesn't capture special attributes like data-glide-el
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.
don't know how to generate the same HTML from JSON
If you need HTML output, I'd suggest you first create DOM and then convert that to HTML using innerHTML
or something similar.
where can I provide my root node in toDOM()
toDOM()
doesn't require a root node, instead it returns a node. You can attach that node to your document using something like document.body.appendChild(node)
.
Can we support SVG too?
This saved my day, thanks a lot mate!