You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1049 lines
34 KiB
1049 lines
34 KiB
{
|
|
"metadata": {
|
|
"cell_tags": [
|
|
[
|
|
"<None>",
|
|
null
|
|
]
|
|
],
|
|
"name": ""
|
|
},
|
|
"nbformat": 3,
|
|
"nbformat_minor": 0,
|
|
"worksheets": [
|
|
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Before reading, make sure 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": [
|
|
"from __future__ import print_function # For py 2.7 compat\n",
|
|
"\n",
|
|
"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"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 1
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 1,
|
|
"metadata": {},
|
|
"source": [
|
|
"Abstract"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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."
|
|
]
|
|
},
|
|
{
|
|
"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 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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 2
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"JavaScript"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"In the IPython notebook [require.js](http://requirejs.org/) is used to load JavaScript dependencies.\n",
|
|
"All IPython widget code depends on `widgets/js/widget.js`,\n",
|
|
"where the base widget model and base view are defined.\n",
|
|
"We use require.js to load this file:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"%%javascript\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
"\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
"\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x109491690>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 3
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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",
|
|
"\n",
|
|
"**Final JavaScript code below:**"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"%%javascript\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){ this.$el.text('Hello World!'); },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){ this.$el.text('Hello World!'); },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x1094917d0>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 4
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"Test"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"To test what we have so far, create the widget, just like you would the builtin widgets:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"DateWidget()"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 5
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 1,
|
|
"metadata": {},
|
|
"source": [
|
|
"Section 2 - Something useful"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"Python"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)\n",
|
|
" value = Unicode(sync=True)"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 6
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"JavaScript"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"%%javascript\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){\n",
|
|
" \n",
|
|
" // Create the date picker control.\n",
|
|
" this.$date = $('<input />')\n",
|
|
" .attr('type', 'date')\n",
|
|
" .appendTo(this.$el);\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){\n",
|
|
" \n",
|
|
" // Create the date picker control.\n",
|
|
" this.$date = $('<input />')\n",
|
|
" .attr('type', 'date')\n",
|
|
" .appendTo(this.$el);\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x109491750>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 7
|
|
},
|
|
{
|
|
"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",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" 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",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" 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",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x109491750>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 8
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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",
|
|
"\n",
|
|
"**Final JavaScript code below:**"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"%%javascript\n",
|
|
"\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" 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",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" \n",
|
|
" // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n",
|
|
" events: {\"change\": \"handle_date_change\"},\n",
|
|
" \n",
|
|
" // Callback for when the date is changed.\n",
|
|
" handle_date_change: function(event) {\n",
|
|
" this.model.set('value', this.$date.val());\n",
|
|
" this.touch();\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" 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",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" \n",
|
|
" // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n",
|
|
" events: {\"change\": \"handle_date_change\"},\n",
|
|
" \n",
|
|
" // Callback for when the date is changed.\n",
|
|
" handle_date_change: function(event) {\n",
|
|
" this.model.set('value', this.$date.val());\n",
|
|
" this.touch();\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x109491b10>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 9
|
|
},
|
|
{
|
|
"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": [],
|
|
"prompt_number": 10
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Display the widget again to make sure that both views remain in sync."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"my_widget"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 11
|
|
},
|
|
{
|
|
"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",
|
|
"prompt_number": 12,
|
|
"text": [
|
|
"u''"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 12
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Set the date from Python"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"my_widget.value = \"1998-12-01\" # December 1st, 1998"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 13
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 1,
|
|
"metadata": {},
|
|
"source": [
|
|
"Section 3 - Extra credit"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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": [
|
|
"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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"Python"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"The standard property name used for widget labels is `description`.\n",
|
|
"In the code block below, `description` has been added to the Python widget."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)\n",
|
|
" value = Unicode(sync=True)\n",
|
|
" description = Unicode(sync=True)"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 15
|
|
},
|
|
{
|
|
"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.\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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)\n",
|
|
" value = Unicode(sync=True)\n",
|
|
" description = Unicode(sync=True)\n",
|
|
"\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"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 16
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now the function parses the date string,\n",
|
|
"and only sets the value in the correct format."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)\n",
|
|
" value = Unicode(sync=True)\n",
|
|
" description = Unicode(sync=True)\n",
|
|
" \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": [],
|
|
"prompt_number": 17
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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",
|
|
"\n",
|
|
"**Final Python code below:**"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"class DateWidget(widgets.DOMWidget):\n",
|
|
" _view_name = Unicode('DatePickerView', sync=True)\n",
|
|
" value = Unicode(sync=True)\n",
|
|
" description = Unicode(sync=True)\n",
|
|
" \n",
|
|
" def __init__(self, **kwargs):\n",
|
|
" super(DateWidget, self).__init__(**kwargs)\n",
|
|
" \n",
|
|
" self.validate = widgets.CallbackDispatcher()\n",
|
|
" \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",
|
|
" valid = self.validate(parsed_date)\n",
|
|
" if valid in (None, True):\n",
|
|
" 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",
|
|
" # to make sure that the date control date doesn't change."
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 18
|
|
},
|
|
{
|
|
"cell_type": "heading",
|
|
"level": 2,
|
|
"metadata": {},
|
|
"source": [
|
|
"JavaScript"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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",
|
|
"\n",
|
|
"We hide the label if the description value is blank."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"%%javascript\n",
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){\n",
|
|
" this.$el.addClass('widget-hbox-single'); /* Apply this class to the widget container to make\n",
|
|
" it fit with the other built in widgets.*/\n",
|
|
" // Create a label.\n",
|
|
" this.$label = $('<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",
|
|
" this.$label.text(description);\n",
|
|
" }\n",
|
|
" \n",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" \n",
|
|
" // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n",
|
|
" events: {\"change\": \"handle_date_change\"},\n",
|
|
" \n",
|
|
" // Callback for when the date is changed.\n",
|
|
" handle_date_change: function(event) {\n",
|
|
" this.model.set('value', this.$date.val());\n",
|
|
" this.touch();\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"javascript": [
|
|
"\n",
|
|
"require([\"widgets/js/widget\"], function(WidgetManager){\n",
|
|
" \n",
|
|
" // Define the DatePickerView\n",
|
|
" var DatePickerView = IPython.DOMWidgetView.extend({\n",
|
|
" render: function(){\n",
|
|
" this.$el.addClass('widget-hbox-single'); /* Apply this class to the widget container to make\n",
|
|
" it fit with the other built in widgets.*/\n",
|
|
" // Create a label.\n",
|
|
" this.$label = $('<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",
|
|
" this.$label.text(description);\n",
|
|
" }\n",
|
|
" \n",
|
|
" return DatePickerView.__super__.update.apply(this);\n",
|
|
" },\n",
|
|
" \n",
|
|
" // Tell Backbone to listen to the change event of input controls (which the HTML date picker is)\n",
|
|
" events: {\"change\": \"handle_date_change\"},\n",
|
|
" \n",
|
|
" // Callback for when the date is changed.\n",
|
|
" handle_date_change: function(event) {\n",
|
|
" this.model.set('value', this.$date.val());\n",
|
|
" this.touch();\n",
|
|
" },\n",
|
|
" });\n",
|
|
" \n",
|
|
" // Register the DatePickerView with the widget manager.\n",
|
|
" WidgetManager.register_widget_view('DatePickerView', DatePickerView);\n",
|
|
"});"
|
|
],
|
|
"metadata": {},
|
|
"output_type": "display_data",
|
|
"text": [
|
|
"<IPython.core.display.Javascript at 0x1094eef90>"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 19
|
|
},
|
|
{
|
|
"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",
|
|
"display(widgets.TextWidget(description=\"First:\"))\n",
|
|
"display(widgets.TextWidget(description=\"Last:\"))\n",
|
|
"\n",
|
|
"my_widget = DateWidget()\n",
|
|
"display(my_widget)\n",
|
|
"my_widget.description=\"DOB:\""
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 20
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"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."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"my_widget = DateWidget()\n",
|
|
"display(my_widget)\n",
|
|
"\n",
|
|
"def require_2014(date):\n",
|
|
" return not date is None and date.year == 2014\n",
|
|
"my_widget.validate.register_callback(require_2014)"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 21
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"# Try setting a valid date\n",
|
|
"my_widget.value = \"December 2, 2014\""
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 22
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"# Try setting an invalid date\n",
|
|
"my_widget.value = \"June 12, 1999\""
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"prompt_number": 23
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"collapsed": false,
|
|
"input": [
|
|
"my_widget.value"
|
|
],
|
|
"language": "python",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"metadata": {},
|
|
"output_type": "pyout",
|
|
"prompt_number": 24,
|
|
"text": [
|
|
"u'2014-12-02'"
|
|
]
|
|
}
|
|
],
|
|
"prompt_number": 24
|
|
}
|
|
],
|
|
"metadata": {}
|
|
}
|
|
]
|
|
} |