Skip to content

Instantly share code, notes, and snippets.

@developit
Last active July 25, 2023 12:45
Show Gist options
  • Select an option

  • Save developit/1409519fe1d62fb02a64b35a2e2fb66f to your computer and use it in GitHub Desktop.

Select an option

Save developit/1409519fe1d62fb02a64b35a2e2fb66f to your computer and use it in GitHub Desktop.

Revisions

  1. developit revised this gist Feb 11, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion *Rendering Interactive HTML using Preact.md
    Original file line number Diff line number Diff line change
    @@ -91,7 +91,7 @@ function convert(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let props = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) props[a[i].name] = a[i].value;
    + if (props.action === 'toggle') props.onclick = function() { this.classList.toggle('active') };
    + if (props.action === 'toggle') props.onclick = function() { this.classList.toggle('active') };
    return h(node.localName, props, [].map.call(node.childNodes, convert)); // recurse children
    }
    ```
  2. developit revised this gist Feb 11, 2021. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion *Rendering Interactive HTML using Preact.md
    Original file line number Diff line number Diff line change
    @@ -114,4 +114,10 @@ const actions = {
    };

    render(<Html html={html} actions={actions} />, document.body);
    ```
    ```

    ---

    For a demo of this working in practise, here's an HTML-based VDOM editor:

    https://jsfiddle.net/developit/narb8qmo/
  3. developit revised this gist Feb 11, 2021. 1 changed file with 10 additions and 7 deletions.
    17 changes: 10 additions & 7 deletions *Rendering Interactive HTML using Preact.md
    Original file line number Diff line number Diff line change
    @@ -10,39 +10,42 @@ There is another technique available that melds HTML to Virtual DOM without such

    An alternative approach taken by libraries like [preact-markup](https://github.com/developit/preact-markup)
    is to parse the HTML using the browser's `DOMParser` API, then convert it to Virtual DOM and render that.
    It's also surprisingly simple.

    It's surprisingly simple. First we need to parse the HTML into a DOM structure:
    First we need to parse the HTML into a DOM structure:

    ```js
    const html = `<button class="foo">Click Me</button>`;

    const dom = new DOMParser().parseFromString(html, 'text/html');
    ```

    Then, we pass that DOM tree to a convert() function, which recurses through it to produce an equivalent Virtual DOM tree.
    We can render the resulting Virtual DOM tree using Preact:
    Then, we pass that DOM tree to a function that will recurse through it and produce the equivalent Virtual DOM tree:

    ```js
    // convert the DOM tree to a Virtual DOM tree:
    const vdom = convert(dom.body.firstElementChild);
    ```

    The `convert()` function we referenced there takes a DOM node and returns the equivalent Virtual DOM node.
    It also loops over the child DOM nodes, creating a nested structure:
    Our `convert()` function takes a DOM node and returns the equivalent Virtual DOM node.

    It also calls itself on each of the DOM node's children, creating a nested structure:

    ```js
    import { h } from 'preact';

    // convert a DOM node to a Virtual DOM node:
    function convert(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let props = {}, a = node.attributes;
    let props = {}, a = node.attributes; // attributes --> props
    for (let i=0; i<a.length; i++) props[a[i].name] = a[i].value;
    return h(node.localName, props, [].map.call(node.childNodes, convert)); // recurse children
    }
    ```

    Finally, we can render the resulting Virtual DOM tree using Preact. The tree we created works just like JSX - it can be nested in other trees or returned from a component:
    Finally, we can render the resulting Virtual DOM tree using Preact.

    The tree we created works just like JSX - it can be nested in other trees or returned from a component:

    ```js
    import { render } from 'preact';
  4. developit revised this gist Feb 11, 2021. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions *Rendering Interactive HTML using Preact.md
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,8 @@ It's possible to render HTML in Preact using the `dangerouslySetInnerHTML` prop,
    however doing so bypasses the Virtual DOM entirely. This may be reasonable for
    static HTML, but interactivity can be a little painful to graft on without VDOM.

    There is another technique available that melds HTML to Virtual DOM without such limitations.

    ## Enter `DOMParser`

    An alternative approach taken by libraries like [preact-markup](https://github.com/developit/preact-markup)
  5. developit renamed this gist Feb 11, 2021. 1 changed file with 0 additions and 0 deletions.
  6. developit revised this gist Feb 11, 2021. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion *render-html.md
    Original file line number Diff line number Diff line change
    @@ -52,7 +52,7 @@ render(vdom, document.body);
    render(<div class="content">{vdom}</div>, document.body);
    ```

    ### Adding Dynamism
    ## Adding Dynamism

    We can process the Virtual DOM nodes our `convert()` function creates in order to inject functionality.
    In Preact, all Virtual DOM elements created by `h()` can be intercepted using a [`vnode` options hook](https://preactjs.com/guide/v10/options/):
    @@ -91,6 +91,8 @@ function convert(node) {
    }
    ```

    ## Putting it all together

    This is the technique used by the little `render-html.js` library provided in this Gist.
    A list of `actions` can be passed into the `<Html>` component, which are available to be bound as event handlers by HTML using `on:foo="action"` attributes:

  7. developit revised this gist Feb 11, 2021. 2 changed files with 60 additions and 17 deletions.
    75 changes: 58 additions & 17 deletions *render-html.md
    Original file line number Diff line number Diff line change
    @@ -1,33 +1,61 @@
    ## Rendering Interactive HTML using Preact
    # Rendering Interactive HTML using Preact

    It's possible to render HTML in Preact using the `dangerouslySetInnerHTML` prop,
    however doing so bypasses the Virtual DOM entirely. This may be reasonable for
    static HTML, but interactivity can be a little painful to graft on without VDOM.

    ## Enter `DOMParser`

    An alternative approach taken by libraries like [preact-markup](https://github.com/developit/preact-markup)
    is to parse the HTML using the browser's `DOMParser` API, then convert it to Virtual DOM and render that.

    It's surprisingly simple:
    It's surprisingly simple. First we need to parse the HTML into a DOM structure:

    ```js
    const html = `<button class="foo">Click Me</button>`;

    const dom = new DOMParser().parseFromString(html, 'text/html');
    ```

    Then, we pass that DOM tree to a convert() function, which recurses through it to produce an equivalent Virtual DOM tree.
    We can render the resulting Virtual DOM tree using Preact:

    ```js
    // parse the HTML to a DOM:
    const dom = new DOMParser().parseFromString(`<button class="foo">Click Me</button>`, 'text/html');
    // convert the DOM tree to a Virtual DOM tree:
    const vdom = convert(dom.body.firstElementChild)
    // render it:
    render(vdom, document.body);
    const vdom = convert(dom.body.firstElementChild);
    ```

    The `convert()` function we referenced there takes a DOM node and returns the equivalent Virtual DOM node.
    It also loops over the child DOM nodes, creating a nested structure:

    ```js
    import { h } from 'preact';

    // convert a DOM node to a Virtual DOM node:
    function convert(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let attrs = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) attrs[a[i].name] = a[i].value;
    return h(node.localName, attrs, [].map.call(node.childNodes, convert));
    let props = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) props[a[i].name] = a[i].value;
    return h(node.localName, props, [].map.call(node.childNodes, convert)); // recurse children
    }
    ```

    We can also process the DOM (or the VDOM, since that's a bit easier). Preact allows
    intercepting all Virtual DOM elements created by `h()` using the `vnode`
    [options hook](https://preactjs.com/guide/v10/options/):
    Finally, we can render the resulting Virtual DOM tree using Preact. The tree we created works just like JSX - it can be nested in other trees or returned from a component:

    ```js
    import { render } from 'preact';

    // render it:
    render(vdom, document.body);

    // or render it inside some other JSX:
    render(<div class="content">{vdom}</div>, document.body);
    ```

    ### Adding Dynamism

    We can process the Virtual DOM nodes our `convert()` function creates in order to inject functionality.
    In Preact, all Virtual DOM elements created by `h()` can be intercepted using a [`vnode` options hook](https://preactjs.com/guide/v10/options/):

    ```js
    import { options } from 'preact';
    @@ -48,16 +76,29 @@ options.vnode = vnode => {
    };
    ```

    This works, but our modifications will be applied to the entire application, since options hooks are global.
    To achieve this only within our HTML-derived Virtual DOM tree, we can process the tree as we create it -
    we're already "walking" the tree to convert DOM nodes to Virtual DOM nodes.
    This works, but any modifications apply to the entire application, since options hooks are global.

    To inject functionality only into our HTML-derived Virtual DOM tree, we can process the tree as we create it.
    We're already "walking" the tree to `convert()` DOM nodes to Virtual DOM nodes, which provides a nice place to intercept them:

    ```diff
    function convert(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let props = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) props[a[i].name] = a[i].value;
    + if (props.action === 'toggle') props.onclick = function() { this.classList.toggle('active') };
    return h(node.localName, props, [].map.call(node.childNodes, convert)); // recurse children
    }
    ```

    This is the technique used by the little `render-html.js` library provided in this Gist:
    This is the technique used by the little `render-html.js` library provided in this Gist.
    A list of `actions` can be passed into the `<Html>` component, which are available to be bound as event handlers by HTML using `on:foo="action"` attributes:

    ```js
    import { Html } from './render-html.js';

    const html = `<button on:click="greet" data-greeting="Bill">Click Me</button>`;
    // ^ on click, call the greet action

    const actions = {
    greet(e) {
    2 changes: 2 additions & 0 deletions render-html.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    import { h } from 'preact';

    let actions; // current actions to apply when walking the tree

    function Html(props) {
  8. developit revised this gist Feb 11, 2021. 2 changed files with 28 additions and 13 deletions.
    22 changes: 18 additions & 4 deletions *render-html.md
    Original file line number Diff line number Diff line change
    @@ -13,15 +13,15 @@ It's surprisingly simple:
    // parse the HTML to a DOM:
    const dom = new DOMParser().parseFromString(`<button class="foo">Click Me</button>`, 'text/html');
    // convert the DOM tree to a Virtual DOM tree:
    const vdom = domToVdom(dom.body.firstElementChild)
    const vdom = convert(dom.body.firstElementChild)
    // render it:
    render(vdom, document.body);

    function domToVdom(node) {
    function convert(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let attrs = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) attrs[a[i].name] = a[i].value;
    return h(node.localName, attrs, node.childNodes.map(walk));
    return h(node.localName, attrs, [].map.call(node.childNodes, convert));
    }
    ```

    @@ -52,4 +52,18 @@ This works, but our modifications will be applied to the entire application, sin
    To achieve this only within our HTML-derived Virtual DOM tree, we can process the tree as we create it -
    we're already "walking" the tree to convert DOM nodes to Virtual DOM nodes.

    This is the technique used by the little `render-html.js` library provided in this Gist!
    This is the technique used by the little `render-html.js` library provided in this Gist:

    ```js
    import { Html } from './render-html.js';

    const html = `<button on:click="greet" data-greeting="Bill">Click Me</button>`;

    const actions = {
    greet(e) {
    alert('Hello ' + this.getAttribute('data-greeting'));
    }
    };

    render(<Html html={html} actions={actions} />, document.body);
    ```
    19 changes: 10 additions & 9 deletions render-html.js
    Original file line number Diff line number Diff line change
    @@ -1,18 +1,19 @@
    let _actions;
    function Html({ html, actions }) {
    const dom = new DOMParser().parseFromString(html, 'text/html');
    _actions = actions || {};
    return dom.body.childNodes.map(walk);
    let actions; // current actions to apply when walking the tree

    function Html(props) {
    const dom = new DOMParser().parseFromString(props.html, 'text/html');
    actions = props.actions || {};
    return [].map.call(dom.body.childNodes, convert);
    }

    function walk(node) {
    function convert(node) {
    if (node.nodeType === 3) return node.data;
    let attrs = {};
    for (let i=0; i<node.attributes.length; i++) {
    const { name, value } = node.attributes[i];
    const m = name.match(/^(?:on:|data-on-?)(.+)$/);
    if (m) attrs['on'+m[1]] = actions[value];
    const m = name.match(/^(?:on:|data-on-?)(.+)$/); // <a on:click="go" data-on-mouseover="blink">
    if (m && actions[value]) attrs['on'+m[1]] = actions[value];
    else attrs[name] = value;
    }
    return h(node.localName, attrs, node.childNodes.map(walk));
    return h(node.localName, attrs, [].map.call(node.childNodes, convert));
    }
  9. developit created this gist Feb 11, 2021.
    55 changes: 55 additions & 0 deletions *render-html.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    ## Rendering Interactive HTML using Preact

    It's possible to render HTML in Preact using the `dangerouslySetInnerHTML` prop,
    however doing so bypasses the Virtual DOM entirely. This may be reasonable for
    static HTML, but interactivity can be a little painful to graft on without VDOM.

    An alternative approach taken by libraries like [preact-markup](https://github.com/developit/preact-markup)
    is to parse the HTML using the browser's `DOMParser` API, then convert it to Virtual DOM and render that.

    It's surprisingly simple:

    ```js
    // parse the HTML to a DOM:
    const dom = new DOMParser().parseFromString(`<button class="foo">Click Me</button>`, 'text/html');
    // convert the DOM tree to a Virtual DOM tree:
    const vdom = domToVdom(dom.body.firstElementChild)
    // render it:
    render(vdom, document.body);

    function domToVdom(node) {
    if (node.nodeType === 3) return node.data; // Text nodes --> strings
    let attrs = {}, a = node.attributes;
    for (let i=0; i<a.length; i++) attrs[a[i].name] = a[i].value;
    return h(node.localName, attrs, node.childNodes.map(walk));
    }
    ```

    We can also process the DOM (or the VDOM, since that's a bit easier). Preact allows
    intercepting all Virtual DOM elements created by `h()` using the `vnode`
    [options hook](https://preactjs.com/guide/v10/options/):

    ```js
    import { options } from 'preact';

    let old = options.vnode;
    options.vnode = vnode => {
    // Example 1: add a click handler to <button action="toggle"> elements:
    if (vnode.props.action === 'toggle') {
    vnode.props.onclick = function() {
    this.classList.toggle('active');
    };
    }

    // Example 2: convert <a> to <Link>
    if (vnode.type === 'a') vnode.type = Link;

    if (old) old(vnode); // call the next hook
    };
    ```

    This works, but our modifications will be applied to the entire application, since options hooks are global.
    To achieve this only within our HTML-derived Virtual DOM tree, we can process the tree as we create it -
    we're already "walking" the tree to convert DOM nodes to Virtual DOM nodes.

    This is the technique used by the little `render-html.js` library provided in this Gist!
    18 changes: 18 additions & 0 deletions render-html.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,18 @@
    let _actions;
    function Html({ html, actions }) {
    const dom = new DOMParser().parseFromString(html, 'text/html');
    _actions = actions || {};
    return dom.body.childNodes.map(walk);
    }

    function walk(node) {
    if (node.nodeType === 3) return node.data;
    let attrs = {};
    for (let i=0; i<node.attributes.length; i++) {
    const { name, value } = node.attributes[i];
    const m = name.match(/^(?:on:|data-on-?)(.+)$/);
    if (m) attrs['on'+m[1]] = actions[value];
    else attrs[name] = value;
    }
    return h(node.localName, attrs, node.childNodes.map(walk));
    }