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.1.3/dist/van-x.nomodule.min.js"></script>
https://cdn.jsdelivr.net/npm/vanjs-ext@0.1.3/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.1.3.d.ts
and add the following code at the top of your .ts
file:
import type * as vanXType from "./van-x-0.1.3.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:
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:
⚠️ 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:
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:
⚠️ 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
Signature | vanX.reactive(obj) => <the created reactive object> |
Description | Converts the input object obj into a reactive object. |
Parameters |
|
Returns | The 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
Signature | vanX.calc(f) => <the created calculated field> |
Description | Creates a calculated field for a reactive object based on the calculation functionf . |
Parameters |
|
Returns | The created calculated field. |
API reference: vanX.stateFields
Signature | vanX.stateFields(obj) => <an object for all underlying state fields of obj> |
Description | Given 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 |
|
Returns | An 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
Signature | vanX.list(containerFunc, items, itemFunc) => <the root element of the created DOM tree> |
Description | Creates a DOM tree for a list of UI elements based on the input reactive object items . |
Parameters |
|
Returns | The 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:
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
Signature | vanX.replace(items, f) => void |
Description | Updates the reactive object items and UI elements bound to it based on the data returned by the replace function f . |
Parameters |
|
Returns | void |
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:
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:
API Index
Below is the list of all top-level APIs in VanX: