Creating a custom notebook widget with backbone.js

In this last part, we will see how to create an entirely custom JavaScript widget. This widget lets us choose an integer with + and - buttons.

We will make use of jQuery, a popular JavaScript library, and backbone.js, a JavaScript framework for designing client-side applications. The IPython notebook is built on top of these libraries.

Let's do a few imports.


In [1]:
from IPython.html.widgets import interact, DOMWidget, IntSliderWidget
from IPython.utils.traitlets import Unicode, Int, link

We first need to create a Python class representing our widget, deriving from DOMWidget.


In [2]:
class NumberSelectorWidget(DOMWidget):
    _view_name = Unicode('NumberSelector', sync=True)
    description = Unicode(help="Description of the value this widget represents", sync=True)
    value = Int(0, sync=True)
  • We specify a _view_name trait attribute, synchronized with the browser view (sync=True). This attribute is mandatory.
  • The description attribute is necessary when using the widget with @interact (see below).
  • We also specify a value attribute containing the widget's value.

We could create other trait attributes. However, value is an important attribute to have when we want the widget to play well with the rest of the widget machinery in IPython.

Trait attributes are supercharged versions of class attributes in Python. Implemented in IPython, they have a type, and they offer a way to react to changes in Python callback functions.

Now we write the JavaScript logic of the widget.


In [3]:
%%javascript
// We import the WidgetManager.
require(["widgets/js/widget"], function(WidgetManager){ 
    
    // We define the NumberSelector view here.
    var NumberSelector = IPython.DOMWidgetView.extend({
        
        // Function for rendering the view.
        render: function(){
            
            // Little trick to pass the current context into closures.
            var that = this;
            
            // We now create the HTML widget using jQuery.
            // First, we create two buttons for + and -.
            var button_minus = $('<button style="width: 30px;">-</button>');
            var button_plus = $('<button style="width: 30px;">+</button>');
            
            // We also create <span> element displaying the number.
            this.$span = $('<span style="margin: 0 10px;">0</span>');
            
            // We add these three elements to the main <div> element of the
            // widget (this.$el, a jQuery handle).
            this.$el.append(button_minus);
            this.$el.append(this.$span);
            this.$el.append(button_plus);
            
            // Clicking on the buttons calls `change_value()`.
            button_minus.click(function() { that.change_value(-1); });
            button_plus.click(function() { that.change_value(+1); });
            
            // This instructs backbone.js to call `value_changed()` whenever the 
            // model's value changes (notably in the corresponding Python model).
            this.model.on('change:value', this.value_changed, this);
            
            // We add a keyboard shortcut.
            this.$el.attr('tabindex', '1').keydown(function (e) {
                if (e.keyCode == 109){
                    that.change_value(-1);
                }
                else if (e.keyCode == 107){
                    that.change_value(+1);
                }
            });
            
        },
        
        // What happens when the model's value changes.
        value_changed: function() {
            
            // We update the <span> element with the value.
            // We can recover the model's value with `this.model.get('value')`.
            this.$span.html(this.model.get('value'));
            
        },
        
        // What happens when we click on the buttons.
        change_value: function(dx) {
            
            // We first recover the current value.
            var value = this.model.get('value');
            
            // We update it.
            this.model.set('value', value + dx);
            
            // This associates the `set()` operation with a particular view
            // so that the output will be routed to the correct cell.
            this.touch();
        }
    });
    
    // We register the NumberSelector widget (must correspond to '_view_name' in the Python widget).
    WidgetManager.register_widget_view('NumberSelector', NumberSelector);
});


Let's test our widget!


In [9]:
widget = NumberSelectorWidget()
widget

The widget's value is synchronized with Python.


In [13]:
widget.value = 1


New value: 1

The widget's value can be synchronized with other widgets, using the link() function.


In [7]:
widget2 = IntSliderWidget(min=-10, max=+10)
mylink = link((widget, 'value'), (widget2, 'value'))
widget2

Our widget can also be used with @interact.


In [8]:
@interact(x=NumberSelectorWidget())
def f(x):
    print("%d^2=%d" % (x, x*x))


0^2=0