Search code examples
javascripthyperhtml

Isomorphic hyperHTML components without passing in wires


I have the following two components:

// component.js
// imports ...

function ListItem(item) {
  const html = wire(item)

  function render() {
    return html`<li>${item.foo}</li>`
  }

  return render()
}

function List(items) {
  const html = wire(items)

  function render() {
    return html`<ul>${items.map(ListItem)}</ul>`
  }

  return render()
}

I want to put them in a module which is shared between the client and the server. However, as far as I can tell, although the API pretty much identical, on the server I have to import the functions from the viperHTML module, on the client I have to use the hyperHTML module. Therefore I can not just import the functions at the top of my shared module, but have to pass to my components at the call site.

Doing so my isomorphic component would look like this:

// component.js

function ListItem(html, item) {
  //const html = wire(item) // <- NOTE

  function render() {
    return html`<li>${item.foo}</li>`
  }

  return render()
}

function List(html, itemHtmls /* :( tried to be consistent */, items) {
  //const html = wire(items) // <- NOTE

  function render() {
    return html`<ul>${items.map(function(item, idx) {
      return ListItem(itemHtmls[idx], item)
    })}</ul>`
  }

  return render()
}

Calling the components from the server:

// server.js
const {hyper, wire, bind, Component} = require('viperhtml')

const items = [{foo: 'bar'}, {foo: 'baz'}, {foo: 'xyz'}]
// wire for the list
const listWire = wire(items)
// wires for the children
const listItemWires = items.map(wire)

const renderedMarkup = List(listWire, listItemWires, items)

Calling from the browser would be the exact same, expect the way hyperhtml is imported:

// client.js
import {hyper, wire, bind, Component} from 'hyperhtml/esm'

However it feels unpleasant to write code like this, because I have a feeling that the result of the wire() calls should live inside the component instances. Is there a better way to write isomorphic hyperHTML/viperHTML components?


Solution

  • update there is now a workaround provided by the hypermorphic module.


    The ideal case scenario is that you have as dependency only viperhtml, which in turns brings in hyperhtml automatically, as you can see by the index.js file.

    At that point, the client bundler should, if capable, tree shake unused code for you but you have a very good point that's not immediately clear.

    I am also not fully sure if bundlers can be that smart, assuming that a check like typeof document === "object" would always be true and target browsers only.

    One way to try that, is to

    import {hyper, wire, bind, Component} from 'viperhtml'
    

    on the client side too, hoping it won't bring in viperHTML dependencies once bundled 'cause there's a lot you'd never need on the browser.

    I have a feeling that the result of the wire() calls should live inside the component instances.

    You could simplify your components using viper.Component so that you'll have render() { return this.html... } and you forget about passing the wire around but I agree with you there's room for improvements.

    At that point you only have to resolve which Component to import in one place and define portable components that work on b both client and server.

    This is basically the reason light Component exists in the first place, it give you the freedom to focus on the component without thinking about what to wire, how and/or where (if client/server).

    ~~I was going to show you an example but the fact you relate content to the item (rightly) made me think current Component could also be improved so I've created a ticket as follow up for your case and I hope I'll have better examples (for components) sooner than later.~~

    edit

    I have updated the library to let you create components able to use/receive data/items as they're created, with a code pen example.

    class ListItem extends Component {
      constructor(item) {
        super().item = item;
      }
      render() {
        return this.html`<li>${this.item.foo}</li>`;
      }
    }
    
    class List extends Component {
      constructor(items) {
        super().items = items;
      }
      render() {
        return this.html`
        <ul>${this.items.map(item => ListItem.for(item))}</ul>`;
      }
    }
    

    When you use components you are ensuring yourself these are portable across client/server.

    The only issue at this point would be to find out which is the best way to retrieve that Component class.

    One possible solution is to centralize in a single entry point the export of such class.

    However, the elephant in the room is that NodeJS is not compatible yet with ESM modules and browsers are not compatible with CommonJS so I don't have the best answer because I don't know if/how you are bundling your code.

    Ideally, you would use CommonJS which works out of the box in NodeJS and is compatible with every browser bundler, and yet you need to differentiate, per build, the file that would export that Component or any other hyper/viperHTML related utilities.

    I hope I've gave you enough hints to eventually work around current limitations.

    Apologies if for now I don't have a better answer. The way I've done it previously used external renders but it's quite possibly not the most convenient way to go with more complex structures / components.


    P.S. you could write those functions just like this

    function ListItem(item) {
      return wire(item)`<li>${item.foo}</li>`;
    }
    
    function List(items) {
      return wire(items)`<ul>${items.map(ListItem)}</ul>`;
    }