X

VanX: The 1.0 kB Official VanJS Extension

VanX is the official extension of VanJS, which provides handy utility functions. VanX makes VanJS more ergonomic for certain use cases and its developer experience closer to other popular UI frameworks. Like VanJS, VanX is also ultra-lightweight, with just 1.0kB in the gzipped minified bundle.

Installation


Via NPM

VanX is published as NPM package vanjs-ext. Run the following command to install the package:

npm install vanjs-ext

Add this line to your script to import the package:

import * as vanX from "vanjs-ext"

You can also import individual utility functions you're going to use:

import { <functions you want to use> } from "vanjs-ext"

Via a Script Tag

Alternatively, you can import VanX from CDN via a <script type="text/javascript"> tag:

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vanjs-ext@0.2.4/dist/van-x.nomodule.min.js"></script>

https://cdn.jsdelivr.net/npm/vanjs-ext@0.2.4/dist/van-x.nomodule.js can be used for the non-minified version.

Note that: VanJS needs to be imported via a <script type="text/javascript"> tag for VanX to work properly.

TypeScript Support for Script Tag Integration

To get TypeScript support for <script> tag integration, download van-x-0.2.4.d.ts and add the following code at the top of your .ts file:

import type * as vanXType from "./van-x-0.2.4.d.ts"

declare const vanX: typeof vanXType

vanX.reactive: Reactive Object to Hold Many Individual States


vanX.reactive provides an ergonomic way to define a single reactive object where each of its individual fields corresponds to an underlying State object. For instance:

const obj = vanX.reactive({a: 1, b: 2})

defines a reactive object with the following underlying state fields:

{a: van.state(1), b: van.state(2)}

The reactive objects defined by vanX.reactive can be deeply nested. For instance:

const obj = vanX.reactive({
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
})

defines a reactive object with the following underlying state fields:

{
  a: van.state(1),
  b: van.state({
    c: van.state(2),
    d: van.state(3),
  }),
}

Getting and setting values of the underlying states can be simply done by getting / setting the fields of the reactive object. For instance, obj.b.c is equivalent to what you would have to write obj.b.val.c.val had the underlying state object been accessed.

A practical example

Now, let's take a look at a practice example on how vanX.reactive can help group multiple states into a single reactive object in your application:

const Name = () => {
  const data = vanX.reactive({name: {first: "Tao", last: "Xin"}})
  return div(
    "First name: ",
    input({type: "text", value: () => data.name.first,
      oninput: e => data.name.first = e.target.value}), " ",
    "Last name: ",
    input({type: "text", value: () => data.name.last,
      oninput: e => data.name.last = e.target.value}), " ",
    "Full name: ", () => `${data.name.first} ${data.name.last}`, " ",
    button({onclick: () => data.name = {first: "Tao", last: "Xin"}}, "Reset"),
  )
}

Demo:

Try on jsfiddle

Note that, not only you can set the value of each individual leaf field, you can also set the entire object of the name field, as what's being done in the onclick handler of the Reset button:

button({onclick: () => data.name = {first: "Tao", last: "Xin"}}, "Reset")

⚠️ Caveat: Accessing to any sub-field of the reactive object needs to be wrapped inside a binding function. Otherwise, your app won't be reactive to the sub-field changes.

⚠️ Caveat: DO NOT alias any sub-field of the reactive object into other variables. Doing so will break the dependency detection when the sub-field alias is used in a binding function.

Calculated fields

You can specify calculated fields (similar to derived states in VanJS) with vanX.calc. The example above can be rewritten to the code below:

const Name = () => {
  const data = vanX.reactive({name: {first: "Tao", last: "Xin"}})
  data.fullName = vanX.calc(() => `${data.name.first} ${data.name.last}`)
  return div(
    "First name: ",
    input({type: "text", value: () => data.name.first,
      oninput: e => data.name.first = e.target.value}), " ",
    "Last name: ",
    input({type: "text", value: () => data.name.last,
      oninput: e => data.name.last = e.target.value}), " ",
    "Full name: ", () => data.fullName, " ",
    button({onclick: () => data.name = {first: "Tao", last: "Xin"}}, "Reset"),
  )
}

Demo:

Try on jsfiddle

⚠️ Caveat: Avoid self-referencing when specify calculated fields. For instance, the code below:

const data = vanX.reactive({
  name: {first: "Tao", last: "Xin"},
  fullName: vanX.calc(() => `${data.name.first} ${data.name.last}`),
})

will lead to ReferenceError as data variable is not yet defined when the calculation function is being executed.

You can either insert the new calculated fields after the reactive object is built (like what's done in the example above), or defining all the calculated fields in a separate reactive object.

Get the underlying State object

Sometimes, it's desirable to get the underlying State objects for fields in a reactive object. This can be achieved with vanX.stateFields. The example above can be modified to use the underlying state field instead of the binding function for Full name:

const Name = () => {
  const data = vanX.reactive({name: {first: "Tao", last: "Xin"}})
  data.fullName = vanX.calc(() => `${data.name.first} ${data.name.last}`)
  return div(
    "First name: ",
    input({type: "text", value: () => data.name.first,
      oninput: e => data.name.first = e.target.value}), " ",
    "Last name: ",
    input({type: "text", value: () => data.name.last,
      oninput: e => data.name.last = e.target.value}), " ",
    "Full name: ", vanX.stateFields(data).fullName, " ",
    button({onclick: () => data.name = {first: "Tao", last: "Xin"}}, "Reset"),
  )
}

Demo:

Try on jsfiddle

Note that, stateFields only gets the underlying state fields for one layer of the reactive object. For instance, to get the state field for First name, you need to write:

vanX.stateFields(vanX.stateFields(data).name.val).first

Add reactivity to existing JavaScript classes

It's possible to add reactivity to objects of existing JavaScript classes with the help of vanX.reactive. For instance, the code below adds the reactivity to a Person object:

class Person {
  constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName }
  get fullName() { return `${this.firstName} ${this.lastName}` }
}

const Name = () => {
  const person = vanX.reactive(new Person("Tao", "Xin"))
  return div(
    "First name: ",
    input({type: "text", value: () => person.firstName,
      oninput: e => person.firstName = e.target.value}), " ",
    "Last name: ",
    input({type: "text", value: () => person.lastName,
      oninput: e => person.lastName = e.target.value}), " ",
    "Full name: ", () => person.fullName, " ",
    button({onclick: () => (person.firstName = "Tao", person.lastName = "Xin")}, "Reset"),
  )
}

Demo:

Try on jsfiddle

⚠️ Caveat: Once an object is turned reactive with vanX.reactive, you shouldn't access the original object anymore. Doing so will create the same issue as aliasing.

⚠️ Caveat: There might be issues if you try to add reactivity to an object implemented in native code (not in JavaScript). Example: #156.

API reference: vanX.reactive

SignaturevanX.reactive(obj) => <the created reactive object>
DescriptionConverts the input object obj into a reactive object.
Parameters
  • obj - Can be a plain object or an object of an existing JavaScript class. obj can have deeply nested fields. The original obj shouldn't be accessed anymore after the vanX.reactive(obj) call.
ReturnsThe created reactive object.

⚠️ Caveat: The passed-in obj object shouldn't have any State fields. Doing so will result in states of other State objects, which is invalid in VanJS.

API reference: vanX.calc

SignaturevanX.calc(f) => <the created calculated field>
DescriptionCreates a calculated field for a reactive object based on the calculation functionf.
Parameters
  • f - The calculation function.
ReturnsThe created calculated field.

API reference: vanX.stateFields

SignaturevanX.stateFields(obj) => <an object for all underlying state fields of obj>
DescriptionGiven a reactive object obj, returns an object for all the underlying state fields of obj. For instance, if obj is {a: 1, b: 2}, {a: van.state(1), b: van.state(2)} will be returned.
Parameters
  • obj - The input reactive object.
ReturnsAn object for all the underlying state fields of obj.

A comprehensive example

You can refer to this file for a comprehensive demo of all the features regarding to reactive objects discussed above. You can preview the app via CodeSandbox.

vanX.list: Reactive List that Minimizes Re-rendering on Updates


vanX.list takes an input reactive object and builds a list of UI elements whose contents are updated whenever any field of the input reactive object changes. The input reactive object can either be an Array for non-keyed input, or a plain object for keyed input.

Let's first take a look at some simple examples.

Array for non-keyed input:

const items = vanX.reactive([1, 2, 3])
return vanX.list(ul, items, v => li(v))

Plain object for keyed input:

const items = vanX.reactive({a: 1, b: 2, c: 3})
return vanX.list(ul, items, v => li(v))

In both examples, <ul><li>1</li><li>2</li><li>3</li></ul> will be returned.

You can add, update, and delete entries in the reactive object items, and the rendered UI elements are bound to the changes while minimizing the re-rendering of the DOM tree. For instance, if you do the following changes to the Array example:

++items[0]
delete items[1]
items.push(4)

the rendered UI elements will be updated to <ul><li>2</li><li>3</li><li>4</li></ul>.

For keyed object, the following changes will produce the same result:

++items.a
delete items.b
items.d = 4

In addition, for Array-based input items, you can call shift, unshift and splice as you would normally do to an array. The rendered UI elements are guaranteed to be in sync. For instance, after executing the following code:

const items = vanX.reactive([1, 2, 3])
const dom = vanX.list(ul, items, v => li(v))

items.shift()
items.unshift(4)
items.splice(1, 1, 5)

dom will become <ul><li>4</li><li>5</li><li>3</li></ul>.

API Reference: vanX.list

SignaturevanX.list(containerFunc, items, itemFunc) => <the root element of the created DOM tree>
DescriptionCreates a DOM tree for a list of UI elements based on the input reactive object items.
Parameters
  • containerFunc - The function (() => Element) that returns the container element for the list of UI elements. VanJS tag functions can be used here. For instance, specifying van.tags.ul means we want to create a <ul> element as the container of the list.
  • items - A reactive object that holds the data for the list. Can be an Array (for non-keyed input) or a plain object (for keyed input).
  • itemFunc - The function ((v, deleter, k) => Node) that is used to generate the UI element (or rarely, text node) for each list item. The function takes the following parameters:
    • v - A State object corresponding to each list item. You can directly use it as a State-based property / child node, read its value for building the UI element, and/or set its value in some event handlers.
    • deleter - a function (() => void) that can be used in the event handler to delete the entire item. Typically the deleter function can be used as the onclick handler of a deletion button.
    • k - (requires VanX 0.2.0 or later) the key of the corresponding list item, which is the index if items is an Array or the property key if items is a plain object.
ReturnsThe root element of the created DOM tree.

A simplified TODO App

Now, let's take a look at a practical example: The Fully Reactive TODO App in VanJS by Example page can be re-implemented with the help of vanX.list. We can see how a 40+ lines of code is simplified to just over 10 lines:

const TodoList = () => {
  const items = vanX.reactive(JSON.parse(localStorage.getItem("appState") ?? "[]"))
  van.derive(() => localStorage.setItem("appState", JSON.stringify(items.filter(_ => 1))))
  const inputDom = input({type: "text"})
  return div(
    inputDom, button({onclick: () => items.push({text: inputDom.value, done: false})}, "Add"),
    vanX.list(div, items, ({val: v}, deleter) => div(
      input({type: "checkbox", checked: () => v.done, onclick: e => v.done = e.target.checked}),
      () => (v.done ? strike : span)(v.text),
      a({onclick: deleter}, "❌"),
    )),
  )
}

Demo:

Try on jsfiddle

You might notice how easy it is to serialize/deserialize a complex reactive object into/from external storage. This is indeed one notable benefit of reactive objects provided by vanX.reactive.

Note that we are calling items.filter(_ => 1) before serializing to the JSON string via JSON.stringify. This is because after some deletions of items, there will be holes in the items array, which can result null values in the result JSON string and cause problems when the JSON string is deserialized. items.filter(_ => 1) eliminates the holes (See a detailed explanation here).

Update, insert, delete and reorder items in batch with vanX.replace

In addition to updating the items object one item at a time, we also provide the vanX.replace function that allows you to update, insert, delete and reorder items in batch. The vanX.replace function takes the items object and a replace function as its input parameters, and is responsible for updating the items object as well as UI elements bound to it based on the new data returned by the replace function. Let's take a look at a few examples:

// Assume we have a few TODO items as following:
const todoItems = vanX.reactive([
  {text: "Implement VanX", done: true},
  {text: "Test VanX", done: false},
  {text: "Write a tutorial for VanX", done: false},
])

// To delete items in batch
const clearCompleted = () => vanX.replace(todoItems, l => l.filter(v => !v.done))

// To update items in batch
const appendText = () =>
  vanX.replace(todoItems, l => l.map(v => ({text: v.text + "!", done: v.done})))

// To reorder items in batch
const sortItems = () =>
  vanX.replace(todoItems, l => l.toSorted((a, b) => a.localeCompare(b)))

// To insert items in batch
const duplicateItems = () => vanX.replace(todoItems, l =>
  l.flatMap(v => [v, {text: v.text + " copy", done: v.done}]))

API reference: vanX.replace

SignaturevanX.replace(items, f) => void
DescriptionUpdates the reactive object items and UI elements bound to it based on the data returned by the replace function f.
Parameters
  • items - The reactive object that you want to update.
  • f - The replace function, which takes the current values of items as input and returns the new values of the update. If items is an array (for non-keyed data), f will take its values as an array (after eliminating holes with filter(_ => 1), see a detailed explanation of hole elimination) and return the updated values as another array. If items is a plain object (for keyed data), f will take its values as an array of key value pairs (the data you would get with Object.entries(items)) and return the updated values as another array of key value pairs.
Returnsvoid

Example 1: sortable list

Let's look at a sample app that we can build with vanX.list and vanX.replace - a list that you can add/delete items, sort items in ascending or descending order, and append a string to all items in the list:

const List = () => {
  const items = vanX.reactive([])
  const inputDom = input({type: "text"})

  return div(
    div(inputDom, button({onclick: () => items.push(inputDom.value)}, "Add")),
    vanX.list(ul, items, (v, deleter) => li(v, " ", a({onclick: deleter}, "❌"))),
    div(
      button({onclick: () => vanX.replace(items, l => l.toSorted())}, "A -> Z"),
      button({onclick: () => vanX.replace(items,
        l => l.toSorted((a, b) => b.localeCompare(a)))}, "Z -> A"),
      button({onclick: () => vanX.replace(items, l => l.map(v => v + "!"))}, 'Append "!"'),
    ),
  )
}

Demo:

Try on jsfiddle

Example 2: an advanced sortable TODO list

Now, let's take a look at a more advanced example - a sortable TODO list, which is implemented with keyed data. i.e.: reactive items is a plain object instead of an array. In additional to the addition, deletion, sorting and appending strings that are implemented in the previous example, you can edit an item, mark an item as complete, clear all completed items and duplicate the entire list. Furthermore, the application state is serialized and persisted into localStorage thus the state is preserved across page loads.

const TodoList = () => {
  const items = vanX.reactive(JSON.parse(localStorage.getItem("items") ?? "{}"))
  van.derive(() => localStorage.setItem("items", JSON.stringify(items)))

  const inputDom = input({type: "text"})
  let id = Math.max(0, ...Object.keys(items).map(v => Number(v.slice(1))))

  return div(
    div(inputDom, button(
      {onclick: () => items["k" + ++id] = {text: inputDom.value, done: false}}, "Add")),
    vanX.list(div, items, ({val: v}, deleter) => div(
      input({type: "checkbox", checked: () => v.done,
        onclick: e => v.done = e.target.checked}), " ",
      input({
        type: "text", value: () => v.text,
        style: () => v.done ? "text-decoration: line-through;" : "",
        oninput: e => v.text = e.target.value,
      }), " ",
      a({onclick: deleter}, "❌"),
    )),
    div(
      button({onclick: () => vanX.replace(items, l => l.filter(([_, v]) => !v.done))},
        "Clear Completed"),
      button({onclick: () => vanX.replace(items, l =>
        l.toSorted(([_1, a], [_2, b]) => a.text.localeCompare(b.text)))}, "A -> Z"),
      button({onclick: () => vanX.replace(items, l =>
        l.toSorted(([_1, a], [_2, b]) => b.text.localeCompare(a.text)))}, "Z -> A"),
      button({onclick: () => vanX.replace(items, l =>
        l.flatMap(([k1, v1]) => [
          [k1, v1],
          ["k" + ++id, {text: v1.text + " - copy", done: v1.done}],
        ]))},
        "Duplicate List"),
      button({onclick: () => Object.values(items).forEach(v => v.text += "!")}, 'Append "!"'),
    ),
  )
}

Demo:

Try on jsfiddle

API Index


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