{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# Building a Custom Widget - Hello World" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Backend (Python Kernel)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `value` traitlet is synchronised with the frontend, and will be used to display in the notebook cell a string set by the backend. The other traitlets (`_view_name`, `_view_module` and `_view_module_version`) are used by the Jupyter Widget framework to make your widget work, but you don't have to worry about this for the time being!" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import ipywidgets as widgets\n", "from traitlets import Unicode, validate\n", "\n", "\n", "class HelloWidget(widgets.DOMWidget):\n", " _view_name = Unicode('HelloView').tag(sync=True)\n", " _view_module = Unicode('hello').tag(sync=True)\n", " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", " value = Unicode('Hello World!').tag(sync=True)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Frontend (JavaScript)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Models and views" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The IPython widget framework front end relies heavily on [Backbone.js](http://backbonejs.org/). Backbone.js is an MVC (model view controller) framework. Widgets defined in the back end are automatically synchronized with generic Backbone.js models in the front end. The traitlets are added to the front end instance automatically on first state push. The `_view_name` trait that you defined earlier is used by the widget framework to create the corresponding Backbone.js view and link that view to the model." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Accessing the model from the view" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To access the model associated with a view instance, use the `model` property of the view. `get` and `set` methods are used to interact with the Backbone model. `get` is trivial, however you have to be careful when using `set`. After calling the model `set` you need call the view's `touch` method. This associates the `set` operation with a particular view so output will be routed to the correct cell. The model also has an `on` method, which allows you to listen to events triggered by the model (like value changes)." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Import @jupyter-widgets/base" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You first need to import the `@jupyter-widgets/base` module. To import modules, use the `define` method of [require.js](http://requirejs.org/) (as seen below)." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", "});" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%javascript\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", "});" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Define the view" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, define your widget view class. Inherit from the `DOMWidgetView` by using the `.extend` method." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " // Define the HelloView\n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " });\n", "\n", " return {\n", " HelloView: HelloView\n", " }\n", "});" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%javascript\n", "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " // Define the HelloView\n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " });\n", "\n", " return {\n", " HelloView: HelloView\n", " }\n", "});" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Render method" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lastly, override the base `render` method of the view to define custom rendering logic. A handle to the widget's default DOM element can be acquired via `this.el`. The `el` property is the DOM element associated with the view. Thanks to the call to `model.get`, the view will display the value of the back end upon display. However, it will not update itself to a new value when the value changes." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " render: function() { \n", " this.el.textContent = this.model.get('value'); \n", " },\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%javascript\n", "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " render: function() { \n", " this.el.textContent = this.model.get('value'); \n", " },\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Dynamic updates" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get the view to update itself dynamically, register a function to update the view's value when the model's `value` property changes. This can be done using the `model.on` method. The `on` method takes three parameters, an event name, callback handle, and callback context. The Backbone event named `change` will fire whenever the model changes. By appending `:value` to it, you tell Backbone to only listen to the change event of the `value` property (as seen below)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " render: function() { \n", " this.value_changed();\n", " this.model.on('change:value', this.value_changed, this);\n", " },\n", " \n", " value_changed: function() {\n", " this.el.textContent = this.model.get('value'); \n", " },\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%javascript\n", "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " \n", " render: function() { \n", " this.value_changed();\n", " this.model.on('change:value', this.value_changed, this);\n", " },\n", " \n", " value_changed: function() {\n", " this.el.textContent = this.model.get('value'); \n", " },\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## Test" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "6fb91f2dca854d5c9472a1e6bb48453a", "version_major": 2, "version_minor": 0 }, "text/html": [ "

Failed to display Jupyter Widget of type HelloWidget.

\n", "

\n", " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", " that the widgets JavaScript is still loading. If this message persists, it\n", " likely means that the widgets JavaScript library is either not installed or\n", " not enabled. See the Jupyter\n", " Widgets Documentation for setup instructions.\n", "

\n", "

\n", " If you're reading this message in another frontend (for example, a static\n", " rendering on GitHub or NBViewer),\n", " it may mean that your frontend doesn't currently support widgets.\n", "

\n" ], "text/plain": [ "HelloWidget()" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "w = HelloWidget()\n", "w" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "w.value = 'my value'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Where are we standing now ?\n", "\n", "The example above dumps the value directly into the DOM. There is no way for you to interact with this dumped data in the front end. To create an example that accepts input, you will have to do something more than blindly dumping the contents of value into the DOM. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding input from the JS side" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the widget consists in a form element, and a title. Both display the `value` string set in the backend. But now you can change the string from the form, and it'll update both the title and the backend value. The synchronisation from the JS frontend to the Python backend is done using an `EventListener` which listens to changes in the form element, and triggers a callback. The callback changes the model value, and syncs with the backend." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " callback:function(inputEvent, formElement){\n", " this.model.set({'value':formElement[0].value}) // update the JS model with the current view value\n", " this.touch() // sync the JS model with the Python backend\n", " },\n", "\n", " render: function() {\n", " this.model.on('change:value', this.value_changed, this);\n", "\n", " let view = this;\n", " \n", " // standard HTML DOM change from JS\n", " let f = document.createElement(\"form\");\n", " let i = document.createElement(\"input\"); // input element, text \n", " i.setAttribute('type',\"text\"); \n", " f.appendChild(i);\n", " this.el.appendChild(f);\n", " let title = document.createElement(\"h3\"); \n", " this.el.appendChild(title);\n", " \n", " // initializing the form and the title values \n", " i.setAttribute('value', this.model.get('value'));\n", " title.textContent = this.model.get('value');\n", " \n", " // Listening to changes in the frontend input\n", " f.addEventListener(\"input\", (inputEvent => view.callback(inputEvent, f)), false);\n", "\n", " // handle to access the DOM elements directly\n", " this.input = i; \n", " this.title = title;\n", " },\n", "\n", "\n", "\n", " value_changed: function() {\n", " // access to the 'input' DOM element\n", " this.input.setAttribute('value', this.model.get('value'))\n", " // access to the 'h3' DOM element\n", " this.title.textContent = this.model.get('value')\n", "\n", " }\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%javascript\n", "require.undef('hello');\n", "\n", "define('hello', [\"@jupyter-widgets/base\"], function(widgets) {\n", " \n", " var HelloView = widgets.DOMWidgetView.extend({\n", " callback:function(inputEvent, formElement){\n", " this.model.set({'value':formElement[0].value}) // update the JS model with the current view value\n", " this.touch() // sync the JS model with the Python backend\n", " },\n", "\n", " render: function() {\n", " this.model.on('change:value', this.value_changed, this);\n", "\n", " let view = this;\n", " \n", " // standard HTML DOM change from JS\n", " let f = document.createElement(\"form\");\n", " let i = document.createElement(\"input\"); // input element, text \n", " i.setAttribute('type',\"text\"); \n", " f.appendChild(i);\n", " this.el.appendChild(f);\n", " let title = document.createElement(\"h3\"); \n", " this.el.appendChild(title);\n", " \n", " // initializing the form and the title values \n", " i.setAttribute('value', this.model.get('value'));\n", " title.textContent = this.model.get('value');\n", " \n", " // Listening to changes in the frontend input\n", " f.addEventListener(\"input\", (inputEvent => view.callback(inputEvent, f)), false);\n", "\n", " // handle to access the DOM elements directly\n", " this.input = i; \n", " this.title = title;\n", " },\n", "\n", "\n", "\n", " value_changed: function() {\n", " // access to the 'input' DOM element\n", " this.input.setAttribute('value', this.model.get('value'))\n", " // access to the 'h3' DOM element\n", " this.title.textContent = this.model.get('value')\n", "\n", " }\n", " });\n", " \n", " return {\n", " HelloView : HelloView\n", " };\n", "});" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "1eff5802729d4a8d828fcfbf91cc1d57", "version_major": 2, "version_minor": 0 }, "text/html": [ "

Failed to display Jupyter Widget of type HelloWidget.

\n", "

\n", " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", " that the widgets JavaScript is still loading. If this message persists, it\n", " likely means that the widgets JavaScript library is either not installed or\n", " not enabled. See the Jupyter\n", " Widgets Documentation for setup instructions.\n", "

\n", "

\n", " If you're reading this message in another frontend (for example, a static\n", " rendering on GitHub or NBViewer),\n", " it may mean that your frontend doesn't currently support widgets.\n", "

\n" ], "text/plain": [ "HelloWidget()" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "w2 = HelloWidget()\n", "w2\n", "# you can change the value in the form below, and it will be automatically updated in the Python kernel" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'Hello World!'" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "w2.value" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": true }, "outputs": [], "source": [ "w2.value = 'Sync from Python to JS'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { "1eff5802729d4a8d828fcfbf91cc1d57": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.0.0", "model_name": "DOMWidgetModel", "state": { "_model_name": "DOMWidgetModel", "_view_module": "hello", "_view_module_version": "0.1.0", "_view_name": "HelloView", "layout": "IPY_MODEL_6e5b0aade176420994bf2f99e0110eac", "value": "Sync from Python to JS" } }, "30e9ca2faa0f4a58aff5a507c5252696": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.0.0", "model_name": "LayoutModel", "state": {} }, "6e5b0aade176420994bf2f99e0110eac": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.0.0", "model_name": "LayoutModel", "state": {} }, "6fb91f2dca854d5c9472a1e6bb48453a": { "model_module": "@jupyter-widgets/base", "model_module_version": "1.0.0", "model_name": "DOMWidgetModel", "state": { "_model_name": "DOMWidgetModel", "_view_module": "hello", "_view_module_version": "0.1.0", "_view_name": "HelloView", "layout": "IPY_MODEL_30e9ca2faa0f4a58aff5a507c5252696", "value": "my value" } } }, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 1 }