VanJS Tutorial

VanJS: Tutorial and API Reference

In this tutorial, we will break down into 3 core functionalities VanJS supports: DOM composition / manipulation, State and State binding.

DOM Composition and Manipulation


Your first VanJS app: a simple Hello page

We will start this tutorial with a simple Hello page, with the code below:

const {a, div, li, p, ul} = van.tags

const Hello = () => div(
  p("👋Hello"),
  ul(
    li("🗺️World"),
    li(a({href: "https://vanjs.org/"}, "🍦VanJS")),
  ),
)

van.add(document.body, Hello())

Try on jsfiddle

The code should be self-explanatory if you have some familiarity with HTML. Unlike React, everything in the code above is just pure JavaScript, meaning that you are simply calling functions from van.js without any transpiling that converts your source code into another form. Reusable UI components built with VanJS can be pure vanilla JavaScript functions as well. Here we capitalize the first letter to follow React conventions.

Also unlike React, VanJS does not introduce an ad-hoc virtual DOM layer. All the tag functions above directly return the created DOM objects. e.g.: the function call p("👋Hello") simply creates an HTMLParagraphElement with 👋Hello as its innerText, meaning that you can directly interact with your created DOM nodes with native DOM APIs.

API reference: van.tags

van.tags is a top-level dynamic object in VanJS implemented with Proxy. van.tags.<name> gets you a function that creates an HTML element with tag name <name>. A common way of using van.tags is like the line below:

const {a, div, p} = van.tags

With the line, a, div, p are functions that create <a>, <div>, <p> HTML elements respectively.

We will use div function as an example, the API reference for div tag function is as below:

Signaturediv([props], ...children) => <the created DOM element>
DescriptionCreates an HTMLDivElement with props as its properties and children as its child nodes.
Parameters
  • props - optional, a plain JavaScript object whose keys and values are the keys and values of the properties of the created HTML element. Keys should be string, and values can be primitives (string, number, boolean or bigint), primitive-valued State objects, or State-derived properties. We will explain the behavior of State-typed and State-derived properties in State Binding section below. For keys like on..., value should be function to represent the event handler.
  • children - caller can provide 0 or more children as arguments to represent the child nodes of the created HTML element. Each child can be a valid DOM node, a primitive (string, number, boolean or bigint), null, undefined, a primitive-valued or null/undefined-valued State object, or an Array of children. null/undefined-valued children are only allowed in 0.12.1 or later and will be ignored. A Text node will be created for each primitive-typed argument. We will explain the behavior of State-typed child in State Binding section below. For DOM node, it shouldn't be already connected to a document tree (isConnected property should be false). i.e.: You should not declare an existing DOM node in the current document as the child node of the newly created element.
ReturnsThe HTMLDivElement object just created.

SVG and MathML Support

Requires VanJS 0.12.0 or later.

HTML elements with namespace URI can be created via van.tagsNS, a variant of van.tags that allows you to specify the namespaceURI of the created elements. Here is an example of composing the SVG DOM tree with van.tagsNS:

const {circle, path, svg} = van.tagsNS("http://www.w3.org/2000/svg")

const Smiley = () => svg({width: "16px", viewBox: "0 0 50 50"},
  circle({cx: "25", cy: "25", "r": "20", stroke: "black", "stroke-width": "2", fill: "yellow"}),
  circle({cx: "16", cy: "20", "r": "2", stroke: "black", "stroke-width": "2", fill: "black"}),
  circle({cx: "34", cy: "20", "r": "2", stroke: "black", "stroke-width": "2", fill: "black"}),
  path({"d": "M 15 30 Q 25 40, 35 30", stroke: "black", "stroke-width": "2", fill: "transparent"}),
)

van.add(document.body, Smiley())

Demo:

Try on jsfiddle

Similarly, math formulas can be created with MathML elements via van.tagsNS:

const {math, mi, mn, mo, mrow, msup} = van.tagsNS("http://www.w3.org/1998/Math/MathML")

const Euler = () => math(
  msup(mi("e"), mrow(mi("i"), mi("π"))), mo("+"), mn("1"), mo("="), mn("0"),
)

van.add(document.body, Euler())

Demo:

Try on jsfiddle

API reference: van.tagsNS

Requires VanJS 0.12.0 or later.
Signaturevan.tagsNS(namespaceURI) => <the created tags object for elements with specified namespaceURI>
DescriptionCreates a tags Proxy object similar to van.tags for elements with specified namespaceURI.
Parameters
  • namespaceURI - a string for the namespaceURI property of elements created via tag functions.
ReturnsThe created tags object.

API reference: van.add

van.add function is similar to tag functions described above. Instead of creating a new HTML element with specified properties and children, van.add function mutates its first argument (which is an existing Element node) by appending 0 or more children with appendChild calls:

Signaturevan.add(dom, ...children) => dom
DescriptionMutates dom by appending 0 or more child nodes to it. Returns dom for possibly further chaining.
Parameters
  • dom - an existing DOM element that we want to append children to.
  • children - caller can provide 0 or more children as arguments to represent the child nodes we want to append to dom. Each child can be a valid DOM node, a primitive, null, undefined, a primitive-valued or null/undefined-valued State object, or an Array of children. null/undefined-valued children are only allowed in 0.12.1 or later and will be ignored. A Text node will be created for each primitive-typed argument. State-typed child behave the same way as in tag function. For DOM node, it shouldn't be already connected to a document tree (isConnected property should be false). i.e.: You should not append an existing DOM node in the current document to dom. If 0 children is provided, this function is a no-op.
Returnsdom

DOM nodes already in the document tree can't be used as children

As mentioned in the API reference, if a DOM node is already connected to the document tree, it shouldn't be used as the child node of tag function or van.add. The following code is invalid and an Error will be thrown when van-<version>.debug.js is being used:

const existing = document.getElementById("some-id")

// Invalid! Existing node can't be used as the child node of tag function.
const dom = div({id: "new-id"}, existing)

// Invalid! Existing node can't be appended to other nodes in `van.add`.
van.add(document.body, existing)

Functional-style DOM tree building

Because both tag functions and van.add can take Array arguments and the Array arguments can be deeply nested. VanJS enables very ergonomic DOM tree composition in functional-style. See examples below:

Building a bullet list:

const {li, ul} = van.tags

const List = ({items}) => ul(items.map(it => li(it)))

van.add(document.body, List({items: ["Item 1", "Item 2", "Item 3"]}))

Try on jsfiddle

Building a table:

const {table, tbody, thead, td, th, tr} = van.tags

const Table = ({head, data}) => table(
  head ? thead(tr(head.map(h => th(h)))) : [],
  tbody(data.map(row => tr(
    row.map(col => td(col)),
  ))),
)

van.add(document.body, Table({
  head: ["ID", "Name", "Country"],
  data: [
    [1, "John Doe", "US"],
    [2, "Jane Smith", "CA"],
    [3, "Bob Johnson", "AU"],
  ],
}))

Try on jsfiddle

on... event handlers

In tag functions, you can provide a function value for property keys like on.... This is a convenient way to specify event handlers. For instance, the code below creates a button that shows an alert whenever clicked:

button({onclick: () => alert("Hello from 🍦VanJS")}, "Hello")

Try on jsfiddle

🎉 Congratulations! You have mastered the skills for building and manipulating DOM trees using VanJS's declarative API, which is incredibly powerful for creating comprehensive applications with elegant code. In the sections below, you will continue to learn how to build reactive applications with state and state binding.

If your application doesn't rely on state and state binding, you can use the slimmed-down version of VanJS - Mini-Van.

State


A State object in VanJS represents a value that can be updated throughout your application. A State object has a public property val, with a custom setter that automatically propagates changes to DOM nodes that are bound to it. In addition, you can register your event handler to listen to updates of a State object via its onnew method.

The code below illustrates how a State object can be used:

const {button, div, input, sup} = van.tags

// Create a new State object with init value 1
const counter = van.state(1)

// Log whenever the value of the state is updated
counter.onnew((v, oldV) => console.log(`Counter: ${oldV} -> ${v}`))

// Used as a child node
const dom1 = div(counter)

// Used as a property
const dom2 = input({type: "number", value: counter, disabled: true})

// Used in a state-derived property
const dom3 = div(
  {style: {deps: [counter], f: c => `font-size: ${c}em;`}},
  "Text")

// Used in a complex binding
const dom4 = van.bind(counter, c => div(c, sup(2), ` = ${c * c}`))

// Button to increment the value of the state
const incrementBtn = button({onclick: () => ++counter.val}, "Increment")
const resetBtn = button({onclick: () => counter.val = 1}, "Reset")

van.add(document.body, incrementBtn, resetBtn, dom1, dom2, dom3, dom4)

Demo:

Try on jsfiddle

API reference: van.state

Signaturevan.state(initVal) => <the created State object>
DescriptionCreates a State object with its init value specified in the argument.
Parameters
  • initVal - the init value of the State object to be created.
ReturnsThe created State object.

Public interface of State objects

  • Property val - the current value of the State object. When a new value of this property is set, all event handlers registered via onnew method will be called and all DOM nodes that bound to it will be updated accordingly. Note that: while setting val, if the provided value is the same as the current one, event handlers and DOM updates will be skipped.
  • Method onnew(l) - register an event handler to listen to the updates of the State object. Whenever a new value is assigned to the state, the event handler l will be called with 2 arguments: v and oldV, representing the new and old value of the state.

The value of a State object can be almost anything, primitive, Object, Array, null, etc., with 2 ad-hoc exceptions that we made: The value of the State object cannot be a DOM node, or another State object. Having values in these 2 types carries little semantic information and is more likely a result of coding bugs. Thus we disallow State objects to have values in these 2 types. In van-{version}.debug.js, an explicit error will be thrown if you try to assign a DOM node or another State object as the value of a state.

State.val is immutable

While you can update State objects by setting the val property, you should never mutate the underlying object of val itself. Doing so will not trigger the DOM tree update as you would expect and might result in undefined behavior due to aliasing. In van-<version>.debug.js, attempt to mutate the object in val will lead to an Error to be thrown.

State Binding


Once State objects are created, we can bind them to DOM nodes in various ways to make your UI reactive to state changes.

State objects as child nodes

State objects can be used as child nodes in tag functions and van.add, like the Counter example shown in the home page. For a State object used as a child node, its value needs to be primitive (string, number, boolean or bigint), and a Text node will be created for it. The content of the created Text node will be always in sync with the value of the state.

The following code shows how to build a simple timer with this feature:

const {button, span} = van.tags

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

const Timer = ({totalSecs}) => {
  const secs = van.state(totalSecs);
  return span(
    secs, "s ",
    button({onclick: async () => {
      while (secs.val > 0) await sleep(1000), --secs.val
      await sleep(10) // Wait briefly for DOM update
      alert("⏰: Time is up")
      secs.val = totalSecs
    }}, "Start"),
  )
}

van.add(document.body, Timer({totalSecs: 5}))

Demo:

Try on jsfiddle

State objects as properties

State objects can be used as properties of HTML elements. Similar to State-based child nodes, the value of the properties will be always in sync with the value of the respective states. When State objects are used as properties, you need to make sure that the values of the states are always valid property values, i.e.: primitives or functions (for event handlers).

The following code demonstrates 2 text inputs whose values are always in sync:

const {input, span} = van.tags

const ConnectedProps = () => {
  const text = van.state("")
  return span(
    input({type: "text", value: text, oninput: e => text.val = e.target.value}),
    input({type: "text", value: text, oninput: e => text.val = e.target.value}),
  )
}

van.add(document.body, ConnectedProps())

Demo:

Try on jsfiddle

State-derived properties

State-derived property is a more advanced way to bind a property of an HTML element to one or more underlying State objects. To use State-derived properties, you need to provide an object with the following fields as the value in props argument while calling to a tag function:

  • deps - an Array of one or more dependencies.
  • f - a function that takes the values of states in deps as parameters. The return value of f should always be valid property values, i.e.: primitives or functions (for event handlers).

The example below is a live font size and color preview implemented with this feature:

const {input, option, select, span} = van.tags

const FontPreview = () => {
  const size = van.state(16), color = van.state("black")
  return span(
    "Size: ",
    input({type: "range", min: 10, max: 36, value: size,
      oninput: e => size.val = e.target.value}),
    " Color: ",
    select({oninput: e => color.val = e.target.value, value: color},
      ["black", "blue", "green", "red", "brown"]
        .map(c => option({value: c}, c)),
    ),
    span({style: {deps: [size, color], f: (size, color) =>
      `font-size: ${size}px; color: ${color};`}}, " Hello 🍦VanJS"),
  )
}

van.add(document.body, FontPreview())

Demo:

Try on jsfiddle

Complex State binding

You can call van.bind to bind an HTML node to one or more State objects in a custom way, as specified in a generation function that you provide. The following example illustrates this:

const {input, li, option, select, span, ul} = van.tags

const SortedList = () => {
  const items = van.state("a,b,c"), sortedBy = van.state("Ascending")
  return span(
    "Comma-separated list: ",
    input({oninput: e => items.val = e.target.value,
      type: "text", value: items}), " ",
    select({oninput: e => sortedBy.val = e.target.value, value: sortedBy},
      option({value: "Ascending"}, "Ascending"),
      option({value: "Descending"}, "Descending"),
    ),
    van.bind(items, sortedBy, (items, sortedBy) =>
      sortedBy === "Ascending" ?
        ul(items.split(",").sort().map(i => li(i))) :
        ul(items.split(",").sort().reverse().map(i => li(i)))),
  )
}

van.add(document.body, SortedList())

Demo:

Try on jsfiddle

API reference: van.bind

Signaturevan.bind(dep1, dep2, ..., depN, f) => <the created DOM node>
DescriptionCreates a DOM node that binds to dependencies: dep1, dep2, ..., depN. Whenever the values of these states change, the generation function - f, will be called to update the DOM tree.
Parameters
  • dep1, dep2, ..., depN - the dependencies bound to the DOM node.
  • f - The generation function, with signature f(v1, v2, ..., vN, [dom, oldV1, oldV2, ..., oldVN]) => <primitive, DOM node, null or undefined>. Whenever any value of dep1, dep2, ..., depN changes, f will be called and returns the new version of the DOM node based on new values of the dependencies. Optionally, f can take dom (the current version of the bound DOM node) and oldV1, oldV2, ...,oldVN (the old values of the dependencies) as additional parameters to enable Stateful binding, which might sometimes choose to mutate existing DOM node instead of generating a new one as an optimization. When f returns a primitive, a Text node will be created based on its content. When f returns null or undefined, the DOM node will removed. Removed DOM node will never be brought back, even when f would return a non-null/undefined value based on future values of the dependencies.
ReturnsThe created DOM node that are bound to dependencies.

Polymorphism between State and non-State dependencies

Requires VanJS 0.12.0 or later.

State-derived properties and van.bind can accept both State and non-State objects as dependency arguments. This polymorphism makes it handy to build reusable components where users can specify both state and non-state property values. Non-state dependencies behave the same way as state dependencies whose val properties never change. Below is an example of a reuseable button whose color, text and onclick properties can be both state and non-state objects:

const {button, span} = van.tags

const Button = ({color, text, onclick}) => button({
  style: {deps: [color], f: color => `background-color: ${color};`},
  onclick,
}, text)

const App = () => {
  const colorState = van.state("green")
  const textState = van.state("Turn Red")

  const turnRed = () => {
    colorState.val = "red"
    textState.val = "Turn Green"
    onclickState.val = turnGreen
  }
  const turnGreen = () => {
    colorState.val = "green"
    textState.val = "Turn Red"
    onclickState.val = turnRed
  }
  const onclickState = van.state(turnRed)

  return span(
    Button({color: "yellow", text: "Click Me", onclick: () => alert("Clicked")}), " ",
    Button({color: colorState, text: textState, onclick: onclickState}),
  )
}

van.add(document.body, App())

Demo:

Try on jsfiddle

Removing a DOM node

As noted in the API reference above, when generation function f returns null or undefined, the DOM node will removed. Removed DOM node will never be brought back, even when f would return a non-null/undefined value based on future values of the dependencies.

The following code illustrates how to build an editable list with this features:

const {a, button, div, input, li, ul} = van.tags

const ListItem = ({text}) => {
  const deleted = van.state(false)
  return van.bind(deleted, d => d ? null : li(
    text, a({onclick: () => deleted.val = true}, "❌"),
  ))
}

const EditableList = () => {
  const listDom = ul()
  const textDom = input({type: "text"})
  return div(
    textDom, " ", button({
      onclick: () => van.add(listDom, ListItem({text: textDom.value})),
    }, "➕"),
    listDom,
  )
}

van.add(document.body, EditableList())

Demo:

Try on jsfiddle

Stateful binding

While dealing with state updates, a user can choose to, instead of regenerating the new version of the DOM node entirely based on new state values, mutate the existing DOM node that is already connected to the document tree based on all the new values and old values of its dependencies. This feature can be used as an optimization to avoid the entire DOM subtree being completely re-rendered.

The following code is a snippet of the auto-complete application which leverages this feature to optimize:

const suggestionList = van.bind(candidates, selectedIndex,
  (candidates, selectedIndex, dom, oldCandidates, oldSelectedIndex) => {
    if (dom && candidates === oldCandidates) {
      // If candidate list doesn't change, we don't need to re-render the
      // suggestion list. Just need to change the selected candidate.
      dom.querySelector(`[data-index="${oldSelectedIndex}"]`)
        ?.classList?.remove("selected")
      dom.querySelector(`[data-index="${selectedIndex}"]`)
        ?.classList?.add("selected")
      return dom
    }
    return SuggestionList({candidates, selectedIndex})
  })

The piece of code above is building a suggestionList that is reactive to the changes of selection candidates and selectedIndex. When selection candidates change, the suggestionList needs to be regenerated. However, if only selectedIndex changes, we only need to update the DOM element to indicate that the new candidate is being selected now, which can be achieved by changing the classList of relevant candidate elements.

The generation function f can either return dom (the existing node in the document tree), or a newly created DOM node. When a newly created DOM node is returned, it shouldn't be already connected to a document tree (isConnected property should be false)

Note that, when the generation function is being called for the first time, dom and oldV1, oldV2, ...,oldVN will all be undefined. Thus the generation function of stateful binding needs to handle this situation as well. This is why in Line 3, we're checking dom in the if statement.

The End


🎉 Congratulations! You have completed the entire tutorial of VanJS. Now you can start your journey of building feature-rich applications!

To learn more, you can:

API Index


Below is the list of all top-level APIs in VanJS: