##// END OF EJS Templates
Added custom widget tutorial examples
Added custom widget tutorial examples

File last commit:

r17494:6c208dd7
r17494:6c208dd7
Show More
Custom Widget - Hello World.ipynb
836 lines | 25.6 KiB | text/plain | TextLexer
/ examples / Interactive Widgets / Custom Widget - Hello World.ipynb
In [20]:
from __future__ import print_function # For py 2.7 compat

Building a Custom Widget

The widget framework is built on top of the Comm framework (short for communication). The Comm framework is a framework that allows you send/recieve JSON messages to/from the front-end. To create a custom widget, you need to define the widget both in the back-end and in the front-end.

Back-end (Python)

DOMWidget and Widget

To define a widget, you must inherit from the Widget or DOMWidget base class. If you intend for your widget to be displayed in the IPython notebook, you'll need to inherit from the DOMWidget. The DOMWidget class itself inherits from the Widget class. The Widget class is useful for cases in which the Widget isn't meant to be displayed directly in the notebook, but instead as a child of another rendering environment. For example, if you wanted to create a D3.js widget (a popular WebGL library), you would implement the rendering window as a DOMWidget and any 3D objects or lights meant to be rendered in that window as Widgets.

sync=True traitlets

Inheriting from the DOMWidget doesn't tell the widget framework what front-end widget to associate with your back-end widget. Instead, you must tell it yourself by defining a specially named Traitlet, _view_name (as seen below).

In [21]:
from IPython.html import widgets
from IPython.utils.traitlets import Unicode

class HelloWidget(widgets.DOMWidget):
    _view_name = Unicode('HelloView', sync=True)

Traitlets is an IPython library for defining type-safe properties on configurable objects. For this tutorial you do not need to worry about the configurable piece of the traitlets machinery. The sync=True kwarg tells the widget framework to handle synchronizing that value to the front-end. Without sync=True, the front-end would have no knowlege of _view_name.

Other traitlet types

Unicode is not the only Traitlet type, there are many more:

  • Any
  • Bool
  • Bytes
  • CBool
  • CBytes
  • CComplex
  • CFloat
  • CInt
  • CLong
  • CRegExp
  • CUnicode
  • CaselessStrEnum
  • Complex
  • Dict
  • DottedObjectName
  • Enum
  • Float
  • FunctionType
  • Instance
  • InstanceType
  • Int
  • List
  • Long
  • Set
  • TCPAddress
  • Tuple
  • Type
  • Unicode

Not all of these traitlets can be synchronized across the network, only the JSON-able traits and Widget instances will be synchronized.

Front-end (JavaScript)

Models and views

The IPython widget framework front-end relies heavily on Backbone.js. Backbone.js is an MVC (model view controller) framework. Widgets defined in the back-end are automatically synchronized with generic Backbone.js models in the front-end. The traitlets are added to the front-end instance automatically on first state push. The _view_name trait that you defined earlier is used by the widget framework to create the corresponding Backbone.js view and link that view to the generic model.

Defining a view

You first need to import the WidgetManager. You will use it later to register your view by name (the same name you used in the backend). To import the widget manager, use the require method of require.js (as seen below).

In [22]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){
    
});
SandBoxed(IPython.core.display.Javascript object)

Next define your widget view class. Inherit from the DOMWidgetView by using the .extend method. Register the view class with the widget manager by calling .register_widget_view. The first parameter is the widget view name (_view_name that you defined earlier in Python) and the second is a handle to the class type.

In [23]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){
    
    // Define the HelloView
    var HelloView = IPython.DOMWidgetView.extend({
        
    });
    
    // Register the HelloView with the widget manager.
    WidgetManager.register_widget_view('HelloView', HelloView);
});
SandBoxed(IPython.core.display.Javascript object)

Lastly, override the base render method of the view to define custom rendering logic. A handle to the widget's default div element can be aquired via this.$el. The $el property is a jQuery object handle (which can be thought of as a supercharged version of the normal DOM element's handle).

In [24]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){ 
    
    var HelloView = IPython.DOMWidgetView.extend({
        
        // Render the view.
        render: function(){ 
            this.$el.text('Hello World!'); 
        },
    });
    
    WidgetManager.register_widget_view('HelloView', HelloView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

You should be able to display your widget just like any other widget now.

In [25]:
HelloWidget()

Making the widget stateful

There isn't much that you can do with the above example that you can't do with the IPython display framework. To change this, you will make the widget stateful. Instead of displaying a static "hello world" message, it will display a string set by the backend. First you need to add the traitlet in the back-end. Use the name of value to stay consistent with the rest of the widget framework and to allow your widget to be used with interact.

In [26]:
class HelloWidget(widgets.DOMWidget):
    _view_name = Unicode('HelloView', sync=True)
    value = Unicode('Hello World!', sync=True)

Backbone.js model

To access the model associate with a view instance, use the model property of the view. get and set methods are used to interact with the Backbone model. get is trivial, however you have to be careful when using set. After calling the model set you need call the view's touch method. This associates the set operation with a particular view so output will be routed to the correct IPython cell. The model also has a on method which allows you to listen to events triggered by the model (like value changes).

Rendering model contents

By replacing the string literal with a call to model.get, the view will now display the value of the back-end upon display. However, it will not update itself to a new value when the value changes.

In [27]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){ 
    
    var HelloView = IPython.DOMWidgetView.extend({
        
        render: function(){ 
            this.$el.text(this.model.get('value')); 
        },
    });
    
    WidgetManager.register_widget_view('HelloView', HelloView);
});
SandBoxed(IPython.core.display.Javascript object)

To get the view to update itself dynamically, register a function to update the view's value when the model's value property changes. This can be done using the model.on method. The on method takes three parameters, an event name, callback handle, and callback context. The Backbone event named change will fire whenever the model changes. By appending :value to it, you tell Backbone to only listen to the change event of the value property (as seen below).

In [38]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){ 
    
    var HelloView = IPython.DOMWidgetView.extend({
        
        
        render: function(){ 
            this.value_changed();
            this.model.on('change:value', this.value_changed, this);
        },
        
        value_changed: function() {
            this.$el.text(this.model.get('value')); 
        },
    });
    
    WidgetManager.register_widget_view('HelloView', HelloView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

In [39]:
w = HelloWidget()
w
In [40]:
w.value = 'test'

Bidirectional communication

The examples above dump the value directly into the DOM. There is no way for you to interact with this dumped data in the front-end. To create an example that accepts input, you will have to do something more than blindly dumping the contents of value into the DOM. For this part of the tutorial, you will use a jQuery spinner to display and accept input in the front-end. IPython currently lacks a spinner implementation so this widget will be unique.

Update the Python code

You will need to change the type of the value traitlet to Int. It also makes sense to change the name of the widget to something more appropriate, like SpinnerWidget.

In [41]:
from IPython.utils.traitlets import CInt
class SpinnerWidget(widgets.DOMWidget):
    _view_name = Unicode('SpinnerView', sync=True)
    value = CInt(0, sync=True)

Updating the Javascript code

The jQuery docs for the spinner control say to use .spinner to create a spinner in an element. Calling .spinner on $el will create a spinner inside $el. Make sure to update the widget name here too so it's the same as _view_name in the back-end.

In [44]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){ 
    
    var SpinnerView = IPython.DOMWidgetView.extend({
        
        render: function(){ 
            
            // jQuery code to create a spinner and append it to $el
            this.$input = $('<input />');
            this.$el.append(this.$input);
            this.$spinner = this.$input.spinner({
                change: function( event, ui ) {}
            });
            
            this.value_changed();
            this.model.on('change:value', this.value_changed, this);
        },
        
        value_changed: function() {
            
        },
    });
    
    WidgetManager.register_widget_view('SpinnerView', SpinnerView);
});
SandBoxed(IPython.core.display.Javascript object)

To set the value of the spinner on update, you need to use jQuery's spinner API. spinner.spinner('value', new) will set the value of the spinner. Add that code to the value_changed method to make the spinner update with the value stored in the back-end. Using jQuery's spinner API, you can add a function to handle the spinner change event by passing it in when constructing the spinner. Inside the change event, call model.set to set the value and then touch to inform the framework that this view was the view that caused the change to the model. Note: The var that = this; is a JavaScript trick to pass the current context into closures.

In [74]:
%%javascript

require(["widgets/js/widget"], function(WidgetManager){ 
    
    var SpinnerView = IPython.DOMWidgetView.extend({
        
        render: function(){ 

            var that = this;
            this.$input = $('<input />');
            this.$el.append(this.$input);
            this.$spinner = this.$input.spinner({
                change: function( event, ui ) {
                    that.handle_spin();
                },
                spin: function( event, ui ) {
                    that.handle_spin();
                }
            });
            
            this.value_changed();
            this.model.on('change:value', this.value_changed, this);
        },
        
        value_changed: function() {
            this.$spinner.spinner('value', this.model.get('value'));
        },
        
        handle_spin: function() {
            this.model.set('value', this.$spinner.spinner('value'));
            this.touch();
        },
    });
    
    WidgetManager.register_widget_view('SpinnerView', SpinnerView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

In [75]:
w = SpinnerWidget(value=5)
w
In [76]:
w.value
Out[76]:
5
In [77]:
w.value = 20

Trying to use the spinner with another widget.

In [78]:
w1 = SpinnerWidget(value=0)
w2 = widgets.IntSliderWidget()
display(w1,w2)

from IPython.utils.traitlets import link
mylink = link((w1, 'value'), (w2, 'value'))