#!/usr/bin/env python # coding: utf-8 # Before reading, make sure to review # # - [MVC prgramming](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) # - [Backbone.js](https://www.codeschool.com/courses/anatomy-of-backbonejs) # - [The widget IPEP](https://github.com/ipython/ipython/wiki/IPEP-23%3A-Backbone.js-Widgets) # - [The original widget PR discussion](https://github.com/ipython/ipython/pull/4374) # In[ ]: from __future__ import print_function # For py 2.7 compat from IPython.html import widgets # Widget definitions from IPython.display import display # Used to display widgets in the notebook from IPython.utils.traitlets import Unicode # Used to declare attributes of our widget # # Abstract # This notebook implements a custom date picker widget, # in order to demonstrate the widget creation process. # # To create a custom widget, both Python and JavaScript code is required. # # Section 1 - Basics # ## Python # When starting a project like this, it is often easiest to make a simple base implementation, # 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 define the widget in Python. # In[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) # Our widget inherits from `widgets.DOMWidget` since it is intended that it will be displayed in the notebook directly. # The `_view_name` trait is special; the widget framework will read the `_view_name` trait to determine what Backbone view the widget is associated with. # **Using `sync=True` is very important** because it tells the widget framework that that specific traitlet should be synced between the front- and back-ends. # ## JavaScript # In the IPython notebook [require.js](http://requirejs.org/) is used to load JavaScript dependencies. # All IPython widget code depends on `widgets/js/widget.js`, # where the base widget model and base view are defined. # We use require.js to load this file: # In[ ]: get_ipython().run_cell_magic('javascript', '', '\nrequire(["widgets/js/widget"], function(WidgetManager){\n\n});\n') # Now we need to define a view that can be used to represent the model. # To do this, the `IPython.DOMWidgetView` 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, for which we need to load another module. # # **Final JavaScript code below:** # In[ ]: get_ipython().run_cell_magic('javascript', '', '\nrequire(["widgets/js/widget", "widgets/js/manager"], function(widget, manager){\n \n // Define the DatePickerView\n var DatePickerView = widget.DOMWidgetView.extend({\n render: function(){ this.$el.text(\'Hello World!\'); },\n });\n \n // Register the DatePickerView with the widget manager.\n manager.WidgetManager.register_widget_view(\'DatePickerView\', DatePickerView);\n});\n') # ## Test # To test what we have so far, create the widget, just like you would the builtin widgets: # In[ ]: DateWidget() # # Section 2 - Something useful # ## Python # In the last section we created a simple widget that displayed *Hello World!* # 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 attribute must be a traitlet, so the widget machinery can handle it. # The traitlet must be constructed with a `sync=True` keyword argument, to tell the widget machinery knows to synchronize it with the front-end. # Adding this to the code from the last section: # In[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) value = Unicode(sync=True) # ## JavaScript # In the JavaScript, there is no need to define counterparts to the traitlets. # When the JavaScript model is created for the first time, # it copies all of the traitlet `sync=True` attributes from the Python model. # We need to replace *Hello World!* with an actual HTML date picker widget. # In[ ]: get_ipython().run_cell_magic('javascript', '', '\nrequire(["widgets/js/widget", "widgets/js/manager"], function(widget, manager){\n \n // Define the DatePickerView\n var DatePickerView = widget.DOMWidgetView.extend({\n render: function(){\n \n // Create the date picker control.\n this.$date = $(\'\')\n .attr(\'type\', \'date\')\n .appendTo(this.$el);\n },\n });\n \n // Register the DatePickerView with the widget manager.\n manager.WidgetManager.register_widget_view(\'DatePickerView\', DatePickerView);\n});\n') # 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[ ]: get_ipython().run_cell_magic('javascript', '', '\nrequire(["widgets/js/widget", "widgets/js/manager"], function(widget, manager){\n \n // Define the DatePickerView\n var DatePickerView = widget.DOMWidgetView.extend({\n render: function(){\n \n // Create the date picker control.\n this.$date = $(\'\')\n .attr(\'type\', \'date\')\n .appendTo(this.$el);\n },\n \n update: function() {\n \n // Set the value of the date control and then call base.\n this.$date.val(this.model.get(\'value\')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required\n return DatePickerView.__super__.update.apply(this);\n },\n });\n \n // Register the DatePickerView with the widget manager.\n manager.WidgetManager.register_widget_view(\'DatePickerView\', DatePickerView);\n});\n') # To get the changed value from the frontend to publish itself to the backend, # we need to listen to the change event triggered by the HTM date control and set the value in the model. # After the date change event fires and the new value is set in the model, # it is very important that we call `this.touch()` 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[ ]: get_ipython().run_cell_magic('javascript', '', '\n\nrequire(["widgets/js/widget", "widgets/js/manager"], function(widget, manager){\n \n // Define the DatePickerView\n var DatePickerView = widget.DOMWidgetView.extend({\n render: function(){\n \n // Create the date picker control.\n this.$date = $(\'\')\n .attr(\'type\', \'date\')\n .appendTo(this.$el);\n },\n \n update: function() {\n \n // Set the value of the date control and then call base.\n this.$date.val(this.model.get(\'value\')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required\n return DatePickerView.__super__.update.apply(this);\n },\n \n // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n events: {"change": "handle_date_change"},\n \n // Callback for when the date is changed.\n handle_date_change: function(event) {\n this.model.set(\'value\', this.$date.val());\n this.touch();\n },\n });\n \n // Register the DatePickerView with the widget manager.\n manager.WidgetManager.register_widget_view(\'DatePickerView\', DatePickerView);\n});\n') # ## Test # To test, create the widget the same way that the other widgets are created. # In[ ]: my_widget = DateWidget() display(my_widget) # Display the widget again to make sure that both views remain in sync. # In[ ]: my_widget # Read the date from Python # In[ ]: my_widget.value # Set the date from Python # In[ ]: my_widget.value = "1998-12-01" # December 1st, 1998 # # Section 3 - Extra credit # The 3rd party `dateutil` library is required to continue. https://pypi.python.org/pypi/python-dateutil # In[ ]: # Import the dateutil library to parse date strings. from dateutil import parser # In the last section we created a fully working date picker widget. # Now we will add custom validation and support for labels. # So far, only the ISO date format "YYYY-MM-DD" is supported. # Now, we will add support for all of the date formats recognized by the Python dateutil library. # ## Python # The standard property name used for widget labels is `description`. # In the code block below, `description` has been added to the Python widget. # In[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) value = Unicode(sync=True) description = Unicode(sync=True) # 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[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) value = Unicode(sync=True) description = Unicode(sync=True) # 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 parses the date string, # and only sets the value in the correct format. # In[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) value = Unicode(sync=True) description = Unicode(sync=True) # 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 `CallbackDispatcher` is added so the user can perform custom validation. # If any one of the callbacks registered with the dispatcher returns False, # the new date is not set. # # **Final Python code below:** # In[ ]: class DateWidget(widgets.DOMWidget): _view_name = Unicode('DatePickerView', sync=True) value = Unicode(sync=True) description = Unicode(sync=True) def __init__(self, **kwargs): super(DateWidget, self).__init__(**kwargs) self.validate = widgets.CallbackDispatcher() # 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 old_value != new_value: valid = self.validate(parsed_date) if valid in (None, True): 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. # ## 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. # `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[ ]: get_ipython().run_cell_magic('javascript', '', '\nrequire(["widgets/js/widget", "widgets/js/manager"], function(widget, manager){\n \n // Define the DatePickerView\n var DatePickerView = widget.DOMWidgetView.extend({\n render: function(){\n this.$el.addClass(\'widget-hbox-single\'); /* Apply this class to the widget container to make\n it fit with the other built in widgets.*/\n // Create a label.\n this.$label = $(\'
\')\n .addClass(\'widget-hlabel\')\n .appendTo(this.$el)\n .hide(); // Hide the label by default.\n \n // Create the date picker control.\n this.$date = $(\'\')\n .attr(\'type\', \'date\')\n .appendTo(this.$el);\n },\n \n update: function() {\n \n // Set the value of the date control and then call base.\n this.$date.val(this.model.get(\'value\')); // ISO format "YYYY-MM-DDTHH:mm:ss.sssZ" is required\n \n // Hide or show the label depending on the existance of a description.\n var description = this.model.get(\'description\');\n if (description == undefined || description == \'\') {\n this.$label.hide();\n } else {\n this.$label.show();\n this.$label.text(description);\n }\n \n return DatePickerView.__super__.update.apply(this);\n },\n \n // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n events: {"change": "handle_date_change"},\n \n // Callback for when the date is changed.\n handle_date_change: function(event) {\n this.model.set(\'value\', this.$date.val());\n this.touch();\n },\n });\n \n // Register the DatePickerView with the widget manager.\n manager.WidgetManager.register_widget_view(\'DatePickerView\', DatePickerView);\n});\n') # ## Test # To test the drawing of the label we create the widget like normal but supply the additional description property a value. # In[ ]: # Add some additional widgets for aesthetic purpose display(widgets.Text(description="First:")) display(widgets.Text(description="Last:")) my_widget = DateWidget() display(my_widget) my_widget.description="DOB:" # Now we will try to create a widget that only accepts dates in the year 2014. We render the widget without a description to verify that it can still render without a label. # In[ ]: my_widget = DateWidget() display(my_widget) def require_2014(date): return not date is None and date.year == 2014 my_widget.validate.register_callback(require_2014) # In[ ]: # Try setting a valid date my_widget.value = "December 2, 2014" # In[ ]: # Try setting an invalid date my_widget.value = "June 12, 1999" # In[ ]: my_widget.value # In[ ]: