/hypnode

Generate HTML node trees from JSX or direct function calls

Primary LanguageTypeScript

A super fast, lightweight (roughly 1kb gzipped) and reactive series of utility functions, used to build HTML node trees and stateful functional components. Can be used either directly via the h function, or from transpiled JSX.

Due to it's size, hypnode is a useful companion utility for writing WebComponents, simplifying HTML structures, element references and state management. See section entitled "WebComponents" for an example, click to jump there.

Getting Started

To install hypnode, you can use one of the following in your project:

yarn add hypnode or npm install hypnode

hypode can be used in one of two ways, either as a target for JSX transpilation, or directly using the exposed h function. It's exported as ES6, so if you need to support older environments, you'll need to transpile down to ES5 in your build tasks.

The h function can be imported in one of the following ways:

import { h } from 'hypnode';
const { h } = require('hypnode');

Direct Usage

Once imported, use the function to generate your tree of DOM Nodes. The function takes 3 arguments, the last two of which are optional:

h([type]: string | Function, [attributes]?: object, [children]?: array[]);

Simple Example

The code below:

import { h, render } from 'hypnode';

/*[...]*/

const result = render(
  h('div', { title: 'A DIV!' }, [
    h('h1', { class: 'title' }, 'Hypnode'),
    h('p', { id: 'text'}, 'My text value'),
  )
);

console.log(result.outerHTML);

Will produce the following:

<div title="A DIV!">
  <h1 class="title">Hypnode</h1>
  <p id="text">My text value</p>
</div>

JSX Elements

hypnode can be used with JSX to provide a more familiar API when building DOM structures. This will need a transpilation step, see below for examples.

TypeScript

Transpilation of JSX is provided out of the box by custom factories (TypeScript 1.8+), to apply this, add the following to your tsconfig.json file:

"compilerOptions": {
   "jsx": "react",
   "jsxFactory": "h",
   /*[...]*/
}

This tells the TypeScript compiler to convert all JSX elements into function calls, in this case using our exported h function. You'll still need to import the h function in every file where you're using JSX.

To apply the correct types, all files that contain JSX must have the extension .tsx.

Simple Example

The code below:

import { h, render } from 'hypnode';

/*[...]*/

const root = document.getElementId('root');

const result = render(
  <div class="wrapper">
    <a id="link" onClick={(ev = console.log(ev))}>
      Click here
    </a>
  </div>
);

root.appendChild(result);

Will produce the following:

<div class="wrapper">
  <a id="link">
    Click here
  </a>
</div>

Rendering

As the render() function exported by hypnode returns a fully formed HTMLElement, you can handle it's output easily using native DOM API's like root.appendChild or root.replaceChild. There is, however, an optional secondary argument that can handle this for you, e.g:

const result = h('div', { class: 'wrapper' }, 'Lorem ipsum');

/*[...]*/

render(result, document.getElementById('root'));

or, with JSX:

const result = <div class="wrapper">Lorem ipsum</div>;

/*[...]*/

render(result, document.getElementById('root'));

Event Binding

hypnode provides a set of properties for you to apply DOM events. All native events are supported, formatted in camelCase and prefixed with on. For example:

h('a', { onClick: (ev) => console.log(ev) }, 'Click Here');

or, with JSX:

<input onKeyUp={(ev) => console.log(ev)} />

Element References

If you need access to a particular node in your tree, use the ref property. For example:

let myElement;

/*[...]*/

h('div', { id: 'container' }, [
  h('p', { ref: (el) => (myElement = el) }, 'Lorem ipsum dolor sit amet, consectetur'),
]);

or with JSX:

let myElement;

/*[...]*/

<div class="wrapper">
  <a href="//link.co" ref={(el) => (myElement = el)}>
    Click here
  </a>
</div>;

Components

hypnode can be used to create re-usable, functional components, below is a simple example:

import { h, render } from 'hypnode';

/*[...]*/

function Button({ className = '', children }) {
  return h('a', { class: `button ${className}` }, children);
}

/*[...]*/

const button = h(Button, { className: 'big' }, buttonText));

render(button, document.getElementById('root'))

or with JSX:

import { h, render } from 'hypnode';

/*[...]*/

function Button({ className = '', children }) {
  return <a class={`button ${className}`}>{children}</a>;
}

/*[...]*/

const button = <Button className="big">Click here</Button>;

render(button, document.getElementById('root'));

WebComponents

Creating and managing complex HTML structures using WebComponents can become tricky as their size increases. hypnode simplifies this by incorporating a familiar component and state pattern into your custom elements. You can find more info on WebComponents here. As hypnode is fast and lightweight, WebComponents can be more easily shared between projects without significant dependency overhead.

A quick example can be found below:

// myComponent.tsx (TypeScript + JSX)

import { h, render, useState, State } from  'hypnode';

/*[...]*/

class MyComponent extends HTMLElement {
  private state: State<number>;

  public connectedCallback() {
    const Button = () => this.renderButton();

    this.appendChild(render(<Button  />));
  }

  private  renderButton = () => {
    const [counter] = (this.state =  useState(0));

    return (
      <div class="my-component">
        <p>Counter: {counter}</p>
        <button onClick={this.onClick}>Click Here</button>
      </div>
    );
  }

  private onClick = (ev: Event) => {
    const [counter, setState] = this.state;

    setState(counter + 1);
  }
}

State

hypnode exposes a simple, declarative hook to provide state into your functional application.

First, you'll need to import the hook function:

import { h, useState } from 'hypnode';

Once imported, you can initialize a state registry in your components by doing the following:

const [state, setState] = useState(([value]: any));

The useState function takes a single argument, the initial value you wish to assign to the state. This can be anything, a primitive or something more complex like an object. For example:

function Button({ buttonText }) {
  const [state, setState] = useState(10);

  return h('button', { onClick: () => setState(state + 1) }, `${buttonText}: ${state}`);
}

You provide mutations to your state via the setState function, this accepts a new value you wish to assign. Whenever this function is called, the component will be re-rendered with the replaced state value.

Effects

hypnode exports a simple effect hook, useEffect. This takes a callback as it's only argument which will be called on "mount". You can optionally return another function from it that will be called on "un-mount". The lifecycle of this function works in a similar way to React's hook by the same name.

See example below:

import { h, useEffect } from 'hypnode';

/*[...]*/

function Banner({ children }) {
  useEffect(() => {
    console.log('onMount');

    return () => console.log('onUnMount (optional)');
  });

  return h('div', { class: 'banner' }, children);
}

Server Side Rendering

hypnode provides a Virtual DOM representation for server side rendering (universal rendering). You can use this output to generate a complete HTML representation of your app. A prebuilt utility that can convert this into an HTML string can be found here: hypnode-server

TypeScript

This utility was created with TypeScript and comes pre-bundled with a definition file.