Part 6 - Custom Widget.ipynb
1058 lines
| 33.9 KiB
| text/plain
|
TextLexer
|
r14340 | { | ||
"metadata": { | ||||
"cell_tags": [ | ||||
[ | ||||
"<None>", | ||||
null | ||||
] | ||||
], | ||||
"name": "" | ||||
}, | ||||
"nbformat": 3, | ||||
"nbformat_minor": 0, | ||||
"worksheets": [ | ||||
{ | ||||
"cells": [ | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14724 | "[< Back to Part 5](Part 5 - Alignment.ipynb) or [Index](index.ipynb)\n", | ||
"\n", | ||||
|
r14798 | "Before reading, make sure to review\n", | ||
|
r14340 | "\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": [ | ||||
|
r14413 | "from __future__ import print_function # For py 2.7 compat\n", | ||
"\n", | ||||
|
r14798 | "from IPython.html import widgets # Widget definitions\n", | ||
"from IPython.display import display # Used to display widgets in the notebook\n", | ||||
"from IPython.utils.traitlets import Unicode # Used to declare attributes of our widget" | ||||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
|
r14342 | "outputs": [], | ||
|
r14724 | "prompt_number": 1 | ||
|
r14413 | }, | ||
{ | ||||
|
r14340 | "cell_type": "heading", | ||
"level": 1, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Abstract" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "This notebook implements a custom date picker widget,\n", | ||
"in order to demonstrate the widget creation process.\n", | ||||
"\n", | ||||
"To create a custom widget, both Python and JavaScript code is required." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "heading", | ||||
"level": 1, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Section 1 - Basics" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Python" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "When starting a project like this, it is often easiest to make a simple base implementation,\n", | ||
"to verify that the underlying framework is working as expected.\n", | ||||
"To start, we will create an empty widget and make sure that it can be rendered.\n", | ||||
"The first step is to define the widget in Python." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 2 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "Our widget inherits from `widgets.DOMWidget` since it is intended that it will be displayed in the notebook directly.\n", | ||
"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.\n", | ||||
"**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." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"JavaScript" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "In the IPython notebook [require.js](http://requirejs.org/) is used to load JavaScript dependencies.\n", | ||
|
r15745 | "All IPython widget code depends on `widgets/js/widget.js`,\n", | ||
|
r14798 | "where the base widget model and base view are defined.\n", | ||
"We use require.js to load this file:" | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"%%javascript\n", | ||||
"\n", | ||||
|
r15745 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | "\n", | ||
"});" | ||||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | "\n", | ||
"});" | ||||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x109491690>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 3 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "Now we need to define a view that can be used to represent the model.\n", | ||
"To do this, the `IPython.DOMWidgetView` is extended.\n", | ||||
"**A render function must be defined**.\n", | ||||
"The render function is used to render a widget view instance to the DOM.\n", | ||||
"For now, the render function renders a div that contains the text *Hello World!*\n", | ||||
"Lastly, the view needs to be registered with the widget manager.\n", | ||||
|
r14340 | "\n", | ||
"**Final JavaScript code below:**" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"%%javascript\n", | ||||
"\n", | ||||
|
r15745 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
" render: function(){ this.$el.text('Hello World!'); },\n", | ||||
|
r14340 | " });\n", | ||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
" render: function(){ this.$el.text('Hello World!'); },\n", | ||||
|
r14340 | " });\n", | ||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x1094917d0>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 4 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Test" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "To test what we have so far, create the widget, just like you would the builtin widgets:" | ||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14720 | "DateWidget()" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 5 | ||
|
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": [ | ||||
|
r14798 | "In the last section we created a simple widget that displayed *Hello World!*\n", | ||
"To make an actual date widget, we need to add a property that will be synced between the Python model and the JavaScript model.\n", | ||||
"The new attribute must be a traitlet, so the widget machinery can handle it.\n", | ||||
"The traitlet must be constructed with a `sync=True` keyword argument, to tell the widget machinery knows to synchronize it with the front-end.\n", | ||||
"Adding this to the code from the last section:" | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)\n", | ||
" value = Unicode(sync=True)" | ||||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 6 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"JavaScript" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "In the JavaScript, there is no need to define counterparts to the traitlets.\n", | ||
"When the JavaScript model is created for the first time,\n", | ||||
"it copies all of the traitlet `sync=True` attributes from the Python model.\n", | ||||
"We need to replace *Hello World!* with an actual HTML date picker widget." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"%%javascript\n", | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
" \n", | ||||
" // Create the date picker control.\n", | ||||
" this.$date = $('<input />')\n", | ||||
|
r14720 | " .attr('type', 'date')\n", | ||
" .appendTo(this.$el);\n", | ||||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
" \n", | ||||
" // Create the date picker control.\n", | ||||
" this.$date = $('<input />')\n", | ||||
|
r14720 | " .attr('type', 'date')\n", | ||
" .appendTo(this.$el);\n", | ||||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x109491750>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 7 | ||
|
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", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
" \n", | ||||
" // Create the date picker control.\n", | ||||
" this.$date = $('<input />')\n", | ||||
|
r14720 | " .attr('type', 'date')\n", | ||
" .appendTo(this.$el);\n", | ||||
|
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", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
" \n", | ||||
" // Create the date picker control.\n", | ||||
" this.$date = $('<input />')\n", | ||||
|
r14720 | " .attr('type', 'date')\n", | ||
" .appendTo(this.$el);\n", | ||||
|
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", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x109491750>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 8 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "To get the changed value from the frontend to publish itself to the backend,\n", | ||
"we need to listen to the change event triggered by the HTM date control and set the value in the model.\n", | ||||
"After the date change event fires and the new value is set in the model,\n", | ||||
"it is very important that we call `this.touch()` to let the widget machinery know which view changed the model.\n", | ||||
"This is important because the widget machinery needs to know which cell to route the message callbacks to.\n", | ||||
|
r14340 | "\n", | ||
"**Final JavaScript code below:**" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"%%javascript\n", | ||||
"\n", | ||||
|
r14720 | "\n", | ||
|
r15745 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
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", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
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", | ||||
|
r14720 | " this.touch();\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r14720 | "\n", | ||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
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", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
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", | ||||
|
r14720 | " this.touch();\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x109491b10>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 9 | ||
|
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": [], | ||||
|
r14724 | "prompt_number": 10 | ||
|
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": [ | ||||
|
r14720 | "my_widget" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 11 | ||
|
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", | ||||
|
r14724 | "prompt_number": 12, | ||
|
r14340 | "text": [ | ||
|
r14798 | "u''" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 12 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Set the date from Python" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14798 | "my_widget.value = \"1998-12-01\" # December 1st, 1998" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 13 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "heading", | ||||
"level": 1, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Section 3 - Extra credit" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14724 | "The 3rd party `dateutil` library is required to continue. 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": [], | ||||
"prompt_number": 14 | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "In the last section we created a fully working date picker widget.\n", | ||
"Now we will add custom validation and support for labels.\n", | ||||
"So far, only the ISO date format \"YYYY-MM-DD\" is supported.\n", | ||||
"Now, we will add support for all of the date formats recognized by the Python dateutil library." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"Python" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "The standard property name used for widget labels is `description`.\n", | ||
"In the code block below, `description` has been added to the Python widget." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)\n", | ||
" value = Unicode(sync=True)\n", | ||||
|
r14724 | " description = Unicode(sync=True)" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 15 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "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.\n", | ||
"We can take advantage of this to perform validation and parsing of different date string formats.\n", | ||||
"Below, a method that listens to value has been added to the DateWidget." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)\n", | ||
" value = Unicode(sync=True)\n", | ||||
|
r14724 | " description = Unicode(sync=True)\n", | ||
"\n", | ||||
|
r14340 | " # 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", | ||||
|
r14724 | " pass" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 16 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "Now the function parses the date string,\n", | ||
"and only sets the value in the correct format." | ||||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)\n", | ||
" value = Unicode(sync=True)\n", | ||||
" description = Unicode(sync=True)\n", | ||||
|
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": [], | ||||
|
r14724 | "prompt_number": 17 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "Finally, a `CallbackDispatcher` is added so the user can perform custom validation.\n", | ||
"If any one of the callbacks registered with the dispatcher returns False,\n", | ||||
"the new date is not set.\n", | ||||
|
r14340 | "\n", | ||
"**Final Python code below:**" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
|
r14724 | "class DateWidget(widgets.DOMWidget):\n", | ||
|
r14720 | " _view_name = Unicode('DatePickerView', sync=True)\n", | ||
" value = Unicode(sync=True)\n", | ||||
" description = Unicode(sync=True)\n", | ||||
|
r14340 | " \n", | ||
" def __init__(self, **kwargs):\n", | ||||
" super(DateWidget, self).__init__(**kwargs)\n", | ||||
|
r14720 | " \n", | ||
|
r14798 | " self.validate = widgets.CallbackDispatcher()\n", | ||
|
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", | ||||
|
r14798 | " valid = self.validate(parsed_date)\n", | ||
" if valid in (None, True):\n", | ||||
|
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", | ||||
|
r14720 | " # to make sure that the date control date doesn't change." | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 18 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "heading", | ||||
"level": 2, | ||||
"metadata": {}, | ||||
"source": [ | ||||
"JavaScript" | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
r14798 | "Using the Javascript code from the last section,\n", | ||
"we add a label to the date time object.\n", | ||||
"The label is a div with the `widget-hlabel` class applied to it.\n", | ||||
"`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.\n", | ||||
"Similar to the `widget-hlabel` class is the `widget-hbox-single` class.\n", | ||||
"The `widget-hbox-single` class applies special styling to widget containers that store a single line horizontal widget.\n", | ||||
|
r14340 | "\n", | ||
"We hide the label if the description value is blank." | ||||
] | ||||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"%%javascript\n", | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
|
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", | ||||
|
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", | ||||
|
r14720 | " this.$label.text(description);\n", | ||
|
r14340 | " }\n", | ||
" \n", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
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", | ||||
|
r14720 | " this.touch();\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"javascript": [ | ||||
"\n", | ||||
|
r15742 | "require([\"widgets/js/widget\"], function(WidgetManager){\n", | ||
|
r14340 | " \n", | ||
" // Define the DatePickerView\n", | ||||
|
r14720 | " var DatePickerView = IPython.DOMWidgetView.extend({\n", | ||
|
r14340 | " render: function(){\n", | ||
|
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", | ||||
|
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", | ||||
|
r14720 | " this.$label.text(description);\n", | ||
|
r14340 | " }\n", | ||
" \n", | ||||
|
r14720 | " return DatePickerView.__super__.update.apply(this);\n", | ||
|
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", | ||||
|
r14720 | " this.touch();\n", | ||
|
r14340 | " },\n", | ||
" });\n", | ||||
" \n", | ||||
" // Register the DatePickerView with the widget manager.\n", | ||||
|
r14720 | " WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n", | ||
|
r14340 | "});" | ||
], | ||||
"metadata": {}, | ||||
"output_type": "display_data", | ||||
"text": [ | ||||
|
r14798 | "<IPython.core.display.Javascript at 0x1094eef90>" | ||
|
r14340 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 19 | ||
|
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", | ||||
|
r14834 | "display(widgets.TextWidget(description=\"First:\"))\n", | ||
"display(widgets.TextWidget(description=\"Last:\"))\n", | ||||
|
r14340 | "\n", | ||
|
r14720 | "my_widget = DateWidget()\n", | ||
"display(my_widget)\n", | ||||
"my_widget.description=\"DOB:\"" | ||||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 20 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
|
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." | ||
|
r14340 | ] | ||
}, | ||||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"my_widget = DateWidget()\n", | ||||
"display(my_widget)\n", | ||||
"\n", | ||||
|
r14798 | "def require_2014(date):\n", | ||
|
r14720 | " return not date is None and date.year == 2014\n", | ||
|
r14798 | "my_widget.validate.register_callback(require_2014)" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 21 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"# Try setting a valid date\n", | ||||
|
r14720 | "my_widget.value = \"December 2, 2014\"" | ||
|
r14340 | ], | ||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 22 | ||
|
r14340 | }, | ||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"# Try setting an invalid date\n", | ||||
"my_widget.value = \"June 12, 1999\"" | ||||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [], | ||||
|
r14724 | "prompt_number": 23 | ||
|
r14505 | }, | ||
{ | ||||
"cell_type": "code", | ||||
"collapsed": false, | ||||
"input": [ | ||||
"my_widget.value" | ||||
], | ||||
"language": "python", | ||||
"metadata": {}, | ||||
"outputs": [ | ||||
{ | ||||
"metadata": {}, | ||||
"output_type": "pyout", | ||||
|
r14724 | "prompt_number": 24, | ||
|
r14505 | "text": [ | ||
|
r14720 | "u'2014-12-02'" | ||
|
r14505 | ] | ||
} | ||||
], | ||||
|
r14724 | "prompt_number": 24 | ||
}, | ||||
{ | ||||
"cell_type": "markdown", | ||||
"metadata": {}, | ||||
"source": [ | ||||
"This concludes Part 6 of the [series](index.ipynb)." | ||||
] | ||||
|
r14340 | } | ||
], | ||||
"metadata": {} | ||||
} | ||||
] | ||||
|
r15742 | } | ||