Custom Widgets.ipynb
1048 lines
| 33.7 KiB
| text/plain
|
TextLexer
|
r14340 | { | |
"metadata": { | |||
"cell_tags": [ | |||
[ | |||
"<None>", | |||
null | |||
] | |||
], | |||
"name": "" | |||
}, | |||
"nbformat": 3, | |||
"nbformat_minor": 0, | |||
"worksheets": [ | |||
{ | |||
"cells": [ | |||
{ | |||
"cell_type": "markdown", | |||
"metadata": {}, | |||
"source": [ | |||
|
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 | |
|
r14340 | } | |
], | |||
"metadata": {} | |||
} | |||
] | |||
|
r16098 | } |