object-to-html-renderer

1.3.3 • Public • Published

A simplistic toolbox to render Javascript objects as HTML nodes dynamically in a DOM

Installation

npm install object-to-html-renderer

Usage

The Object to HTML renderer module allows you to represent HTML components with javascript objects, and render them in the DOM.

Example:

An HTML node representation for a div with class "a-div", with id "my-div", and nesting a span and a button with an onclick handler:

{
    tag: "div",
    class: "a-div",
    id: "my-div",
    contents: [
        {
            tag:"span",
            contents: "Checkout that button ->"
        },
        {
            tag:"button",
            contents: "Click me",
            onclick: () => alert("Hello !"),
        },
    ]
}

tag

The tag key is required and represents the tag of the node we want to insert. It can be any existing html tag like div, section, ul, li, canvas, iframe, br, ... anything.

contents

The contents key must be be either a String or an Array of objects similar to the parent object (= with a tag key), so you can nest components inside each other.

example: html form

{
    tag: "form",
    onsubmit: e => {
        e.preventDefault();
        // Do something with the submitted data
    },
    contents: [
        {
            tag: "div",
            class:"form-input-block",
            contents: [
                {tag:"label", for: "login", contents: "Your login"},
                {tag:"input", name:"login", type:"text"}
            ]
        },
        {
            tag: "div",
            class:"form-input-block",
            contents: [
                {tag:"label", for: "password", contents: "Your password"},
                {tag:"input", name:"password", type:"password"}
            ]
        },
        {
            tag: "div",
            class:"form-input-block",
            contents: [
                {tag:"input",type:"submit", value:"ok"}
            ]
        }
    ]
}

This will be render in html DOM like this:

<form onsubmit="... the callback function you have set">
	<div class="form-input-block">
		<label for="login">Your login</label>
		<input name="login" type="text" />
	</div>
	<div class="form-input-block">
		<label for="password">Your password</label>
		<input name="password" type="password" />
	</div>
	<div class="form-input-block">
		<input type="submit" value="ok" />
	</div>
</form>

All the other keys will be used as attributes for the inserted html node, so a value: "hello" entry will set a value attribute on the html node and set its value to "hello", an onclick: () => ... entry will set the onclick attribute with the specified callback. It will also work for custom html5 attributes like some_data: {... whatever data you need}.

style_rules

You can also set a style_rules entry that will be use to set the style of the node. The style_rules entry must be a single object and each key must be a valid styling attribute, written with js camel case syntax.

However, the style_rules object cannot handle the pseudo elements or pseudo class rules like :hover or ::after, etc.

example: list

{
    tag: "ul",
    style_rules: {
        display: "flex",
        margin: 0,
        padding: 0,
        flexDirection: "column",
        listStyleType: "none", // The css attribute names must be used in their camel case version, like when you set it directly from js: element.style.listStyleType="none"
    },
    contents: ["some", "listed", "data"].map(text_item => {
        return {
            tag: "li",
            contents: [
                {
                    tag:"span",
                    contents: text_item,
                    style_rules: {
                        padding: "10px",
                    },
                }
            ]
        }
    })
}

on_render

You can set an on_render attribute with a callback that will be called once the element will be appended to the DOM. The appended html_node is automatically passed as an argument to the callback so you can use it directly.

render() {
	return {
		tag: "p",
		contents: "Some text in a paragraph",
		on_render: html_node => {
			console.log("This node has been rendered to the DOM", html_node);
			html_node.innerHTML = "Do something with your html node";
		},
	}
}

Root component initialization

In order to use the module, you must define a root component and initialize the library with it:

const renderer = require("object-to-html-renderer");

class RootComponent {
	render() {
		return {
			tag: "main", // the tag for the root component must be <main>
			contents: [{ tag: "h1", contents: "Hello, world !" }],
		};
	}
}

renderer.setRenderCycleRoot(new RootComponent()); // Set the component root in the renderer object
renderer.renderCycle(); // Call this method to fire the first rendering cycle

To make this work you will need to previoulsy create a basic html document in which you will have set the script tag calling the javascript bundle built from your project source (you can use something simple like Browserify to do this, or Babel, or Gulp, it's up to you, everything should work the same.) A minimum html document should look like this

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>Object to Html - Test project</title>
		<link rel="stylesheet" href="path-to-some/stylesheet.css" />
	</head>

	<body>
		<!-- The main tag used to render the app. If it is not provided it will be created and appended to document.body. -->
		<main></main>
	</body>

	<script src="path-to-your/bundle.js"></script>
</html>

Dynamic rendering

The whole point of doing all this is of course to allow a dynamic interaction with the DOM rendering, in a similar way frameworks like Reactjs or Vuejs do.

This lib is very different from React or Vue, because those frameworks do a lot of work for you that isn't done here. But it has the advantage to be a lot less heavy, and it remains quite flexible.

The renderCycle function

const renderer = require("object-to-html-renderer");
renderer.renderCycle();

Calling this method will trigger the object to html transformation from the root component and refresh the all tree in the html DOM.

Logically you won't have to call this a lot. You call it at least once at the start of the script in order to create your entire component tree for the first time. But next times you rather will be calling the subRender method that allow you to refresh only the node you want in the DOM.

The subRender function

Signature

subRender(object, htmlNode, (options = { mode: "append" }));

The subRender method will take a "renderable" object as an argument (= with at least a tag entry and optional contents or other attributes), this object will be transform in an actual html node and it will be inserted in the given htmlNode. The insertion mode can be specified in the options argument.

Arguments

  • object: This is the object you want to transform, it must have a tag entry, and can have multiple nested objects in its contents entry
  • htmlNode: This is the html node DOM reference you want to target.
  • options: The options argument is an object that accepts a mode entry. Modes can be:
    • "append" (default): this will perform an appendChild of your object on the htmlNode target.
    • "override": will override the current inner content of the htmlNode by the input object. Its implemented with a simple htmlNode.innerHTML = your_object_as_html
    • "insertBefore": Will insert your object before a specified index inside the htmlNode. The insertion index must be specified in options with an insertIndex entry. Ex: { mode:"insert-before", insertIndex:2 }. This is implemented with htmlNode.insertBefore(your_object_as_html, htmlNode.childNodes[options.insertIndex]);
    • "adjacent": Will append your object next to the htmlNode. Your have to specify the insertion location in the options with a insertLocation entry. This is implemented with htmlNode.insertAdjacentHTML(options.insertLocation, your_object_as_html);. InsertLocation must be one of: - "afterbegin" - "afterend" - "beforebegin" - "beforeend"
    • "replace": Will replace the htmlNode by your object. Implementation: htmlNode.parentNode.replaceChild(your_object_as_html, htmlNode);
    • "remove": Will remove the htmlNode simply calling htmlNode.remove();. In this case your input object will be useless but as it can't be undefined you can just give an empty div like {tag:"div"}

Usage example - refresh after fetch response:

const renderer = require("object-to-html-renderer");

class DataListComponent {
	constructor() {
		this.render_data = [];
		this.list_id = "my-data-list";
	}

	async fetchData() {
		const fetchData = await (await fetch(`some/json/data/url`)).json();
		return fetchData;
	}

	renderDataItem(item) {
		return {
			tag: "div",
			contents: [
				// Whatever you want to do to render your data item...
			],
		};
	}

	renderDataList() {
		return {
			tag: "ul",
			id: this.list_id,
			contents: this.render_data.map(data_item => {
				return {
					tag: "li",
					contents: [this.renderDataItem(data_item)],
				};
			}),
		};
	}

	render() {
		return {
			tag: "div",
			contents: [
				{
					tag: "button",
					contents: "fetch some data !",
					onclick: async () => {
						const data = await this.fetchData();
						this.render_data = data;
						renderer.subRender(
							this.renderDataList(),
							document.getElementById(this.list_id),
							{ mode: "replace" },
						);
					},
				},
				this.renderDataList(),
			],
		};
	}
}

class RootComponent {
	render() {
		return {
			tag: "main", // the tag for the root component must be <main>
			contents: [new DataListComponent().render()],
		};
	}
}

renderer.setRenderCycleRoot(new RootComponent());
renderer.renderCycle();

Rendering events

The renderer object will fire a custom event each time you call a render cycle with renderCycle or subRender.

This event is dispatched on the window level. So if needed you can catch the rendering events anywhere in your app like this:

window.addEventListener("objtohtml-render-cycle", e => {
	// Something you want to do after a render cycle has been triggered.
});

The events provides some data about its origin:

function youEventListener(render_event) {
	const event_data = render_event.detail;
	console.log(event_data);
	/* output
    {
        inputObject: {... the js object that was given as argument},
        outputNode: {... the input object transformed as an html node},
        insertOptions: {... the options argument that was given},
        targetNode: {... the target html node reference that was given},
    }
    */
}

Window registration

You can register the renderer as a window attribute in order to make it accessible everywhere:

const MyRootComponent = require("./my-root-component");
const renderer = require("object-to-html-renderer");

// Register the renderer with the key "obj2html" or whatever name you like to give to the renderer object. If left empty a default "objectToHtmlRenderer" key will be used.
renderer.register("obj2htm");

// Start using your renderer anywhere.
obj2htm.setRenderCycleRoot(new MyRootComponent());
obj2htm.renderCycle();

Asynchronous subscriptions

Sometimes a component will need to give its reference to an asynchronous closure, like a timeout or an setInterval routine.

Is these case there is a risk that at some point the rendering tree will be refresh and the instance of component that have been referenced in the closure will be destroyed. But as the reference will still be counted in the closure scope, it will cause a memory leak.

To avoid that, we can register the subscription along with a cleanup callback.

renderer.registerAsyncSubscription(htmlNodeID: String, cleanupCallback:Function);

If the id of a component having been referenced in an asynchronous scope is found in the new rendering cycle, then the cleanup callback will be called.

EX:

const renderer = require("object-to-html-renderer");

class ComponentSubscribesSetInterval {
    constructor() {
        this.count = 0;
        this.id = "setintervalcpt";
        this.run_interval();
    }

    run_interval() {
        this.interval_id = setInterval(() => this.count++, 1000);

        renderer.registerAsyncSubscription(
			this.id, 
			() => clearInterval(this.interval_id)
		);
    }

    render() {
        return {
            tag: "div",
            id: this.id,
        }
    }
}

class App {
	constructor() {
		this.state = {
			render_interval_component: true,
		};
	}

	render() {
		return {
			tag:"div",
			contents: this.state.render_interval_component ? [
				new ComponentSubscribesSetInterval().render(), // This component will be recreated or destroyed if render is trigger again, and the reference in setInterval will have no valid HTML to refer to.
			] : [{tag:"span"}]
		}
	}
}

const app = new App();

renderer.setRenderCycleRoot(app);
renderer.renderCycle();

app.state.render_setint_cpt = false;

renderer.renderCycle(); // The setInterval subscription will be cleaned by this call

Package Sidebar

Install

npm i object-to-html-renderer

Weekly Downloads

3

Version

1.3.3

License

LGPL-3.0

Unpacked Size

47.8 kB

Total Files

13

Last publish

Collaborators

  • kuadrado-software