##// END OF EJS Templates
Remove init_widget_js, use require.js for everything...
Remove init_widget_js, use require.js for everything Updated examples Fixed bug with message throttling

File last commit:

r14342:90efa5b8
r14342:90efa5b8
Show More
Part 6 - Custom Widget.ipynb
1202 lines | 42.9 KiB | text/plain | TextLexer
/ examples / widgets / Part 6 - Custom Widget.ipynb

Before reading, the author recommends the reader to review

In [1]:
from IPython.html import widgets # Widget definitions
from IPython.display import display # Used to display widgets in the notebook

Abstract

This notebook implements a custom date picker widget. The purpose of this notebook is to demonstrate the widget creation process. To create a custom widget, custom Python and JavaScript is required.

Section 1 - Basics

Python

When starting a project like this, it is often easiest to make an overly simplified base to verify that the underlying framework is working as expected. To start we will create an empty widget and make sure that it can be rendered. The first step is to create the widget in Python.

In [2]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
  • target_name is a special Widget property that tells the widget framework which Backbone model in the front-end corresponds to this widget.
  • default_view_name is the default Backbone view to display when the user calls display to display an instance of this widget.

JavaScript

In the IPython notebook require.js is used to load JavaScript dependencies. All IPython widget code depends on notebook/js/widget.js. In it the base widget model, base view, and widget manager are defined. We need to use require.js to include this file:

In [3]:
%%javascript

require(["notebook/js/widget"], function(){

});
SandBoxed(IPython.core.display.Javascript object)

The next step is to add a definition for the widget's model. It's important to extend the IPython.WidgetModel which extends the Backbone.js base model instead of trying to extend the Backbone.js base model directly. After defining the model, it needs to be registed with the widget manager using the target_name used in the Python code.

In [4]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
});
SandBoxed(IPython.core.display.Javascript object)

Now that the model is defined, we need to define a view that can be used to represent the model. To do this, the IPython.WidgetView is extended. A render function must be defined. The render function is used to render a widget view instance to the DOM. For now the render function renders a div that contains the text Hello World! Lastly, the view needs to be registered with the widget manager like the model was.

Final JavaScript code below:

In [5]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
    
    // Define the DatePickerView
    var DatePickerView = IPython.WidgetView.extend({
        
        render: function(){
            this.$el = $('<div />')
                .html('Hello World!');
        },
    });
    
    // Register the DatePickerView with the widget manager.
    IPython.widget_manager.register_widget_view('DatePickerView', DatePickerView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

To test, create the widget the same way that the other widgets are created.

In [6]:
my_widget = DateWidget()
display(my_widget)

Section 2 - Something useful

Python

In the last section we created a simple widget that displayed Hello World! There was no custom state information associated with the widget. To make an actual date widget, we need to add a property that will be synced between the Python model and the JavaScript model. The new property must be a traitlet property so the widget machinery can automatically handle it. The property needs to be added to the the _keys list. The _keys list tells the widget machinery what traitlets should be synced with the front-end. Adding this to the code from the last section:

In [7]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
    
    # Define the custom state properties to sync with the front-end
    _keys = ['value']
    value = Unicode()

JavaScript

In the JavaScript there is no need to define the same properties in the JavaScript model. When the JavaScript model is created for the first time, it copies all of the attributes from the Python model. We need to replace Hello World! with an actual HTML date picker widget.

In [8]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
    
    // Define the DatePickerView
    var DatePickerView = IPython.WidgetView.extend({
        
        render: function(){
            
            // Create a div to hold our widget.
            this.$el = $('<div />');
            
            // Create the date picker control.
            this.$date = $('<input />')
                .attr('type', 'date');
        },
    });
    
    // Register the DatePickerView with the widget manager.
    IPython.widget_manager.register_widget_view('DatePickerView', DatePickerView);
});
SandBoxed(IPython.core.display.Javascript object)

In order to get the HTML date picker to update itself with the value set in the back-end, we need to implement an update() method.

In [9]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
    
    // Define the DatePickerView
    var DatePickerView = IPython.WidgetView.extend({
        
        render: function(){
            
            // Create a div to hold our widget.
            this.$el = $('<div />');
            
            // Create the date picker control.
            this.$date = $('<input />')
                .attr('type', 'date');
        },
        
        update: function() {
            
            // Set the value of the date control and then call base.
            this.$date.val(this.model.get('value')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required
            return IPython.WidgetView.prototype.update.call(this);
        },
    });
    
    // Register the DatePickerView with the widget manager.
    IPython.widget_manager.register_widget_view('DatePickerView', DatePickerView);
});
SandBoxed(IPython.core.display.Javascript object)

To get the changed value from the front-end to publish itself to the back-end, we need to listen to the change event triggered by the HTM date control and set the value in the model. By setting the this.$el property of the view, we break the Backbone powered event handling. To fix this, a call to this.delegateEvents() must be added after this.$el is set.

After the date change event fires and the new value is set in the model, it's very important that we call update_other_views(this) to make the other views on the page update and to let the widget machinery know which view changed the model. This is important because the widget machinery needs to know which cell to route the message callbacks to.

Final JavaScript code below:

In [10]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
    
    // Define the DatePickerView
    var DatePickerView = IPython.WidgetView.extend({
        
        render: function(){
            
            // Create a div to hold our widget.
            this.$el = $('<div />');
            this.delegateEvents();
            
            // Create the date picker control.
            this.$date = $('<input />')
                .attr('type', 'date')
                .appendTo(this.$el);
        },
        
        update: function() {
            
            // Set the value of the date control and then call base.
            this.$date.val(this.model.get('value')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required
            return IPython.WidgetView.prototype.update.call(this);
        },
        
        // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)
        events: {"change": "handle_date_change"},
        
        // Callback for when the date is changed.
        handle_date_change: function(event) {
            this.model.set('value', this.$date.val());
            this.model.update_other_views(this);
        },
        
    });
    
    // Register the DatePickerView with the widget manager.
    IPython.widget_manager.register_widget_view('DatePickerView', DatePickerView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

To test, create the widget the same way that the other widgets are created.

In [11]:
my_widget = DateWidget()
display(my_widget)

Display the widget again to make sure that both views remain in sync.

In [12]:
display(my_widget)

Read the date from Python

In [13]:
my_widget.value
Out[13]:
u'2013-11-14'

Set the date from Python

In [14]:
my_widget.value = "1999-12-01" # December 1st, 1999

Section 3 - Extra credit

In the last section we created a fully working date picker widget. Now we will add custom validation and support for labels. Currently only the ISO date format "YYYY-MM-DD" is supported. We will add support for all of the date formats recognized by the 3rd party Python dateutil library.

Python

The traitlet machinery searches the class that the trait is defined in for methods with "_changed" suffixed onto their names. Any method with the format "X_changed" will be called when "X" is modified. We can take advantage of this to perform validation and parsing of different date string formats. Below a method that listens to value has been added to the DateWidget.

In [15]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
    
    # Define the custom state properties to sync with the front-end
    _keys = ['value']
    value = Unicode()
    
    # This function automatically gets called by the traitlet machinery when
    # value is modified because of this function's name.
    def _value_changed(self, name, old_value, new_value):
        pass
    

Now the function that parses the date string and only sets it in the correct format can be added.

In [16]:
# Import the dateutil library to parse date strings.
from dateutil import parser

# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
    
    # Define the custom state properties to sync with the front-end
    _keys = ['value']
    value = Unicode()
    
    # This function automatically gets called by the traitlet machinery when
    # value is modified because of this function's name.
    def _value_changed(self, name, old_value, new_value):
        
        # Parse the date time value.
        try:
            parsed_date = parser.parse(new_value)
            parsed_date_string = parsed_date.strftime("%Y-%m-%d")
        except:
            parsed_date_string = ''
        
        # Set the parsed date string if the current date string is different.
        if self.value != parsed_date_string:
            self.value = parsed_date_string

The standard property name used for widget labels is description. In the code block below, description has been added to the Python widget.

In [17]:
# Import the dateutil library to parse date strings.
from dateutil import parser

# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
    
    # Define the custom state properties to sync with the front-end
    _keys = ['value', 'description']
    value = Unicode()
    description = Unicode()
    
    # This function automatically gets called by the traitlet machinery when
    # value is modified because of this function's name.
    def _value_changed(self, name, old_value, new_value):
        
        # Parse the date time value.
        try:
            parsed_date = parser.parse(new_value)
            parsed_date_string = parsed_date.strftime("%Y-%m-%d")
        except:
            parsed_date_string = ''
        
        # Set the parsed date string if the current date string is different.
        if self.value != parsed_date_string:
            self.value = parsed_date_string

Finally, a callback list is added so the user can perform custom validation. If any one of the callbacks returns False, the new date time is not set.

Final Python code below:

In [18]:
# Import the dateutil library to parse date strings.
from dateutil import parser

# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets import Widget
from IPython.utils.traitlets import Unicode

# Define our DateWidget and its target model and default view.
class DateWidget(Widget):
    target_name = Unicode('DateWidgetModel')
    default_view_name = Unicode('DatePickerView')
    
    # Define the custom state properties to sync with the front-end
    _keys = ['value', 'description']
    value = Unicode()
    description = Unicode()
    
    def __init__(self, **kwargs):
        super(DateWidget, self).__init__(**kwargs)
        self._validation_callbacks = []
    
    # This function automatically gets called by the traitlet machinery when
    # value is modified because of this function's name.
    def _value_changed(self, name, old_value, new_value):
        
        # Parse the date time value.
        try:
            parsed_date = parser.parse(new_value)
            parsed_date_string = parsed_date.strftime("%Y-%m-%d")
        except:
            parsed_date = None
            parsed_date_string = ''
        
        # Set the parsed date string if the current date string is different.
        if old_value != new_value:
            if self.handle_validate(parsed_date):
                self.value = parsed_date_string
            else:
                self.value = old_value
                self.send_state() # The traitlet event won't fire since the value isn't changing.
                                  # We need to force the back-end to send the front-end the state
                                  # to make sure that the date control date doesn't change.
                
    
    # Allow the user to register custom validation callbacks.
    # callback(new value as a datetime object)
    def on_validate(self, callback, remove=False):
        if remove and callback in self._validation_callbacks:
            self._validation_callbacks.remove(callback)
        elif (not remove) and (not callback in self._validation_callbacks):
            self._validation_callbacks.append(callback)
    
    # Call user validation callbacks.  Return True if valid.
    def handle_validate(self, new_value):
        for callback in self._validation_callbacks:
            if not callback(new_value):
                return False
        return True
        

JavaScript

Using the Javascript code from the last section, we add a label to the date time object. The label is a div with the widget-hlabel class applied to it. The widget-hlabel is a class provided by the widget framework that applies special styling to a div to make it look like the rest of the horizontal labels used with the built in widgets. Similar to the widget-hlabel class is the widget-hbox-single class. The widget-hbox-single class applies special styling to widget containers that store a single line horizontal widget.

We hide the label if the description value is blank.

In [19]:
%%javascript

require(["notebook/js/widget"], function(){
    
    // Define the DateModel and register it with the widget manager.
    var DateModel = IPython.WidgetModel.extend({});
    IPython.widget_manager.register_widget_model('DateWidgetModel', DateModel);
    
    // Define the DatePickerView
    var DatePickerView = IPython.WidgetView.extend({
        
        render: function(){
            
            // Create a div to hold our widget.
            this.$el = $('<div />')
                .addClass('widget-hbox-single'); // Apply this class to the widget container to make
                                                 // it fit with the other built in widgets.
            this.delegateEvents();
            
            // Create a label.
            this.$label = $('<div />')
                .addClass('widget-hlabel')
                .appendTo(this.$el)
                .hide(); // Hide the label by default.
            
            // Create the date picker control.
            this.$date = $('<input />')
                .attr('type', 'date')
                .appendTo(this.$el);
        },
        
        update: function() {
            
            // Set the value of the date control and then call base.
            this.$date.val(this.model.get('value')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required
            
            // Hide or show the label depending on the existance of a description.
            var description = this.model.get('description');
            if (description == undefined || description == '') {
                this.$label.hide();
            } else {
                this.$label.show();
                this.$label.html(description);
            }
            
            return IPython.WidgetView.prototype.update.call(this);
        },
        
        // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)
        events: {"change": "handle_date_change"},
        
        // Callback for when the date is changed.
        handle_date_change: function(event) {
            this.model.set('value', this.$date.val());
            this.model.update_other_views(this);
        },
        
    });
    
    // Register the DatePickerView with the widget manager.
    IPython.widget_manager.register_widget_view('DatePickerView', DatePickerView);
});
SandBoxed(IPython.core.display.Javascript object)

Test

To test the drawing of the label we create the widget like normal but supply the additional description property a value.

In [20]:
# Add some additional widgets for aesthetic purpose
display(widgets.StringWidget(description="First:"))
display(widgets.StringWidget(description="Last:"))

my_widget = DateWidget(description="DOB:")
display(my_widget)

Since the date widget uses value and description, we can also display its value using a TextBoxView. The allows us to look at the raw date value being passed to and from the back-end and front-end.

In [21]:
display(my_widget, view_name="TextBoxView")

Now we will try to create a widget that only accepts dates in the year 2013. We render the widget without a description to verify that it can still render without a label.

In [22]:
my_widget = DateWidget()
display(my_widget)

def validate_date(date):
    return not date is None and date.year == 2013
my_widget.on_validate(validate_date)
In [23]:
# Try setting a valid date
my_widget.value = "December 2, 2013"
In [24]:
# Try setting an invalid date
my_widget.value = "June 12, 1999"