Binding

bind() lets you synchronize data / application state to the user-interface reliably, efficiently, and with a minimum of code.

The design goal of bind is to eliminate most of the code used to sync state to and from the UI (DOM elements) making code simpler, faster, and more reliable.

An Aside on Reactive Programming vs. the Observer Model

A good deal of front-end code deals with keeping the application's state synchronized with the user-interface. One approach to this problem is Reactive Programming as exemplified by React and its many imitators.

tosijs works very well with React via the useTosi React "hook". But tosijs is not designed for "reactive programming" and in fact "hooks" aren't "reactive" at all, so much as an example of the "observer" or "pub/sub" pattern.

tosijs is a "path-observer" in that it's an implementation of the Observer Pattern where path strings serve as a level of indirection to the things observed. This allows data to be "observed" before it exists, which in particular decouples the setup of the user interface from the initialization of data and allows user interfaces built with tosijs to be deeply asynchronous.

bind()

bind<T = Element>(
  element: T,
  what: XinTouchableType,
  binding: XinBinding,
  options: XinObject
): T

bind() binds a path to an element, syncing the value at the path to and/or from the DOM.

import { bind, tosi } from 'tosijs'

const { simpleBindExample } = tosi({
  simpleBindExample: {
    showThing: true
  }
})

bind(
  preview.querySelector('b'),
  'simpleBindExample.showThing',
  {
    toDOM(element, value) {
      element.style.visibility = value ? 'visible' : 'hidden'
    }
  }
)

bind(
  preview.querySelector('input[type=checkbox]'),
  // the tosi can be used instead of a string path
  simpleBindExample.showThing,
  // we could just use bindings.value here
  {
    toDOM(element, value) {
      element.checked = value
    },
    fromDOM(element) {
      return element.checked
    }
  }
)
<b>The thing</b><br>
<label>
  <input type="checkbox">
  Show the thing
</label>

The bind function is a simple way of tying an HTMLElement's properties to state via path using bindings

import {bind, bindings, xin, elements, updates} from 'tosijs'
const {div, input} = elements

const divElt = div()
bind(divElt, 'app.title', bindings.text)
document.body.append(divElt)

const inputElt = input()
bind(inputElt, 'app.title', bindings.value)

xin.app = {title: 'hello world'}
await updates()

What's happening is essentially the same as:

divElt.textContent = xin.app.title
observe('app.title', () => divElt.textContent = xin.app.title)

inputElt.value = xin.app.title
observe('app.title', () => inputElt.value = xin.app.title)
inputElt.addEventListener('change', () => { xin.app.title = inputElt.value })

Except:

  1. this code is harder to write
  2. it will fail if xin.app hasn't been initialized (which it hasn't been!)
  3. inputElt will also trigger debounced updates on input events

After this. div.textContent and inputElt.value are 'hello world'. If the user edits the value of inputElt then xin.app.title will be updated, and app.title will be listed as a changed path, and an update will be fired via setTimout. When that update fires, anything observer of the paths app.text and app will be fired.

A binding looks like this:

interface XinBinding {
  toDOM?: (element: HTMLElement, value: any, options?: XinObject) => void
  fromDOM?: (element: HTMLElement) => any
}

Simply put the toDOM method updates the DOM based on changes in state while fromDOM updates state based on data in the DOM. Most bindings will have a toDOM method but no fromDOM method since bindings.value (which has both) covers most of the use-cases for fromDOM.

It's easy to write your own bindings if those in bindings don't meet your need, e.g. here's a custom binding that toggles the visibility of an element based on whether the bound value is neither "falsy" nor an empty Array.

const visibility = {
  toDOM(element, value) {
    if (element.dataset.origDisplay === undefined && element.style.display !== 'none') {
      element.dataset.origDisplay = element.style.display
    }
    element.style.display = (value != null && value.length > 0) ? element.dataset.origDisplay : 'none'
  }
}
bind(listElement, 'app.bigList', visibility)

on()

on(element: Element, eventType: string, handler: XinEventHandler): VoidFunction

export type XinEventHandler<T extends Event = Event, E extends Element = Element> =
  | ((evt: T & {target: E}) => void)
  | ((evt: T & {target: E}) => Promise<void>)
  | string
import { elements, on, tosi } from 'tosijs'
import { postNotification } from 'tosijs-ui'

const makeHandler = (message) => () => {
  postNotification({ message, duration: 2 })
}

const { onExample } = tosi({
  onExample: {
    clickHandler: makeHandler('Hello from onExample proxy')
  }
})

const { button, div, h2 } = elements

const hasListener = button('has listener')
hasListener.addEventListener('click', makeHandler('Hello from addEventListener'))

preview.append(
  div(
    {
      style: {
        display: 'flex',
        flexDirection: 'column',
        padding: 10,
        gap: 10
      }
    },
    h2('Event Handler Examples'),
    hasListener,
    button('just a callback', {onClick: makeHandler('just a callback')}),
    button('via proxy', {onClick: onExample.clickHandler}),
  )
)

on() binds event-handlers to DOM elements.

More than syntax sugar for addEventListener, on() allows you to bind event handlers inside xin by path (e.g. allowing event-handling code to be loaded asynchronously or lazily, or simply allowing event-handlers to be switched dynamically without rebinding) and it uses event-bubbling to minimize the actual number of event handlers that need to be registered.

on() returns a function for removing the event handler.

In essence, only one event handler of a given type is ever added to the DOM by on() (at document.body level), and then when that event is detected, that handler goes from the original target through to the DOM and fires off event-handlers, passing them an event proxy (so that stopPropagation() still works).

touchElement()

touchElement(element: Element, changedPath?: string)

This is a low-level function for immediately updating a bound element. If you specifically want to force a render of an element (versus anything bound to a path), simply call touchElement(element). Specifying a changedPath will only trigger bindings bound to paths staring with the provided path.