The tosi proxy

tosi() assigns an object passed to it to a global state object, and returns an observer proxy (BoxedProxy) wrapped around the global state object.

BoxedProxy wraps any object you pull out of it in an observer proxy. It boxes booleans, numbers, and strings in lightweight proxies that know their path and can access/modify the underlying value.

In rough terms:

const state = {}
const boxed = new Proxy(state, ...)
tosi = (obj<T>): BoxedProxy<T> => {
  Object.assign(state, obj)
  return state
}

This allows the following pattern, which gives Typescript a lot of useful information for free, allowing autocomplete, etc. with a minimumn of boilerplate.

import { tosi, elements, bind } from 'tosijs'

const { prefs } = tosi({
  prefs: {
    theme: 'system',
    highcontrast: false
  }
})

// this example continues…

So { prefs: ... } is assigned to the state object, and now prefs holds the same stuff except it's wrapped in a BoxedProxy.

The BoxedProxy behaves just like the original object, except that it:

prefs.theme.value === 'system'          // true
prefs.theme.path === 'prefs.theme'      // true
prefs.theme.valueOf() === 'system'      // true
String(prefs.theme) === 'system'        // true (via Symbol.toPrimitive)

The BoxedProxy observes changes made through it and updates bound elements accordingly:

bind(document.body, prefs.theme, {
  toDOM(element, value) {
    element.classList.toggle('dark-mode', value === 'dark')
  }
}

const { select, option } = elements

document.body.append(
  select(
    { bindValue: prefs.theme },
    option('system'),
    option('dark'),
    option('light')
  )
)

Setting up the binding to document.body will set the class appropriately, and modifying prefs.theme will update the bound element automatically.

proxy.theme.value = 'dark'

In javascript you can just write proxy.theme = 'dark' (TypeScript doesn't allow this due to asymmetric get/set type limitations).

This, in a nutshell, explains exactly what tosijs is designed to do.

tosi tracks state and allows you to bind data to your user interface directly and with almost no code, with clean separation between business logic and presentation.

The elements proxy lets you build HTML elements with data and event bindings more compactly and efficiently than you can using JSX/TSX, and it deals in regular HTMLElement—no virtual DOM, tranpilation magic, spooky action at a distance, or any similar nonsense.

If you need to do complex bindings, the bind method lets you directly link data to the DOM and automatically sets up observers for you.

Component lets you create reusable web-components.

css lets you write CSS economically and makes it easy to leverage the power of CSS variables, while Color allows you to do color math quickly and easily until similar functionality is added to CSS.

In Finnish, tosi means true or really.

As conceived, tosi() is an observer Proxy wrapped around your application's state. It's the single source of truth for application state.

Note that the interactive examples on the tosijs.net website allow TypeScript but the Typescript is simply stripped to javascript using sucrase.

xin

xin is a path-based implementation of the observer or pub/sub pattern designed to be very simple and straightforward to use, leverage Typescript type-checking and autocompletion, and let you get more done with less code and no weird build magic (such as special decorators or "execution zones").

In a nutshell

xin is a single object wrapped with an observer proxy.

In the following example there's a <div> and an <input>. The textContent of the former and the value of the latter are bound to the path xinExample.string.

xin is exposed as a global in the console, so you can go into console and look at xin.xinExample and (for example) directly change it via the console.

You can also turn on Chrome's rendering tools to see how efficiently the DOM is updated. And also note that typing into the input field doesn't lose any state (so your text selection and insertion point are stable.

import { xin, elements } from 'tosijs'

xin.xinExample = {
  string: 'hello, xin'
}

const { label, input, div, span } = elements

preview.append(
  div(
    {
      style: {
        display: 'flex',
        flexDirection: 'column',
        gap: 10,
        padding: 10
      }
    },
    div({bindText: 'xinExample.string'}),
    label(
      span('Edit this'),
      input({ bindValue: 'xinExample.string'})
    )
  )
)

A Calculator

import { xin, elements, touch } from 'tosijs'

// here's a vanilla javascript calculator
const calculator = {
  x: 4,
  y: 3,
  op: '+',
  result: 0,
  evaluate() {
    this.result = eval(`${this.x} ${this.op} ${this.y}`)
  }
}

calculator.evaluate()

xin.calculatorExample = calculator

// now we'll give it a user interface…
const { input, select, option, div, span } = elements

preview.append(
  div(
    {
      onChange() {
        calculator.evaluate()
        touch('calculatorExample.result')
      }
    },
    input({bindValue: 'calculatorExample.x', placeholder: 'x'}),
    select(
      {
        bindValue: 'calculatorExample.op'
      },
      option('+'),
      option('-'),
      option({value: '*'}, '×'),
      option({value: '/'}, '÷'),
    ),
    input({bindValue: 'calculatorExample.y', placeholder: 'y'}),
    span('='),
    span({bindText: 'calculatorExample.result' })
  )
)

Important points:

If you're reading this on tosijs.net then this the demo app you're looking works a bit like this and xin (and boxed) are exposed as globals so you can play with them in the debug console.

Try going into the console and typing xin.app.title to see what you get, and then try `xin.app.title = 'foobar' and see what happens to the heading.

Also try xin.prefs.theme and try app.prefs.theme = 'dark' etc.

Once an object is assigned to xin, changing it within xin is simple. Try this in the console:

xin.calculatorExample.x = 17

This will update the x field in the calculator, but not the result. The result is updated when a change event is triggered.

If you wanted the calculator to update based on any change to its internal state, you could instead write:

observe('calculatorExample', () => {
  calculator.evaluate()
  touch('calculatorExample.result')
})

Now the onChange handler isn't necessary at all. observe is documented in path-listener.

import { observe, xin, elements } from 'tosijs'

const { h3, div } = elements

const history = div('This shows changes made to the preceding example')

preview.append(
  h3('Changes to the calculatorExample'),
  history
)

observe(/calculatorExample\./, path => {
  const value = xin[path]
  history.insertBefore(div(`${path} = ${value}`), history.firstChild)
})

Now, if you sneakily make changes behind xin's back, e.g. by modifying the values directly, e.g.

const emails = await getEmails()
xin.emails = emails

// notes that xin.emails is really JUST emails
emails.push(...)
emails.splice(...)
emails[17].from = '...'

Then xin won't know and observers won't fire. So you can simply touch the path impacted:

import { touch } from 'tosijs'
touch('emails')

In the calculator example, the vanilla calculator code calls evaluate behind xin's back and uses touch('calculatorExample.result') to let xin know that calculatorExample.result has changed. This causes xin to update the DOM.

How it works

xin is a Proxy wrapped around a bare object: effectively a map of strings to values.

When you access the properties of an object assigned to xin it wraps the values in similar proxies, and tracks the path that got you there:

xin.foo = {
  bar: 'baz',
  luhrman: {
    job: 'director'
  }
}

Now if you pull objects back out of xin:

let foo = xin.foo
let luhrman = foo.luhrman

foo is a Proxy wrapped around the original untouched object, and it knows it came from 'foo'. Similarly luhrman is a Proxy that knows it came from 'foo.luhrman'.

If you change a value in a wrapped object, e.g.

foo.bar = 'bob'
luhrman.job = 'writer'

Then it will trigger any observers looking for relevant changes. And each change will fire the observer and tell it the path that was changed. E.g. an observer watching lurman will be fired if lurman or one of lurman's properties is changed.

The boxed proxy

boxed is a sister to xin that wraps "scalar" values (boolean, number, string) in lightweight proxies. These proxies know their path and provide convenient access to the underlying value. E.g. if you write something like:

xin.test = { answer: 42 }
boxed.box = { pie: 'apple' }

Then:

xin.test.answer === 42
xin.box.pie === 'apple'
// boxed scalars have .value and .path
boxed.test.answer.value === 42
boxed.box.pie.value === 'apple'
boxed.test.answer.path === 'test.answer'
boxed.box.pie.path === 'box.pie'
// valueOf() works for coercion
boxed.test.answer.valueOf() === 42
String(boxed.box.pie) === 'apple'

Boxed scalars also delegate to the underlying value's prototype methods, so you can call string, number, and boolean methods directly:

boxed.box.pie.toUpperCase() === 'APPLE'
boxed.box.pie.startsWith('app') === true
boxed.box.pie.length === 5
boxed.test.answer.toFixed(2) === '42.00'

Aside from always "boxing" scalar values, boxed works just like xin.

In the console, you can also access boxed and look at what happens if you access boxed.xinExample.string. Note that this changes the value you get, the underlying value is still what it was. If you set it to a new string value that's what will be stored. xin doesn't monkey with the values you assign.

Why?!

As far as Typescript is concerned, xinProxy just passes back what you put into it, which means that you can now write bindings with type-checking and autocomplete and never use string literals. So something like this just works:

const div = elements.div({bindText: boxed.box.pie})

…because boxed.box.pie has a xinPath which is what is actually used for binding, whereas xin.box.pie is just a scalar value. Without boxed you could write bindText: 'box.pie' but you don't get lint support or autocomplete. (Also, in some cases, you might even mangle the names of an object during minification and boxed will know the mangled name).

If you need the thing itself or the path to the thing…

proxys returned by xin are typically indistinguishable from the original object, but in a pinch tosiPath() will give you the path (string) of a XinProxy while tosiValue will give its "bare" value. tosiPath() can also be used to test if something is actually a proxy, as it will return undefined for regular objects.

E.g.

tosiPath(luhrman) === 'foo.luhrman'     // true
const bareLurhman = tosiValue(luhrman)  // not wrapped

You may want the thing itself to, for example, perform a large number of changes to an object without firing observers. You can let xin know you've made changes behind its back using touch, e.g.

doTerribleThings(tosiValue(luhrman))
// eslint-disable-next-line
touch(luhrman)

This is useful because boxed.foo.bar always knows where it came from, while xin.foo.bar only knows where it came from if it's an object value.

This means you can write:

import { boxed, elements } from 'tosijs'

boxed.boxedExample = {
  string: 'hello, boxed'
}

const { boxedExample } = boxed

const { label, input, div, span } = elements

preview.append(
  div(
    {
      style: {
        display: 'flex',
        flexDirection: 'column',
        gap: 10,
        padding: 10
      }
    },
    div({bindText: boxedExample.string}),
    label(
      span('Edit this'),
      input({ bindValue: boxedExample.string})
    )
  )
)

And the difference here is you can bind direct to the reference itself rather than a string. This leverages autocomplete, linting, and so on in a way that using string paths doesn't.

It does have a downside! boxedExample.string !== 'hello, boxed' and boxedExample.string !== boxedExample.string because they're proxies, not primitives. This is critical for comparisons such as === and !==. Always use .value, tosiValue(), or valueOf() when comparing: boxed.foo.bar.value === 'hello' or tosiValue(boxed.foo.bar) === 'hello'.

The .tosi accessor

Every boxed proxy has a .tosi property that provides the full reactive API without risk of name collisions. If your data has properties named value, path, observe, etc., those will shadow the proxy's direct API — but .tosi is always available.

proxy.foo.tosi.value        // get the underlying value
proxy.foo.tosi.value = 17   // set it (triggers observers)
proxy.foo.tosi.path         // 'foo'
proxy.foo.tosi.observe(cb)  // watch for changes
proxy.foo.tosi.touch()      // force update notification
proxy.foo.tosi.bind(el, binding)
proxy.foo.tosi.on(el, eventType)
proxy.foo.tosi.binding(binding)
proxy.foo.tosi.listBinding(templateBuilder, options)
proxy.foo.tosi.listFind(selector, value)
proxy.foo.tosi.listUpdate(selector, newValue)
proxy.foo.tosi.listRemove(selector, value)

This is the recommended API. The only reserved name is tosi itself — don't use it as a property name in your data.

If you do have data with a tosi property, use tosiAccessor(proxy) or the TOSI_ACCESSOR symbol directly — these are guaranteed collision-free:

import { tosiAccessor, TOSI_ACCESSOR } from 'tosijs'

const acc = tosiAccessor(proxy.foo)  // always works
const acc2 = proxy.foo[TOSI_ACCESSOR]  // also always works
acc.value  // underlying value
acc.path   // path string

Direct helper properties (deprecated)

The following properties are also available directly on boxed proxies, but they can be shadowed by actual properties on the target object. Prefer .tosi.* instead:

Boxed scalars also expose all methods from the underlying primitive's prototype (e.g. .toUpperCase(), .startsWith(), .toFixed(), .length, index access).

Type coercion

Number(), String(), arithmetic, template literals, and == comparison all work on boxed scalars via Symbol.toPrimitive:

Number(proxy.score)       // 42
String(proxy.name)        // 'Alice'
proxy.score + 1           // 43
`hello ${proxy.name}`     // 'hello Alice'
proxy.score == 42         // true

Boolean() is the exception — in JavaScript, Boolean(anyObject) always returns true. This is a language-level behavior, not a tosijs limitation (Boolean(new Boolean(false)) is also true). For boolean checks, use .value or .valueOf():

// ❌ Always true — Boolean() on any object
if (Boolean(proxy.flag)) ...

// ✅ Correct
if (proxy.flag.value) ...
if (proxy.flag.valueOf()) ...

Arrays also have:

Note: The xinValue, xinPath, xinObserve, xinBind, xinOn, and tosiValue, tosiPath, etc. names still work but are deprecated. Use .tosi.* instead.

.take() — Reactive Binding Transforms

.take() creates a reactive binding descriptor that transforms values before they reach the DOM. It eliminates most custom bindings.

Single-path transform

import { tosi, elements } from 'tosijs'

const { takeDemo } = tosi({ takeDemo: { count: 3, items: ['a', 'b', 'c'] } })

const { span, button } = elements

preview.append(
  span({ bindText: takeDemo.count.tosi.take(n => `Count: ${n}`) }),
  button('Delete', {
    bindEnabled: takeDemo.items.tosi.take(list => list.length > 0),
  })
)

Multi-path transform

Pass additional proxies before the transform function. The transform receives all current values when any of the watched paths change.

import { tosi, elements } from 'tosijs'

const { takeMultiDemo } = tosi({
  takeMultiDemo: { firstName: 'Alice', lastName: 'Smith' }
})

const { span, input, label } = elements

preview.append(
  span({ bindText: takeMultiDemo.firstName.tosi.take(
    takeMultiDemo.lastName,
    (first, last) => `${first} ${last}`
  ) }),
  label('First', input({ bindValue: takeMultiDemo.firstName })),
  label('Last', input({ bindValue: takeMultiDemo.lastName })),
)

.take() is efficient: the transform only runs when the input values actually change (compared by identity). If an observer fires but the value is the same, the transform is skipped entirely.

Filtered list with .take()

.take() works with list bindings to create reactive filtered views. The filter re-evaluates when any of the watched paths change, but the list binding still does surgical DOM updates.

import { elements, tosi, touch } from 'tosijs'

const { takeFilterDemo } = tosi({
  takeFilterDemo: {
    search: '',
    items: [
      { id: 1, name: 'Alice', role: 'engineer' },
      { id: 2, name: 'Bob', role: 'designer' },
      { id: 3, name: 'Carol', role: 'engineer' },
      { id: 4, name: 'Dave', role: 'manager' },
      { id: 5, name: 'Eve', role: 'designer' },
    ]
  }
})

const { div, input, label, ul } = elements

preview.append(
  div(
    { style: { display: 'flex', flexDirection: 'column', gap: 10, padding: 10 } },
    label('Filter', input({
      placeholder: 'type to filter...',
      bindValue: takeFilterDemo.search,
    })),
    div({
      bindText: takeFilterDemo.items.tosi.take(
        takeFilterDemo.search,
        (items, search) => {
          const s = search.toLowerCase()
          const count = s ? items.filter(i => i.name.toLowerCase().includes(s) || i.role.includes(s)).length : items.length
          return `Showing ${count} of ${items.length}`
        }
      ),
      style: { fontStyle: 'italic', opacity: 0.7 },
    }),
    ul(
      ...takeFilterDemo.items.tosi.listBinding(
        ({li, span}, item) => li(
          span({ bindText: item.name, style: { fontWeight: 'bold' } }),
          ' — ',
          span({ bindText: item.role }),
        ),
        {
          idPath: 'id',
          filter: (items, needle) => needle
            ? items.filter(i => i.name.toLowerCase().includes(needle) || i.role.includes(needle))
            : items,
          needle: takeFilterDemo.search.tosi.take(s => s.toLowerCase()),
        }
      )
    )
  )
)

List Operations

When working with list-bound arrays, you often need to find, update, or remove items efficiently. The listFind, listUpdate, and listRemove methods on proxied arrays handle this with the same selector pattern used by listBinding.

Selectors

All three methods use a selector callback to identify which field to match on. The callback receives a placeholder proxy (the same ^ proxy trick used by listBinding) and should return a property of the item:

(item) => item.id        // match on the 'id' field
(item) => item.uid       // match on 'uid'
(item) => item.meta.key  // nested field paths work too

listFind(selector, value) / listFind(element)

Find an item and return it as a proxied object (so mutations trigger observers):

const item = app.items.listFind((item) => item.id, 'abc')
if (item) {
  item.name.value = 'Updated'  // triggers observers + DOM updates
}

You can also pass a DOM element to find the array item bound to it — useful in click handlers on list-bound elements:

container.addEventListener('click', (e) => {
  const item = app.items.listFind(e.target)
  if (item) console.log('Clicked:', item.name.value)
})

listUpdate(selector, newValue)

Upsert: update an existing item in place or push a new one. This is the recommended way to update list items because it preserves object identity — the itemToElement WeakMap still maps to the same DOM element, so no teardown/recreation occurs:

// Update existing — only changed properties fire observers
app.items.listUpdate((item) => item.id, {
  id: 'abc', name: 'New Name', score: 100
})

// Item not found — pushes as new
app.items.listUpdate((item) => item.id, {
  id: 'xyz', name: 'Brand New'
})

Returns the proxied item (existing or newly pushed).

listRemove(selector, value)

Remove an item by field match. Returns true if removed, false if not found:

app.items.listRemove((item) => item.id, 'abc')  // true if found

To Do List Example

Each of the features described thus far, along with the features of the elementCreator functions provided by the elements proxy are designed to eliminate boilerplate, simplify your code, and reduce the chance of making costly errors.

This example puts all of this together.

import { elements, tosi } from 'tosijs'

const { todos } = tosi({
  todos: {
    list: [],
    newItem: ''
  }
})

const { h3, div, label, input, button, template } = elements

const addItem = () => {
  todos.list.push({
    description: todos.newItem.value
  })
  todos.newItem.value = ''
}

preview.append(
  h3('To do'),
  div(
    {
      bindList: {
        value: todos.list
      }
    },
    template(
      div({ bindText: '^.description' })
    )
  ),
  div(
    input({
      placeholder: 'task',
      bindValue: todos.newItem,
      onKeyup(event) {
        if(event.key === 'Enter' && todos.newItem.value !== '') {
          addItem()
        }
      }
    }),
    button('Add', {
      bindEnabled: todos.newItem,
      onClick: addItem
    })
  )
)