Skip to content

Latest commit

 

History

History
319 lines (265 loc) · 6.6 KB

File metadata and controls

319 lines (265 loc) · 6.6 KB

Core Concepts

Virtual Nodes

A virtual node is an object that describes an HTML/DOM tree.

It consists of a tag, e.g. div, svg, etc., data attributes and an array of child nodes.

{
  tag: "div",
  data: {
    id: "app"
  },
  children: [{
    tag: "h1",
    data: null,
    children: ["Hi."]
  }]
}

The virtual DOM engine consumes a virtual node and produces an HTML tree.

<div id="app">
  <h1>Hi.</h1>
</div>

You can use the h(tag, data, children) utility function to create virtual nodes.

h("div", { id: "app" }, [
  h("h1", null, "Hi.")
])

Or setup a build pipeline and use Hyperx or JSX instead.

Data Attributes

Any valid HTML attributes/properties, events, styles, etc.

data: {
  id: "myButton",
  class: "PrimaryButton",
  onclick: () => alert("Hi."),
  disabled: false,
  style: {
    fontSize: "3em"
  }
}

Attributes also include lifecycle events and meta data such as keys.

Applications

Use the app(props) function to create an application.

app({
  view: () => <h1>Hi.</h1>
})

The app function renders the given view and appends it to document.body.

To mount the application on a different element, use the root property.

app({
  view: () => <h1>Hi.</h1>,
  root: document.getElementById("app")
})

View and State

The view is a function of the state. It is called every time the state is modified to rebuild the application's virtual node tree, which is used to update the DOM.

app({
  state: "Hi.",
  view: state => <h1>{state}</h1>
})

Use the state to describe your application's data model.

app({
  state: ["Hi", "Hola", "Bonjour"],
  view: state => (
    <ul>
      {state.map(hello => <li>{hello}</li>)}
    </ul>
  )
})

Actions

Use actions to update the state.

app({
  state: "Hi.",
  view: (state, actions) => (
    <h1 onclick={actions.ucase}>{state}</h1>
  ),
  actions: {
    ucase: state => state.toUpperCase()
  }
})

To update the state, an action must return a new state or a part of it.

app({
  state: 0,
  view: (state, actions) => (
    <main>
      <h1>{state}</h1>
      <button onclick={actions.addOne}>+1</button>
    </main>
  ),
  actions: {
    addOne: state => state + 1
  }
})

You can pass data to actions as well.

app({
  state: 0,
  view: (state, actions) => (
    <main>
      <h1>{state}</h1>
      <button
        onclick={() => actions.addSome(1))}>More
      </button>
    </main>
  ),
  actions: {
    addSome: (state, actions, data = 0) => state + data
  }
})

Actions are not required to have a return value. You can use them to call other actions, for example after an asynchronous operation has completed.

app({
  state: 0,
  view: (state, actions) => (
    <main>
      <h1>{state}</h1>
      <button onclick={actions.addOneDelayed}></button>
    </main>
  ),
  actions: {
    addOne: state => state + 1,
    addOneDelayed: (state, actions) => {
      setTimeout(actions.addOne, 1000)
    }
  }
})

An action may return a promise. This enables you to use async functions.

const delay = seconds =>
  new Promise(done => setTimeout(done, seconds * 1000))

app({
  state: 0,
  view: (state, actions) => (
    <main>
      <h1>{state}</h1>
      <button onclick={actions.addOneDelayed}>+1</button>
    </main>
  ),
  actions: {
    addOne: state => state + 1,
    addOneDelayed: async (state, actions) => {
      await delay(1)
      actions.addOne()
    }
  }
})

Namespaces

Namespaces let you organize actions into categories and help reduce name collisions as your application grows larger.

app({
  state: 0,
  view: (state, actions) => (
    <main>
      <button onclick={actions.counter.add}>+</button>
      <h1>{state}</h1>
      <button onclick={actions.counter.sub}>-</button>
    </main>
  ),
  actions: {
    counter: {
      add: state => state + 1,
      sub: state => state - 1
    }
  }
})

Events

Use events to get notified when your application is completely loaded, an action is called, before a view is rendered etc.

app({
  state: { x: 0, y: 0 },
  view: state => (
    <h1>{state.x + ", " + state.y}</h1>
  ),
  actions: {
    move: (state, { x, y }) => ({ x, y })
  },
  events: {
    loaded: (state, actions) =>
      addEventListener("mousemove", e =>
        actions.move({
          x: e.clientX,
          y: e.clientY
        })
      )
  }
})

Events can be used to hook into the update and render pipeline.

app({
  view: state => <h1>Hi.</h1>,
  events: {
    render: (state, actions, data) => {
      if (location.pathname === "/warp") {
        return state => <h1>Welcome to warp zone!</h1>
      }
    }
  }
})

For a practical example see the implementation of the Router.

Custom Events

To create custom events, use the emit(event, data) function. This function is passed as the last argument to actions/events.

app({
  view: (state, actions) =>
    <button onclick={actions.fail}>Fail</button>,
  actions: {
    fail: (state, actions, data, emit) =>
      emit("error", "Fail")
  },
  events: {
    error: (state, actions, error) => {
      throw error
    }
  }
})

Mixins

Use mixins to extend your application state, actions and events in a modular fashion.

const Logger = () => ({
  events: {
    action: (state, actions, data) => console.log(data)
  }
})

app({
  state: 0,
  view: (state, actions) =>
    <main>
      <h1>{state}</h1>
      <button onclick={actions.addOne}>+1</button>
    </main>,
  actions: {
    addOne: state => state + 1
  },
  mixins: [Logger]
})