Skip to content

Instantly share code, notes, and snippets.

@treshugart
Last active March 10, 2020 06:48
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save treshugart/2fb509a8828adf7fee5245bfa2a54ba7 to your computer and use it in GitHub Desktop.
Save treshugart/2fb509a8828adf7fee5245bfa2a54ba7 to your computer and use it in GitHub Desktop.
Give yourself full control over the DOM that any hyperscript VDOM style function creates http://www.webpackbin.com/4kR0ZnXFf

This is now unmaintained because a repo was created from this idea: https://github.com/skatejs/val

Better VDOM / DOM integration

The goal of this wrapper is to provide a consistent interface across all virtual DOM solutions that provide a hyperscript-style virtual DOM function. This includes, but is not limited to:

  • React
  • Preact
  • Virtual DOM
  • Hyperscript (any implementation)
  • ...

Rationale

The problems these different implemenations face is that the only common thing is the function that you invoke and the arguments that it accepts, at a top level. However, they all behave differently with the arguments you give them.

For example, React will only set props on DOM elements that are in its whitelist. Preact will set props on DOM elements if they are in element. There's problems with each of these.

The problem with React is that you can't pass complex data structures to DOM elements that have properties that aren't in their whitelist, which every web component would be subject to.

With Preact, it's mostly good. However, the assumption is that your custom element definition is defined prior to Preact creating the DOM element in its virtual DOM implementation. This will fail if your custom element definitions are loaded asynchronously, which is not uncommon when wanting to defer the loading of non-critical resources.

Theres other issues such as React not working at all with custom events.

Solution

The best solution I've come across so far is to create a wrapper that works for any of these. The wrapper enables several things:

  • Ability to pass a custom element constructor as the node name (first argument)
  • Explicit control over which attributes to set (via attrs: {} in the second argument)
  • Explicit control over which events are bound (via events: {} in the second argument)
  • Explicit control over which props are set (anything that isn't attrs or events in the second argument)
  • Everything else is just passed through and subject to the standard behaviour of whatever library you're using

Requirements

This assumes that whatever library you're wrapping has support for a ref callback as a common way for us to get access to the raw DOM element that we need to use underneath the hood.

Usage

The usage is simple. You import the wrapper and invoke it with the only argument being the virtual DOM function that you want it to wrap.

React:

import { createElement } from 'react';
import makeCreateElement from './make-create-element';

export makeCreateElement(createElement);

Preact:

import { h } from 'preact';
import makeH from './make-create-element';

export makeH(h);

In your components, you'd then import your wrapped function instead of the one from the library.

/** @jsx h */

import h from 'your-wrapper';
import { PureComponent } from 'react';
import { render } from 'react-dom';

class WebComponent extends HTMLElement {}
class ReactComponent extends PureComponent {
  render () {
    return <WebComponent />;
  }
}

render(<ReactComponent />, document.getElementById('root'));

Attributes

Attributes are specified using the attrs object.

import h from 'your-wrapper';

h('my-element', {
  attrs: {
    'my-attribute': 'some value'
  }
});

Events and custom events

Events are bound using the events object. This works for any events, including custom events.

import h from 'your-wrapper';

h('my-element', {
  events: {
    click () {},
    customevent () {}
  }
});

Properties

Properties are categorised as anything that is not attrs or events.

h('my-element', {
  someProp: true
});

Custom element constructors

You can also use your web component constructor instead of the name that was passed to customElements.define().

// So we have the reference to pass to h().
class WebComponent extends HTMLElement {}

// It must be defined first.
customElements.define('web-component', WebComponent);

// Now we can use it.
h(WebComponent);

Todo

  • If any specified children are real DOM nodes, they should be converted to virtual DOM nodes.
import hify from './create-element';
import React from 'react';
import { render } from 'react-dom';
const h = hify(React.createElement.bind(React));
class Test extends HTMLElement {
static observedAttributes = ['attr']
attributeChangedCallback (name, oldValue, newValue) {
this.innerHTML = `Hello, ${this.getAttribute('attr')}!`;
}
}
customElements.define('x-test', Test);
const attr = 'World';
const click = e => e.target.dispatchEvent(new CustomEvent('custom'));
const custom = e => console.log('custom', e);
const prop = true;
const ReactElement = <Test
prop={prop}
attrs={{ attr }}
events={{ click, custom }}
/>;
render(ReactElement, document.getElementById('root'));
const { customElements, HTMLElement } = window;
const cacheCtorLocalNames = new Map();
const cacheElementEventHandlers = new WeakMap();
// Override customElements.define() to cache constructor local names.
const { define } = customElements;
customElements.define = (name, Ctor) => {
cacheCtorLocalNames.set(Ctor, name);
return define(name, Ctor);
};
// Applies attributes to the ref element. It doesn't traverse through
// existing attributes and assumes that the supplied object will supply
// all attributes that the applicator should care about, even ones that
// should be removed.
function applyAttrs (e, attrs) {
Object.keys(attrs || {}).forEach(name => {
const value = attrs[name];
if (value == null) {
e.removeAttribute(name);
} else {
e.setAttribute(name, value);
}
});
}
function applyEvents (e, events) {
const handlers = cacheElementEventHandlers.get(e) || {};
cacheElementEventHandlers.set(e, events = events || {});
// Remove any old listeners that are different - or aren't specified
// in - the new set.
Object.keys(handlers).forEach(name => {
if (handlers[name] && handlers[name] !== events[name]) {
e.removeEventListener(name, handlers[name]);
}
});
// Bind new listeners.
Object.keys(events || {}).forEach(name => {
if (events[name] !== handlers[name]) {
e.addEventListener(name, events[name]);
}
});
}
// Sets props. Straight up.
function applyProps (e, props) {
Object.keys(props || {}).forEach(name => {
e[name] = props[name];
});
}
// Ensures that if a ref was specified that it's called as normal.
function applyRef (e, ref) {
if (ref) {
ref(e);
}
}
// Ensures attrs, events and props are all set as the consumer intended.
function ensureAttrs (name, objs) {
const { attrs, events, ref, ...props } = objs || {};
const newRef = ensureRef({ attrs, events, props, ref });
return { ref: newRef };
}
// Ensures a ref is supplied that set each member appropriately and that
// the original ref is called.
function ensureRef ({ attrs, events, props, ref }) {
return e => {
if (e) {
applyAttrs(e, attrs);
applyEvents(e, events);
applyProps(e, props);
}
applyRef(e, ref);
};
}
// Returns the custom element local name if it exists or the original
// value.
function ensureLocalName (lname) {
const temp = cacheCtorLocalNames.get(lname);
return temp || lname;
}
// Provides a function that takes the original createElement that is being
// wrapped. It returns a function that you call like you normally would.
//
// It requires support for:
// - `ref`
export default function (createElement) {
return function (lname, attrs, ...chren) {
lname = ensureLocalName(lname);
attrs = typeof lname === 'string' ? ensureAttrs(lname, attrs) : attrs;
return createElement(lname, attrs, ...chren);
};
}
@treshugart
Copy link
Author

treshugart commented Feb 28, 2017

I'm now 🚲 shedding names:

  • val (vdom abstraction layer)
  • kilmer (see above)

These will be scoped under the skatejs org. For example, @skatejs/val.

@matthewrobb
Copy link

  • vcr
  • vans

Those are my top suggestions for you. No explainers, leave it to mystery and deduction.

@matthewrobb
Copy link

Although in the spirit of your suggestions let me put forth:
http://www.ruthlessreviews.com/wp-content/uploads/2011/08/TOP-GUN-ICEMAN1.jpg

"Iceman" or "Ice"

@Hotell
Copy link

Hotell commented Mar 1, 2017

hopefully it will be written in Typescript :P

Naming suggestion:
@skatejs/hifi => high-quality reproduction of vdom :) :D and sounds also cool. And every true skater needs some badass sound system in his riding stack on various deck types ( React, Preact, whatever new VDom renderer )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment