Skip to content

Selection bindings

ggmod edited this page Apr 3, 2016 · 3 revisions

#Selection bindings

D3bind extends the d3.js selections in a way that enables you to bind observables to the view elements. Every time the observable model changes, it automatically updates the view elements.

var model = d3bind.observable({ myValue: 'bar' });
d3bind.select('body').append('div').bindText(model.$myValue); // <div>bar</div>
model.myValue = 'foo';  // <div>foo</div>

The selection returned by d3bind.select is a simple d3.js selection with additional bind methods. The bind methods accept anything that implements the Observable interface (see Observables) , for example the properties of observable objects, ObservableValue instances, the $length property of an ObservableArray.

Bind functions

The bind functions available are the following:

.bindText(observable, [converter], [transition])
.bindHtml(observable, [converter])
.bindClassed(className, observable, [converter])
.bindStyle(styleName, observable, [converter], [transition])
.bindAttr(attrName, observable, [converter], [transition])
.bindProperty(propertyName, observable, [converter])

In each case, the parameter called observable can be anything that implements the Observable interface, or an array of objects implementing the Observable interface.

The optional converter parameter is always a function, and its input is the current value of the observable (or current values, if the observable parameter is an array of observables).

The optional transition parameter is an object with a transition property, that specifies a function that can modify the d3.js transition object used when the bound model value changes. (This is admittedly not a very elegant part of the API, but it's a consequence of how transitions work in d3.js v3.)

Converters

The bind methods usually accept a converter function as the second parameter.

.bindText(modal.$myValue, value => value.toUpperCase()); // <div>FOO</div>

It's also possible to bind to multiple observables, and then the view element will be updated if any of them change. In this case it's always necessary to define a converter function:

var model = d3bind.observable({ a: 2, b: 3 });
d3bind.select('body').append('div')
  .bindText([model.$a, model.$b], (a, b) => a + b); // <div>5</div>

Transitions

It's possible to combine observable bindings with transitions. This way every time the model value changes, the view element will transition from the current state into the new one. The transition property of the last parameter is a function that lets you modify the transition object used when the model value changes.

.bindAttr("x", model.$x, { transition: t => t.duration(750) })
// or combined with a converter:
.bindAttr("x", model.$x, x => x*10, { transition: t => t.duration(750) })

Unbinding

If you remove a selection with the selection.remove() function, then the selection and all of its descendants in the DOM tree will automatically unsubscribe from all of their bindings. This way you don't have to worry about memory leaks, where the DOM elements removed from the document would still react to changes in the model. (This works even if you remove at the end of a transition with the transition.remove() function.)

But if you want to manually manage to subscribtions of the view elements, then you can use selection.remove(true) to remove the view elements without removing the bindings. And then later you can manually remove the bindings by calling selection.unbindAll() or just a single binding by calling for example selection.unbindText().

Custom bind functions

There are a few special bind functions, that can be used to apply custom changes to the view element:

.bind(observable, handler)
.bindCall(observable, handler, [transition])
.bindRedraw(observable, handler)

The observable parameter in each case is an object or array of objects implementing the Observable interface.

.bind(observable, handler)

The handler of the .bind() function takes the current values of the observable(s) as its input, like a converter, but it applies the needed changes to the selection manually. For example, it can be used to run a transition only if certain conditions are met:

.bind(model.$x, function(x) {
    if (skipAnimation) {
        this.attr("x", x);
    } else {
        this.transition().duration(750).attr("x", x);
    }
})
.bindCall(observable, handler, [transition])

The .bindCall() function applies the handler function to the selection every time the observable model changes. Usually the handler function is something that you would use repeatedly with the selection.call() function of d3.js. For example:

var x = d3bind.scale.linear().domain([0,1]).range([0, 300]);
var xAxis = d3.svg.axis().scale(x);
svg.append("g").bindCall(x.$domain, xAxis);

x.domain([0,2]); // applies xAxis to the <g> element again
.bindRedraw(observable, handler)

Every time the observed model value changes, the .bindRedraw() function removes the current children of the selection, and then runs the handler to rerender the contents of the view element. The input of the handler function is the current value or values of the observable parameter. The handler should contain append or insert calls on the selection, to redraw the contents.

selection.bindRedraw(model.$visible, function(visible) {
     if (visible) {
         this.append('div').bindText(model.$message);
     }
});

Input binding

It's also possible to bind an input element's value. Unlike all the other bindings - which propagate changes one way only, from model to view -, this one actually implements two-way binding: when the model changes, the input element is updated, and when the user types into the input, the model gets updated.

var model = d3bind.observable({ myValue: 'test' });

view.append('input').attr('type', 'text').bindInput(model.$myValue);
view.append('div').bindText(model.$myValue);

The bindInput function works with all kinds of input types, including text, number and checkbox. If you don't want to use this immediate two-way binding, you can just resort to using .bindProperty('value', model.$myValue) and listening to the input element's DOM events with the selection.on(..) function of d3.js.

d3.js selections

For the binding functions to be present on a selection, you usually need to create the root of the selection with d3bind.select(..) or d3bind.selectAll(..). But it is also possible to take an existing d3.js selection, and extend it with the binding methods using d3bind.wrap:

var sel = d3bind.wrap(d3.select('#stuff'));
sel.bindText(model.$name);

Clone this wiki locally