b8r 0.5.0

b8r 0.5.0 is available from npm and github. See the github-pages demo here. It’s the single most radical change to b8r since it was first released, although unlike the switch from require() to ES6 Modules it’s non-breaking, so I’ve updated the documentation accordingly.

How it started:

b8r.register('myApp', { 
  foo: 17, 
  bar: { 
    baz: 'lurman'
  }, 
  list: []
})
  
const { baz } = b8r.get('myApp.bar')
b8r.set('myApp.bar.baz', 'lurman')
b8r.pushByPath('myApp.list', { id: 17, name: 'Romeo + Juliet' })
const { id } = b8r.get('myApp.list[id=17]') // one of b8r's coolest features

How it’s going:

b8r.reg.myApp  = { 
  foo: 17, 
  bar: { 
    baz: 'lurman'
  }, 
  list: [] 
}

const {baz} = b8r.reg.myApp.bar
// OMG I've been misspelling his name in all my examples!
b8r.reg.myApp.bar.baz = 'luhrmann' 
b8r.reg.myApp.list.push({id: 17, name: 'Romeo + Juliet'})
const {id} = b8r.reg.myApp.list['id=17'] // this still works!

The changes are, basically, syntax sugar for the most common operations with b8r, which means that there’s a huge potential to make code that’s already about as compact and simple as front-end code gets, and make it more compact and more simple. How simple? Here’s the code implementing the simple todo component from the React vs. Bindinator document:

data.list = []
data.text = ''
data.nextItem = 1
data.addItem = () => {
  const {text} = data
  data.list.push({text})
  data.nextItem += 1
  data.text = ''
}

The only piece of information you need to know about this is that data ia the component’s state, which has been retrieved from the registry, and if you change properties inside it, they’ll be detected and changed.

The same example used to look like this:

set({
  list: [],
  text: '',
  nextItem: 1,
  addItem() {
    const {list, nextItem, text} = get()
    list.push({text})
    set({
      nextItem: nextItem + 1,
      list,
      text: ''
    })
  }
})

You need to know that set() is automatically going to set properties inside the component’s state (so that b8r will know they’ve been changed), and get() will retrieve the object. You need to know that if you pull an object out of the registry and mess with it, b8r will not know unless you tell it. So, at some point you’ll need to know that get() and set() also take paths, and how paths work.

How did this happen?

So, yesterday, a friend and former colleague who uses bindinator (b8r) reached out to me and said, in essence, it would be a lot easier to convert existing code to b8r if you could put b8r expressions on the left-hand-side of assignment statements.

My reaction was, in essence, “sure, that’s the holy grail, but I think it’s impossible”.

He replied that he thought that I might be able to use ES6 Proxy—which I immediately looked up…

So, as of today, the main branch of b8r now supports a new, cleaner, more intuitive syntax (the old syntax is still supported, don’t worry!), it’s documented, and there’s decent test coverage.

Motivation

The basic idea of b8r is that it binds data to “paths” which are analogous to file paths on a logical storage device, except that the paths look like standard javascript (mostly).

E.g. you might bind the value in an input field to app.user.name. This looks like <input data-bind=”value=app.user.name”>.

This isn’t binding to a variable or a value, but to the path.

Similarly, you can bind an object to a name, e.g.

const obj = {
  user: {
    name: "tonio",
    website: "https://loewald.com" 
  }
} 

b8r.register('app', obj) // binds obj to the name "app"

If you did both of these things then the input field would now contain “tonio”. But if the user edits the input field, then the value in the bound object changes.

Now, what happens if I write:

obj.user.name = "fredo"?

Well, the value in the object has changed, but the input doesn’t get updated because it was done “behind b8r’s back”. To notify b8r of the update so it can keep bound user interface elements consistent with the application state, you need to do something like:

// simple
b8r.set('app.user.name', 'fredo')

// manual
obj.user.name = 'fredo'
b8r.touch('app.user.name')

This works very well, and is both pretty elegant and doesn’t involve any “spooky magic at a distance”. E.g. because you know you can change things behind b8r’s back, you can use this to make updates more efficient (e.g. you might be streaming data into a large array, and not want to force large updates frequently and at random intervals, so you could update the array, but only tell b8r when you want to redraw the view).

But what you can’t do is something like this:

b8r.get('app.user.name') = 'fredo'

A javascript function cannot return a variable. It can return an object with a computed property, so it would be possible to enable syntax like:

b8r.get(‘app.user.name’).value = ‘fredo’

Which is (a) clumsy, and (b) makes the common case less elegant. Or maybe we could enable:

b8r.set('app.user.name').value = 'fredo'

Which returns the object if no new value is passed (or whether or not a new value is passed) but this doesn’t work for:

b8r.set('app.user').name.value = 'fredo'

And

b8r.set('app.user').name = 'fredo'

…performs the change, but behind b8r’s back.

ES6 Proxy to the Rescue

So, in a nutshell, ES6 proxy gives the Javascript programmer direct access to the mechanism that allows object prototypes (i.e. “class instances” to work, by creating a “proxy” for another object that intercepts property lookups. This is similar to—but far less hacky than—the mechanism in PHP that lets you write a special function for a class that gets called whenever an unrecognized property of an instance is referenced.

In short, you use Proxy to create a proxy for an object (e.g. foo) that can intercept any property reference, e.g. proxy.bar, and decide what to do, knowing that the user is trying to get or set a property named ‘bar’ on the original object.

So, now, b8r.reg is a proxy for the b8r registry, which is the object containing your application’s state.

Our original example can now look like this:

b8r.reg.app =  {
  user: {
    name: "tonio",
    website: "https://loewald.com" 
  }
}

And we can change a property via:

b8r.reg.app.user.name = 'fredo'

And common convenience constructs like this work:

const {user} = b8r.reg.app
user.name = 'fredo'
user.website = 'https://deadmobster.org'

And b8r is notified when the changes are made. (You can still make changes behind b8r’s back the old way, if you want to avoid updates!)

Not bad for a Saturday afternoon!

Arrays

Wouldn’t it be nice if arrays worked exactly as you expected? So instead of this pattern in b8r code:

const {list} = b8r.get('path.to.list')
// do stuff to list like add items, remove them, filter them
...
b8r.touch('path.to.list') // hey, b8r, we messed with the list!

We could do stuff like this?

b8r.reg.path.to.list.push(...newItems)

or

b8r.reg.path.to.list.splice(5, 10) // chop some items out

I.e. wouldn’t it be nice if you could just perform standard operations you can do with any array directly from the b8r registry, and b8r knew it happened? Why yes, yes it would.

The new reg proxy exposes array methods that tend to mutate the array with a simple wrapper that touches the path of the array. E.g. pop, splice, and forEach.

And, why about the coolest feature of b8r data-paths, id-paths in array references?

b8r.set('path.to.list[id=123].name', 'foobar')

becomes

b8r.reg.path.to.list['id=123'].name = 'foobar'

Spooky Action at a Distance?

This all might smell a bit like “spooky magic at a distance”. How is the user expected to maintain a mental model of what’s going on behind the scenes for when things go wrong?

Well, the b8r.reg returns a proxy of the registry, and exposes each of its object properties as proxies, and so on. Each proxy knows its own path within the registry. Each object registered is just a property of the registry. In

If you set the property of a proxied object, it calls b8r.set() on the path.

That’s it. That’s all the magic.

Now, here are two articles discussing how ngZone and change-detection-strategy onPush, and how React hooks work.