Part 6 - Custom Widget.ipynb
1064 lines
| 35.9 KiB
| text/plain
|
TextLexer
Jonathan Frederic
|
r14340 | { | |
"metadata": { | |||
"cell_tags": [ | |||
[ | |||
"<None>", | |||
null | |||
] | |||
], | |||
"name": "" | |||
}, | |||
"nbformat": 3, | |||
"nbformat_minor": 0, | |||
"worksheets": [ | |||
{ | |||
"cells": [ | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"Before reading, the author recommends the reader to review\n", | |||
"\n", | |||
"- [MVC prgramming](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)\n", | |||
"- [Backbone.js](https://www.codeschool.com/courses/anatomy-of-backbonejs)\n", | |||
"- [The widget IPEP](https://github.com/ipython/ipython/wiki/IPEP-23%3A-Backbone.js-Widgets)\n", | |||
"- [The original widget PR discussion](https://github.com/ipython/ipython/pull/4374)" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
Jonathan Frederic
|
r14413 | "from __future__ import print_function # For py 2.7 compat\n", | |
"\n", | |||
Jonathan Frederic
|
r14340 | "from IPython.html import widgets # Widget definitions\n", | |
Jonathan Frederic
|
r14342 | "from IPython.display import display # Used to display widgets in the notebook" | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
Jonathan Frederic
|
r14342 | "outputs": [], | |
Jonathan Frederic
|
r14720 | "prompt_number": 19 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
Jonathan Frederic
|
r14413 | "cell_type": "markdown", | |
"metadata": {}, | |||
"source": [ | |||
"The 3 part of this tutorial requires the 3rd party `dateutil` library. https://pypi.python.org/pypi/python-dateutil" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the dateutil library to parse date strings.\n", | |||
"from dateutil import parser" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 20 | |
Jonathan Frederic
|
r14413 | }, | |
{ | |||
Jonathan Frederic
|
r14340 | "cell_type": "heading", | |
"level": 1, | |||
"metadata": {}, | |||
"source": [ | |||
"Abstract" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "heading", | |||
"level": 1, | |||
"metadata": {}, | |||
"source": [ | |||
"Section 1 - Basics" | |||
] | |||
}, | |||
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Python" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)" | |||
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 21 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "- **_view_name** is the default Backbone view to display when the user calls `display` to display an instance of this widget.\n" | |
Jonathan Frederic
|
r14340 | ] | |
}, | |||
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"JavaScript" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "In the IPython notebook [require.js](http://requirejs.org/) is used to load JavaScript dependencies. All IPython widget code depends on `notebook/js/widgets/widget.js`. In it the base widget model and base view are defined. We need to use require.js to include this file:" | |
Jonathan Frederic
|
r14340 | ] | |
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | "\n", | |
"});" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | "\n", | |
"});" | |||
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1e409d0>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 22 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "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 like the model was.\n", | |
Jonathan Frederic
|
r14340 | "\n", | |
"**Final JavaScript code below:**" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
" render: function(){ this.$el.text('Hello World!'); },\n", | |||
Jonathan Frederic
|
r14340 | " });\n", | |
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
" render: function(){ this.$el.text('Hello World!'); },\n", | |||
Jonathan Frederic
|
r14340 | " });\n", | |
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1e40a50>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jason Grout
|
r14505 | "prompt_number": 23 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Test" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"To test, create the widget the same way that the other widgets are created." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
Jonathan Frederic
|
r14720 | "DateWidget()" | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jason Grout
|
r14505 | "prompt_number": 24 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 1, | |||
"metadata": {}, | |||
"source": [ | |||
"Section 2 - Something useful" | |||
] | |||
}, | |||
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Python" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "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 property must be a traitlet property so the widget machinery can automatically handle it. The property needs to be constructed with a `sync=True` keyword argument so the widget machinery knows to syncronize it with the fron-end. Adding this to the code from the last section:" | |
Jonathan Frederic
|
r14340 | ] | |
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)\n", | |||
" value = Unicode(sync=True)" | |||
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jason Grout
|
r14505 | "prompt_number": 25 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"JavaScript" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\n", | |||
Jonathan Frederic
|
r14720 | " .attr('type', 'date')\n", | |
" .appendTo(this.$el);\n", | |||
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\n", | |||
Jonathan Frederic
|
r14720 | " .attr('type', 'date')\n", | |
" .appendTo(this.$el);\n", | |||
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1e40810>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jason Grout
|
r14505 | "prompt_number": 26 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\n", | |||
Jonathan Frederic
|
r14720 | " .attr('type', 'date')\n", | |
" .appendTo(this.$el);\n", | |||
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\n", | |||
Jonathan Frederic
|
r14720 | " .attr('type', 'date')\n", | |
" .appendTo(this.$el);\n", | |||
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1e40390>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jason Grout
|
r14505 | "prompt_number": 27 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "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. After the date change event fires and the new value is set in the model, it's 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.\n", | |
Jonathan Frederic
|
r14340 | "\n", | |
"**Final JavaScript code below:**" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "\n", | |
"require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\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", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " this.touch();\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "\n", | |
"require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
" \n", | |||
" // Create the date picker control.\n", | |||
" this.$date = $('<input />')\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", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " this.touch();\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1e40890>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 28 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Test" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"To test, create the widget the same way that the other widgets are created." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"my_widget = DateWidget()\n", | |||
"display(my_widget)" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jason Grout
|
r14505 | "prompt_number": 29 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"Display the widget again to make sure that both views remain in sync." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
Jonathan Frederic
|
r14720 | "my_widget" | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jason Grout
|
r14505 | "prompt_number": 30 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"Read the date from Python" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"my_widget.value" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"metadata": {}, | |||
"output_type": "pyout", | |||
Jonathan Frederic
|
r14720 | "prompt_number": 31, | |
Jonathan Frederic
|
r14340 | "text": [ | |
Jonathan Frederic
|
r14720 | "u''" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 31 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"Set the date from Python" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
Jason Grout
|
r14505 | "my_widget.value = \"1998-12-01\" # December 1st, 1999" | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 32 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 1, | |||
"metadata": {}, | |||
"source": [ | |||
"Section 3 - Extra credit" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Python" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)\n", | |||
" value = Unicode(sync=True)\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" # This function automatically gets called by the traitlet machinery when\n", | |||
" # value is modified because of this function's name.\n", | |||
" def _value_changed(self, name, old_value, new_value):\n", | |||
" pass\n", | |||
" " | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 33 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"Now the function that parses the date string and only sets it in the correct format can be added." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
Jonathan Frederic
|
r14720 | "# Import the dateutil library to parse date strings.\n", | |
"from dateutil import parser\n", | |||
Jonathan Frederic
|
r14340 | "\n", | |
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)\n", | |||
" value = Unicode(sync=True)\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" # This function automatically gets called by the traitlet machinery when\n", | |||
" # value is modified because of this function's name.\n", | |||
" def _value_changed(self, name, old_value, new_value):\n", | |||
" \n", | |||
" # Parse the date time value.\n", | |||
" try:\n", | |||
" parsed_date = parser.parse(new_value)\n", | |||
" parsed_date_string = parsed_date.strftime(\"%Y-%m-%d\")\n", | |||
" except:\n", | |||
" parsed_date_string = ''\n", | |||
" \n", | |||
" # Set the parsed date string if the current date string is different.\n", | |||
" if self.value != parsed_date_string:\n", | |||
" self.value = parsed_date_string" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 34 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"The standard property name used for widget labels is `description`. In the code block below, `description` has been added to the Python widget." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the dateutil library to parse date strings.\n", | |||
"from dateutil import parser\n", | |||
"\n", | |||
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)\n", | |||
" value = Unicode(sync=True)\n", | |||
" description = Unicode(sync=True)\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" # This function automatically gets called by the traitlet machinery when\n", | |||
" # value is modified because of this function's name.\n", | |||
" def _value_changed(self, name, old_value, new_value):\n", | |||
" \n", | |||
" # Parse the date time value.\n", | |||
" try:\n", | |||
" parsed_date = parser.parse(new_value)\n", | |||
" parsed_date_string = parsed_date.strftime(\"%Y-%m-%d\")\n", | |||
" except:\n", | |||
" parsed_date_string = ''\n", | |||
" \n", | |||
" # Set the parsed date string if the current date string is different.\n", | |||
" if self.value != parsed_date_string:\n", | |||
" self.value = parsed_date_string" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 35 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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.\n", | |||
"\n", | |||
"**Final Python code below:**" | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Import the dateutil library to parse date strings.\n", | |||
"from dateutil import parser\n", | |||
"\n", | |||
"# Import the base Widget class and the traitlets Unicode class.\n", | |||
Jonathan Frederic
|
r14720 | "from IPython.html.widgets import DOMWidget, CallbackDispatcher\n", | |
Jonathan Frederic
|
r14340 | "from IPython.utils.traitlets import Unicode\n", | |
"\n", | |||
"# Define our DateWidget and its target model and default view.\n", | |||
Jonathan Frederic
|
r14720 | "class DateWidget(DOMWidget):\n", | |
" _view_name = Unicode('DatePickerView', sync=True)\n", | |||
" value = Unicode(sync=True)\n", | |||
" description = Unicode(sync=True)\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" def __init__(self, **kwargs):\n", | |||
" super(DateWidget, self).__init__(**kwargs)\n", | |||
Jonathan Frederic
|
r14720 | " \n", | |
" # Specify the number of positional arguments supported. For \n", | |||
" # validation we only are worried about one parameter, the\n", | |||
" # new value that should be validated.\n", | |||
" self.validation = CallbackDispatcher(acceptable_nargs=[1])\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" # This function automatically gets called by the traitlet machinery when\n", | |||
" # value is modified because of this function's name.\n", | |||
" def _value_changed(self, name, old_value, new_value):\n", | |||
" \n", | |||
" # Parse the date time value.\n", | |||
" try:\n", | |||
" parsed_date = parser.parse(new_value)\n", | |||
" parsed_date_string = parsed_date.strftime(\"%Y-%m-%d\")\n", | |||
" except:\n", | |||
" parsed_date_string = ''\n", | |||
" \n", | |||
" # Set the parsed date string if the current date string is different.\n", | |||
" if old_value != new_value:\n", | |||
Jonathan Frederic
|
r14720 | " validation = self.validation(parsed_date)\n", | |
" if validation is None or validation == True:\n", | |||
Jonathan Frederic
|
r14340 | " self.value = parsed_date_string\n", | |
" else:\n", | |||
" self.value = old_value\n", | |||
" self.send_state() # The traitlet event won't fire since the value isn't changing.\n", | |||
" # We need to force the back-end to send the front-end the state\n", | |||
Jonathan Frederic
|
r14720 | " # to make sure that the date control date doesn't change." | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 45 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"JavaScript" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"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. \n", | |||
"\n", | |||
"We hide the label if the description value is blank." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"%%javascript\n", | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "\n", | |
"require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
Jonathan Frederic
|
r14720 | " 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", | |||
Jonathan Frederic
|
r14340 | " // Create a label.\n", | |
" this.$label = $('<div />')\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 = $('<input />')\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", | |||
Jonathan Frederic
|
r14720 | " this.$label.text(description);\n", | |
Jonathan Frederic
|
r14340 | " }\n", | |
" \n", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " this.touch();\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"javascript": [ | |||
"\n", | |||
Jonathan Frederic
|
r14720 | "\n", | |
"require([\"notebook/js/widgets/widget\"], function(WidgetManager){\n", | |||
Jonathan Frederic
|
r14340 | " \n", | |
" // Define the DatePickerView\n", | |||
Jonathan Frederic
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | |
Jonathan Frederic
|
r14340 | " render: function(){\n", | |
Jonathan Frederic
|
r14720 | " 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", | |||
Jonathan Frederic
|
r14340 | " // Create a label.\n", | |
" this.$label = $('<div />')\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 = $('<input />')\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", | |||
Jonathan Frederic
|
r14720 | " this.$label.text(description);\n", | |
Jonathan Frederic
|
r14340 | " }\n", | |
" \n", | |||
Jonathan Frederic
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | |
Jonathan Frederic
|
r14340 | " },\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", | |||
Jonathan Frederic
|
r14720 | " this.touch();\n", | |
Jonathan Frederic
|
r14340 | " },\n", | |
" });\n", | |||
" \n", | |||
" // Register the DatePickerView with the widget manager.\n", | |||
Jonathan Frederic
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | |
Jonathan Frederic
|
r14340 | "});" | |
], | |||
"metadata": {}, | |||
"output_type": "display_data", | |||
"text": [ | |||
Jonathan Frederic
|
r14720 | "<IPython.core.display.Javascript at 0x1efe210>" | |
Jonathan Frederic
|
r14340 | ] | |
} | |||
], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 40 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "heading", | |||
"level": 2, | |||
"metadata": {}, | |||
"source": [ | |||
"Test" | |||
] | |||
}, | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
"To test the drawing of the label we create the widget like normal but supply the additional description property a value." | |||
] | |||
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Add some additional widgets for aesthetic purpose\n", | |||
Jonathan Frederic
|
r14720 | "display(widgets.TextBoxWidget(description=\"First:\"))\n", | |
"display(widgets.TextBoxWidget(description=\"Last:\"))\n", | |||
Jonathan Frederic
|
r14340 | "\n", | |
Jonathan Frederic
|
r14720 | "my_widget = DateWidget()\n", | |
"display(my_widget)\n", | |||
"my_widget.description=\"DOB:\"" | |||
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 46 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
Jonathan Frederic
|
r14720 | "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." | |
Jonathan Frederic
|
r14340 | ] | |
}, | |||
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"my_widget = DateWidget()\n", | |||
"display(my_widget)\n", | |||
"\n", | |||
"def validate_date(date):\n", | |||
Jonathan Frederic
|
r14720 | " return not date is None and date.year == 2014\n", | |
"my_widget.validation.register_callback(validate_date)" | |||
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 47 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Try setting a valid date\n", | |||
Jonathan Frederic
|
r14720 | "my_widget.value = \"December 2, 2014\"" | |
Jonathan Frederic
|
r14340 | ], | |
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 48 | |
Jonathan Frederic
|
r14340 | }, | |
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"# Try setting an invalid date\n", | |||
"my_widget.value = \"June 12, 1999\"" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 49 | |
Jason Grout
|
r14505 | }, | |
{ | |||
"cell_type": "code", | |||
"collapsed": false, | |||
"input": [ | |||
"my_widget.value" | |||
], | |||
"language": "python", | |||
"metadata": {}, | |||
"outputs": [ | |||
{ | |||
"metadata": {}, | |||
"output_type": "pyout", | |||
Jonathan Frederic
|
r14720 | "prompt_number": 50, | |
Jason Grout
|
r14505 | "text": [ | |
Jonathan Frederic
|
r14720 | "u'2014-12-02'" | |
Jason Grout
|
r14505 | ] | |
} | |||
], | |||
Jonathan Frederic
|
r14720 | "prompt_number": 50 | |
Jonathan Frederic
|
r14340 | } | |
], | |||
"metadata": {} | |||
} | |||
] | |||
} |