From 85b424d9610826d914156bd63fb53aa6ebc759d0 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Mon, 13 Jul 2020 16:52:54 +0200 Subject: [PATCH 01/16] Bootstrap the custom widget tutorial --- docs/source/examples/Index.ipynb | 1 + docs/source/examples/Widget Custom.ipynb | 795 +++++++++++++++++++++++ 2 files changed, 796 insertions(+) create mode 100644 docs/source/examples/Widget Custom.ipynb diff --git a/docs/source/examples/Index.ipynb b/docs/source/examples/Index.ipynb index b4ef5efb2b..cd139aa680 100644 --- a/docs/source/examples/Index.ipynb +++ b/docs/source/examples/Index.ipynb @@ -38,6 +38,7 @@ "- [Widget List](Widget%20List.ipynb) \n", "- [Widget Styling](Widget%20Styling.ipynb)\n", "- [Layout Templates](Layout%20Templates.ipynb)\n", + "- [Custom Widget Tutorial](Widget%20Custom.ipynb)\n", "- [Widget Asynchronous](Widget%20Asynchronous.ipynb): how to pause and listen in the kernel for widget changes in the frontend." ] }, diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb new file mode 100644 index 0000000000..f294c52d9c --- /dev/null +++ b/docs/source/examples/Widget Custom.ipynb @@ -0,0 +1,795 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "[Index](Index.ipynb) - [Back](Widget Styling.ipynb) - [Next](Widget Asynchronous.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Building a Custom Widget - Email widget" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The widget framework is built on top of the Comm framework (short for communication). The Comm framework is a framework that allows the kernel to send/receive JSON messages to/from the front end (as seen below).\n", + "\n", + "![Widget layer](images/WidgetArch.png)\n", + "\n", + "To create a custom widget, you need to define the widget both in the browser and in the Python kernel.\n", + "\n", + "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup a dev environment\n", + "\n", + "### Install conda with miniconda\n", + "\n", + "We recommend installing `conda` using `miniconda`.\n", + "\n", + "Instructions are available on the [conda installation documentation](https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html).\n", + "\n", + "### Create a new conda environment with the dependencies\n", + "\n", + "Next create a conda environment that includes:\n", + "\n", + "1. the latest release of JupyterLab or the classic notebook\n", + "2. [cookiecutter](https://github.com/cookiecutter/cookiecutter), the tool you will use to bootstrap the custom widget\n", + "3. [NodeJS](https://nodejs.org): the JavaScript runtime you'll use to\n", + " compile the web assets (e.g., TypeScript, CSS) for the custom widget\n", + "\n", + "To create the environment, execute the following command:\n", + "\n", + "```bash\n", + "conda create -n ipyemail -c conda-forge jupyterlab cookiecutter nodejs python\n", + "```\n", + "\n", + "Then activate the environment with:\n", + "\n", + "```bash\n", + "conda activate ipyemail\n", + "```\n", + "\n", + "## Create a new project\n", + "\n", + "### Initialize the project from a cookiecutter\n", + "\n", + "It is usually recommended to bootstrap the widget with `cookiecutter`.\n", + "\n", + "Two cookiecutter projects are currently available:\n", + "\n", + "- [widget-ts-cookiecutter](https://github.com/jupyter-widgets/widget-ts-cookiecutter): To create a custom widget in TypeScript\n", + "- [widget-cookiecutter](https://github.com/jupyter-widgets/widget-cookiecutter): To create a custom widget in JavaScript\n", + "\n", + "In this tutorial, we are going to use the TypeScript cookiecutter, as many of the existing widgets are written in TypeScript.\n", + "\n", + "To generate the project, run the following command:\n", + "\n", + "```bash\n", + "cookiecutter https://github.com/jupyter-widgets/widget-ts-cookiecutter\n", + "```\n", + "\n", + "When prompted, enter the desired values like the following for all of the cookiecutter prompts:\n", + "\n", + "```bash\n", + "author_name []:\n", + "author_email []:\n", + "github_project_name []: ipyemail\n", + "github_organization_name []:\n", + "python_package_name [ipyemail]:\n", + "npm_package_name [ipyemail]:\n", + "npm_package_version [0.1.0]:\n", + "project_short_description [A Custom Jupyter Widget Library]:\n", + "```\n", + "\n", + "Change to the directory the cookiecutter created and list the files.\n", + "\n", + "```bash\n", + "cd ipyemail\n", + "ls\n", + "```\n", + "\n", + "You should see a list like the following.\n", + "\n", + "```bash\n", + "appveyor.yml css examples ipyemail.json MANIFEST.in pytest.ini readthedocs.yml setup.cfg src tsconfig.json\n", + "codecov.yml docs ipyemail LICENSE.txt package.json README.md setupbase.py setup.py tests webpack.config.js\n", + "```\n", + "\n", + "### Build and install the widget for development\n", + "\n", + "The generated project should already contain a `README.md` file with the instructions to develop the widget locally.\n", + "\n", + "Since the widget contains a Python part, you need to install the package in editable mode:\n", + "\n", + "```bash\n", + "python -m pip install -e .\n", + "```\n", + "\n", + "You also need to enable the widget frontend extension.\n", + "\n", + "If you are using JupyterLab:\n", + "\n", + "```bash\n", + "# install the widget manager to display Widgets in the JupyterLab interface\n", + "jupyter labextension install @jupyter-widgets/jupyterlab-manager --no-build\n", + "\n", + "# install the local extension\n", + "jupyter labextension install .\n", + "```\n", + "\n", + "If you are using the classic notebook:\n", + "\n", + "```bash\n", + "jupyter nbextension install --sys-prefix --symlink --overwrite --py ipyemail\n", + "jupyter nbextension enable --sys-prefix --py ipyemail\n", + "```\n", + "\n", + "You are now ready to implement the core functionality of the widget!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Building a Custom Widget" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Python Kernel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### DOMWidget, ValueWidget and Widget\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To define a widget, you must inherit from the `DOMWidget`, `ValueWidget`, or `Widget` base class. If you intend for your widget to be displayed, you'll want to inherit from `DOMWidget`. If you intend for your widget to be used as an input for [interact](./Using%20Interact.ipynb), you'll want to inherit from `ValueWidget`. Your widget should inherit from `ValueWidget` if it has a single ovious output (for example, the output of an `IntSlider` is clearly the current value of the slider).\n", + "\n", + "Both the `DOMWidget` and `ValueWidget` classes inherit from the `Widget` class. The `Widget` class is useful for cases in which the widget is not meant to be displayed directly in the notebook, but instead as a child of another rendering environment. Here are some examples:\n", + "\n", + "- If you wanted to create a [three.js](https://threejs.org/) widget (three.js is a popular WebGL library), you would implement the rendering window as a `DOMWidget` and any 3D objects or lights meant to be rendered in that window as `Widget`\n", + "- If you wanted to create a widget that displays directly in the notebook for usage with `interact` (like `IntSlider`), you should multiple inherit from both `DOMWidget` and `ValueWidget`. \n", + "- If you wanted to create a widget that provides a value to `interact` but is controlled and viewed by another widget or an external source, you should inherit from only `ValueWidget`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### _view_name" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Inheriting from the DOMWidget does not tell the widget framework what front end widget to associate with your back end widget.\n", + "\n", + "Instead, you must tell it yourself by defining specially named trait attributes, `_view_name`, `_view_module`, and `_view_module_version` (as seen below) and optionally `_model_name` and `_model_module`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from traitlets import Unicode, Bool, validate, TraitError\n", + "from ipywidgets import DOMWidget, ValueWidget, register\n", + "\n", + "\n", + "@register\n", + "class Email(DOMWidget, ValueWidget):\n", + " _view_name = Unicode('EmailView').tag(sync=True)\n", + " _view_module = Unicode('email_widget').tag(sync=True)\n", + " _view_module_version = Unicode('0.1.0').tag(sync=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### sync=True traitlets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Traitlets is an IPython library for defining type-safe properties on configurable objects. For this tutorial you do not need to worry about the *configurable* piece of the traitlets machinery. The `sync=True` keyword argument tells the widget framework to handle synchronizing that value to the browser. Without `sync=True`, attributes of the widget won't be synchronized with the front-end." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Other traitlet types" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unicode, used for `_view_name`, is not the only Traitlet type, there are many more some of which are listed below: \n", + "\n", + "- Any\n", + "- Bool\n", + "- Bytes\n", + "- CBool\n", + "- CBytes\n", + "- CComplex\n", + "- CFloat\n", + "- CInt\n", + "- CLong\n", + "- CRegExp\n", + "- CUnicode\n", + "- CaselessStrEnum\n", + "- Complex\n", + "- Dict\n", + "- DottedObjectName\n", + "- Enum\n", + "- Float\n", + "- FunctionType\n", + "- Instance\n", + "- InstanceType\n", + "- Int\n", + "- List\n", + "- Long\n", + "- Set\n", + "- TCPAddress\n", + "- Tuple\n", + "- Type\n", + "- Unicode\n", + "- Union\n", + "\n", + "\n", + "Not all of these traitlets can be synchronized across the network, only the JSON-able traits and Widget instances will be synchronized." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Front end (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": [ + "### 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": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "define('email_widget', [\"@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": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "require.undef('email_widget');\n", + "\n", + "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + " \n", + " // Define the EmailView\n", + " var EmailView = widgets.DOMWidgetView.extend({\n", + " \n", + " });\n", + "\n", + " return {\n", + " EmailView: EmailView\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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "require.undef('email_widget');\n", + "\n", + "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + "\n", + " var EmailView = widgets.DOMWidgetView.extend({\n", + "\n", + " // Render the view.\n", + " render: function() { \n", + " this.email_input = document.createElement('input');\n", + " this.email_input.type = 'email';\n", + " this.email_input.value = 'example@example.com';\n", + " this.email_input.disabled = true;\n", + "\n", + " this.el.appendChild(this.email_input); \n", + " },\n", + " });\n", + "\n", + " return {\n", + " EmailView: EmailView\n", + " };\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Test" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should be able to display your widget just like any other widget now." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Email()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Making the widget stateful" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is not much that you can do with the above example that you can't do with the IPython display framework. To change this, you will make the widget stateful. Instead of displaying a static \"example@example.com\" email address, it will display an address set by the back end. First you need to add a traitlet in the back end. Use the name of `value` to stay consistent with the rest of the widget framework and to allow your widget to be used with interact." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We want to be able to avoid user to write an invalid email address, so we need a validator using traitlets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from traitlets import Unicode, Bool, validate, TraitError\n", + "from ipywidgets import DOMWidget, ValueWidget, register\n", + "\n", + "\n", + "@register\n", + "class Email(DOMWidget, ValueWidget):\n", + " _view_name = Unicode('EmailView').tag(sync=True)\n", + " _view_module = Unicode('email_widget').tag(sync=True)\n", + " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", + "\n", + " # Attributes\n", + " value = Unicode('example@example.com', help=\"The email value.\").tag(sync=True)\n", + " disabled = Bool(False, help=\"Enable or disable user changes.\").tag(sync=True)\n", + "\n", + " # Basic validator for the email value\n", + " @validate('value')\n", + " def _valid_value(self, proposal):\n", + " if proposal['value'].count(\"@\") != 1:\n", + " raise TraitError('Invalid email value: it must contain an \"@\" character')\n", + " if proposal['value'].count(\".\") == 0:\n", + " raise TraitError('Invalid email value: it must contain at least one \".\" character')\n", + " return proposal['value']" + ] + }, + { + "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": [ + "### Rendering model contents" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By replacing the string literal with a call to `model.get`, the view will now 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": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "require.undef('email_widget');\n", + "\n", + "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + " \n", + " var EmailView = widgets.DOMWidgetView.extend({\n", + "\n", + " // Render the view.\n", + " render: function() { \n", + " this.email_input = document.createElement('input');\n", + " this.email_input.type = 'email';\n", + " this.email_input.value = this.model.get('value');\n", + " this.email_input.disabled = this.model.get('disabled');\n", + "\n", + " this.el.appendChild(this.email_input);\n", + " },\n", + " });\n", + "\n", + " return {\n", + " EmailView: EmailView\n", + " };\n", + "});" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Email(value='john.doe@domain.com', disabled=True)" + ] + }, + { + "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": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "require.undef('email_widget');\n", + "\n", + "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + " \n", + " var EmailView = widgets.DOMWidgetView.extend({\n", + "\n", + " // Render the view.\n", + " render: function() { \n", + " this.email_input = document.createElement('input');\n", + " this.email_input.type = 'email';\n", + " this.email_input.value = this.model.get('value');\n", + " this.email_input.disabled = this.model.get('disabled');\n", + "\n", + " this.el.appendChild(this.email_input);\n", + " \n", + " // Python -> JavaScript update\n", + " this.model.on('change:value', this.value_changed, this);\n", + " this.model.on('change:disabled', this.disabled_changed, this);\n", + " },\n", + " \n", + " value_changed: function() {\n", + " this.email_input.value = this.model.get('value'); \n", + " },\n", + " \n", + " disabled_changed: function() {\n", + " this.email_input.disabled = this.model.get('disabled'); \n", + " },\n", + " });\n", + "\n", + " return {\n", + " EmailView: EmailView\n", + " };\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we can do it using the `model.set` method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%javascript\n", + "require.undef('email_widget');\n", + "\n", + "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + " \n", + " var EmailView = widgets.DOMWidgetView.extend({\n", + "\n", + " // Render the view.\n", + " render: function() { \n", + " this.email_input = document.createElement('input');\n", + " this.email_input.type = 'email';\n", + " this.email_input.value = this.model.get('value');\n", + " this.email_input.disabled = this.model.get('disabled');\n", + "\n", + " this.el.appendChild(this.email_input);\n", + " \n", + " // Python -> JavaScript update\n", + " this.model.on('change:value', this.value_changed, this);\n", + " this.model.on('change:disabled', this.disabled_changed, this);\n", + " \n", + " // JavaScript -> Python update\n", + " this.email_input.onchange = this.input_changed.bind(this);\n", + " },\n", + " \n", + " value_changed: function() {\n", + " this.email_input.value = this.model.get('value'); \n", + " },\n", + " \n", + " disabled_changed: function() {\n", + " this.email_input.disabled = this.model.get('disabled'); \n", + " },\n", + " \n", + " input_changed: function() {\n", + " this.model.set('value', this.email_input.value);\n", + " this.model.save_changes();\n", + " },\n", + " });\n", + "\n", + " return {\n", + " EmailView: EmailView\n", + " };\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "email = Email(value='john.doe@domain.com', disabled=False)\n", + "email" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "email.value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "email.value = 'jane.doe@domain.com'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More advanced uses: Packaging and distributing Jupyter widgets\n", + "\n", + "A template project is available in the form of a cookie cutter: https://github.com/jupyter/widget-cookiecutter\n", + "\n", + "This project is meant to help custom widget authors get started with the packaging and the distribution of Jupyter interactive widgets.\n", + "\n", + "It produces a project for a Jupyter interactive widget library following the current best practices for using interactive widgets. An implementation for a placeholder \"Hello World\" widget is provided." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "[Index](Index.ipynb) - [Back](Widget Styling.ipynb) - [Next](Widget Asynchronous.ipynb)" + ] + } + ], + "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.8.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From a3f177074356a5c67febafcd97a04878b9a57c72 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Mon, 13 Jul 2020 19:43:03 +0200 Subject: [PATCH 02/16] Iterate on the custom widget tutorial --- docs/source/examples/Widget Custom.ipynb | 232 ++++++++++-------- docs/source/examples/Widget Styling.ipynb | 2 +- .../examples/images/custom-widget-hello.png | Bin 0 -> 9995 bytes 3 files changed, 125 insertions(+), 109 deletions(-) create mode 100644 docs/source/examples/images/custom-widget-hello.png diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index f294c52d9c..ac7ba84a07 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -141,7 +141,27 @@ "jupyter nbextension enable --sys-prefix --py ipyemail\n", "```\n", "\n", - "You are now ready to implement the core functionality of the widget!" + "### Testing the installation\n", + "\n", + "At this point, you should be able to open a notebook and create a new `ExampleWidget`.\n", + "\n", + "To test it, execute the following in a terminal:\n", + "\n", + "```bash\n", + "# if you are using the classic notebook\n", + "jupyter notebook\n", + "\n", + "# if you are using JupyterLab\n", + "jupyter lab\n", + "```\n", + "\n", + "And open `examples/introduction.ipynb`.\n", + "\n", + "By default, the widget displays the string `Hello World` with a colored background:\n", + "\n", + "![hello-world](./images/custom-widget-hello.png)\n", + "\n", + "The next step will walk you through how to modify the existing code to transform the widget into an email widget." ] }, { @@ -152,7 +172,7 @@ } }, "source": [ - "## Building a Custom Widget" + "## Implementing the widget" ] }, { @@ -203,24 +223,45 @@ "source": [ "Inheriting from the DOMWidget does not tell the widget framework what front end widget to associate with your back end widget.\n", "\n", - "Instead, you must tell it yourself by defining specially named trait attributes, `_view_name`, `_view_module`, and `_view_module_version` (as seen below) and optionally `_model_name` and `_model_module`." + "Instead, you must tell it yourself by defining specially named trait attributes, `_view_name`, `_view_module`, and `_view_module_version` (as seen below) and optionally `_model_name` and `_model_module`.\n", + "\n", + "\n", + "In `ipyemail/example.py`, replace the example code with the following:\n", + "\n", + "```python\n", + "from ipywidgets import DOMWidget, ValueWidget, register\n", + "from traitlets import Unicode, Bool, validate, TraitError\n", + "\n", + "from ._frontend import module_name, module_version\n", + "\n", + "\n", + "@register\n", + "class EmailWidget(DOMWidget, ValueWidget):\n", + " _model_name = Unicode('EmailModel').tag(sync=True)\n", + " _model_module = Unicode(module_name).tag(sync=True)\n", + " _model_module_version = Unicode(module_version).tag(sync=True)\n", + "\n", + " _view_name = Unicode('EmailView').tag(sync=True)\n", + " _view_module = Unicode(module_name).tag(sync=True)\n", + " _view_module_version = Unicode(module_version).tag(sync=True)\n", + "```" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "from traitlets import Unicode, Bool, validate, TraitError\n", - "from ipywidgets import DOMWidget, ValueWidget, register\n", + "In `ipyemail/__init__.py`, change the import from:\n", "\n", + "```python\n", + "from .example import ExampleWidget\n", + "```\n", "\n", - "@register\n", - "class Email(DOMWidget, ValueWidget):\n", - " _view_name = Unicode('EmailView').tag(sync=True)\n", - " _view_module = Unicode('email_widget').tag(sync=True)\n", - " _view_module_version = Unicode('0.1.0').tag(sync=True)" + "To:\n", + "\n", + "```python\n", + "from .example import EmailWidget\n", + "```" ] }, { @@ -300,7 +341,7 @@ } }, "source": [ - "## Front end (JavaScript)" + "## Front end (TypeScript)" ] }, { @@ -325,66 +366,52 @@ } }, "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": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "define('email_widget', [\"@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": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "require.undef('email_widget');\n", - "\n", - "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", - " \n", - " // Define the EmailView\n", - " var EmailView = widgets.DOMWidgetView.extend({\n", - " \n", - " });\n", + "The TypeScript cookiecutter generates a file `src/widget.ts`. Open the file and rename `ExampleModel` to `EmailModel` and `ExampleView` to `EmailView`:\n", + "\n", + "```typescript\n", + "export\n", + "class EmailModel extends DOMWidgetModel {\n", + " defaults() {\n", + " return {...super.defaults(),\n", + " _model_name: EmailModel.model_name,\n", + " _model_module: EmailModel.model_module,\n", + " _model_module_version: EmailModel.model_module_version,\n", + " _view_name: EmailModel.view_name,\n", + " _view_module: EmailModel.view_module,\n", + " _view_module_version: EmailModel.view_module_version,\n", + " value : 'Hello World'\n", + " };\n", + " }\n", "\n", - " return {\n", - " EmailView: EmailView\n", + " static serializers: ISerializers = {\n", + " ...DOMWidgetModel.serializers,\n", + " // Add any extra serializers here\n", " }\n", - "});" + "\n", + " static model_name = 'EmailModel';\n", + " static model_module = MODULE_NAME;\n", + " static model_module_version = MODULE_VERSION;\n", + " static view_name = 'EmailView'; // Set to null if no view\n", + " static view_module = MODULE_NAME; // Set to null if no view\n", + " static view_module_version = MODULE_VERSION;\n", + "}\n", + "\n", + "\n", + "export\n", + "class EmailView extends DOMWidgetView {\n", + " render() {\n", + " this.el.classList.add('custom-widget');\n", + "\n", + " this.value_changed();\n", + " this.model.on('change:value', this.value_changed, this);\n", + " }\n", + "\n", + " value_changed() {\n", + " this.el.textContent = this.model.get('value');\n", + " }\n", + "}\n", + "\n", + "```" ] }, { @@ -402,37 +429,29 @@ "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." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "require.undef('email_widget');\n", + "Now, override the base `render` method of the view to define custom rendering logic.\n", "\n", - "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", + "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.\n", "\n", - " var EmailView = widgets.DOMWidgetView.extend({\n", + "In `src/widget.ts`, define the `email_input` attribute:\n", "\n", - " // Render the view.\n", - " render: function() { \n", - " this.email_input = document.createElement('input');\n", - " this.email_input.type = 'email';\n", - " this.email_input.value = 'example@example.com';\n", - " this.email_input.disabled = true;\n", + "```typescript\n", + "export class EmailView extends DOMWidgetView {\n", + " private _emailInput: HTMLInputElement;\n", + "}\n", + "```\n", "\n", - " this.el.appendChild(this.email_input); \n", - " },\n", - " });\n", + "Then, add the following logic for the `render`:\n", "\n", - " return {\n", - " EmailView: EmailView\n", - " };\n", - "});" + "```typescript\n", + "render: function() { \n", + " this._emailInput = document.createElement('input');\n", + " this._emailInput.type = 'email';\n", + " this._emailInput.value = 'example@example.com';\n", + " this._emailInput.disabled = true;\n", + " this.el.appendChild(this._emailInput);\n", + "},\n", + "```" ] }, { @@ -450,16 +469,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You should be able to display your widget just like any other widget now." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Email()" + "You should be able to display your widget just like any other widget now:\n", + "\n", + "```python\n", + "from ipyemail import EmailWidget\n", + "\n", + "EmailWidget()\n", + "```" ] }, { diff --git a/docs/source/examples/Widget Styling.ipynb b/docs/source/examples/Widget Styling.ipynb index 6ee99cbe60..e4163e4cdd 100644 --- a/docs/source/examples/Widget Styling.ipynb +++ b/docs/source/examples/Widget Styling.ipynb @@ -1413,7 +1413,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.8.1" } }, "nbformat": 4, diff --git a/docs/source/examples/images/custom-widget-hello.png b/docs/source/examples/images/custom-widget-hello.png new file mode 100644 index 0000000000000000000000000000000000000000..c09284fac3c91dce28a2c1541218c4c984150bcb GIT binary patch literal 9995 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 OfU>;0T(yiv=>GyOU>uMD literal 0 HcmV?d00001 From d01da4d584c9c9838f16e103a56e83515da61db8 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Mon, 13 Jul 2020 20:19:33 +0200 Subject: [PATCH 03/16] Update custom widget tutorial --- docs/source/examples/Widget Custom.ipynb | 288 +++++++++-------------- 1 file changed, 114 insertions(+), 174 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index ac7ba84a07..066a76752c 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -500,27 +500,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We want to be able to avoid user to write an invalid email address, so we need a validator using traitlets." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "We want to be able to avoid user to write an invalid email address, so we need a validator using traitlets.\n", + "\n", + "```python\n", "from traitlets import Unicode, Bool, validate, TraitError\n", "from ipywidgets import DOMWidget, ValueWidget, register\n", "\n", + "from ._frontend import module_name, module_version\n", + "\n", "\n", "@register\n", "class Email(DOMWidget, ValueWidget):\n", + " _model_name = Unicode('EmailModel').tag(sync=True)\n", + " _model_module = Unicode(module_name).tag(sync=True)\n", + " _model_module_version = Unicode(module_version).tag(sync=True)\n", + "\n", " _view_name = Unicode('EmailView').tag(sync=True)\n", - " _view_module = Unicode('email_widget').tag(sync=True)\n", - " _view_module_version = Unicode('0.1.0').tag(sync=True)\n", + " _view_module = Unicode(module_name).tag(sync=True)\n", + " _view_module_version = Unicode(module_version).tag(sync=True)\n", "\n", - " # Attributes\n", - " value = Unicode('example@example.com', help=\"The email value.\").tag(sync=True)\n", + " value = Unicode('example@example.com').tag(sync=True)\n", " disabled = Bool(False, help=\"Enable or disable user changes.\").tag(sync=True)\n", "\n", " # Basic validator for the email value\n", @@ -530,7 +529,8 @@ " raise TraitError('Invalid email value: it must contain an \"@\" character')\n", " if proposal['value'].count(\".\") == 0:\n", " raise TraitError('Invalid email value: it must contain at least one \".\" character')\n", - " return proposal['value']" + " return proposal['value']\n", + "```" ] }, { @@ -566,46 +566,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "By replacing the string literal with a call to `model.get`, the view will now 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": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "require.undef('email_widget');\n", - "\n", - "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", - " \n", - " var EmailView = widgets.DOMWidgetView.extend({\n", - "\n", - " // Render the view.\n", - " render: function() { \n", - " this.email_input = document.createElement('input');\n", - " this.email_input.type = 'email';\n", - " this.email_input.value = this.model.get('value');\n", - " this.email_input.disabled = this.model.get('disabled');\n", + "By replacing the string literal with a call to `model.get`, the view will now display the value of the back end upon display. However, it will not update itself to a new value when the value changes.\n", "\n", - " this.el.appendChild(this.email_input);\n", - " },\n", - " });\n", + "```typescript\n", + "export\n", + "class EmailView extends DOMWidgetView {\n", + " render() {\n", + " this._emailInput = document.createElement('input');\n", + " this._emailInput.type = 'email';\n", + " this._emailInput.value = this.model.get('value');\n", + " this._emailInput.disabled = this.model.get('disabled');\n", + " \n", + " this.el.appendChild(this._emailInput);\n", + " }\n", "\n", - " return {\n", - " EmailView: EmailView\n", - " };\n", - "});" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Email(value='john.doe@domain.com', disabled=True)" + " private _emailInput: HTMLInputElement;\n", + "}\n", + "```" ] }, { @@ -623,106 +600,78 @@ "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": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "require.undef('email_widget');\n", - "\n", - "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", - " \n", - " var EmailView = widgets.DOMWidgetView.extend({\n", - "\n", - " // Render the view.\n", - " render: function() { \n", - " this.email_input = document.createElement('input');\n", - " this.email_input.type = 'email';\n", - " this.email_input.value = this.model.get('value');\n", - " this.email_input.disabled = this.model.get('disabled');\n", - "\n", - " this.el.appendChild(this.email_input);\n", - " \n", - " // Python -> JavaScript update\n", - " this.model.on('change:value', this.value_changed, this);\n", - " this.model.on('change:disabled', this.disabled_changed, this);\n", - " },\n", - " \n", - " value_changed: function() {\n", - " this.email_input.value = this.model.get('value'); \n", - " },\n", - " \n", - " disabled_changed: function() {\n", - " this.email_input.disabled = this.model.get('disabled'); \n", - " },\n", - " });\n", - "\n", - " return {\n", - " EmailView: EmailView\n", - " };\n", - "});" + "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).\n", + "\n", + "```typescript\n", + "export\n", + "class EmailView extends DOMWidgetView {\n", + " render() {\n", + " this._emailInput = document.createElement('input');\n", + " this._emailInput.type = 'email';\n", + " this._emailInput.value = this.model.get('value');\n", + " this._emailInput.disabled = this.model.get('disabled');\n", + "\n", + " this.el.appendChild(this._emailInput);\n", + "\n", + " // Python -> JavaScript update\n", + " this.model.on('change:value', this._onValueChanged, this);\n", + " this.model.on('change:disabled', this._onDisabledChanged, this);\n", + " }\n", + "\n", + " private _onValueChanged() {\n", + " this._emailInput.value = this.model.get('value');\n", + " }\n", + "\n", + " private _onDisabledChanged() {\n", + " this._emailInput.disabled = this.model.get('disabled');\n", + " }\n", + "\n", + " private _emailInput: HTMLInputElement;\n", + "}\n", + "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we can do it using the `model.set` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "require.undef('email_widget');\n", - "\n", - "define('email_widget', [\"@jupyter-widgets/base\"], function(widgets) {\n", - " \n", - " var EmailView = widgets.DOMWidgetView.extend({\n", - "\n", - " // Render the view.\n", - " render: function() { \n", - " this.email_input = document.createElement('input');\n", - " this.email_input.type = 'email';\n", - " this.email_input.value = this.model.get('value');\n", - " this.email_input.disabled = this.model.get('disabled');\n", - "\n", - " this.el.appendChild(this.email_input);\n", - " \n", - " // Python -> JavaScript update\n", - " this.model.on('change:value', this.value_changed, this);\n", - " this.model.on('change:disabled', this.disabled_changed, this);\n", - " \n", - " // JavaScript -> Python update\n", - " this.email_input.onchange = this.input_changed.bind(this);\n", - " },\n", - " \n", - " value_changed: function() {\n", - " this.email_input.value = this.model.get('value'); \n", - " },\n", - " \n", - " disabled_changed: function() {\n", - " this.email_input.disabled = this.model.get('disabled'); \n", - " },\n", - " \n", - " input_changed: function() {\n", - " this.model.set('value', this.email_input.value);\n", - " this.model.save_changes();\n", - " },\n", - " });\n", - "\n", - " return {\n", - " EmailView: EmailView\n", - " };\n", - "});" + "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we can do it using the `model.set` method.\n", + "\n", + "```typescript\n", + "export\n", + "class EmailView extends DOMWidgetView {\n", + " render() {\n", + " this._emailInput = document.createElement('input');\n", + " this._emailInput.type = 'email';\n", + " this._emailInput.value = this.model.get('value');\n", + " this._emailInput.disabled = this.model.get('disabled');\n", + "\n", + " this.el.appendChild(this._emailInput);\n", + "\n", + " // Python -> JavaScript update\n", + " this.model.on('change:value', this._onValueChanged, this);\n", + " this.model.on('change:disabled', this._onDisabledChanged, this);\n", + "\n", + " // JavaScript -> Python update\n", + " this._emailInput.onchange = this._onInputChanged.bind(this);\n", + " }\n", + "\n", + " private _onValueChanged() {\n", + " this._emailInput.value = this.model.get('value');\n", + " }\n", + "\n", + " private _onDisabledChanged() {\n", + " this._emailInput.disabled = this.model.get('disabled');\n", + " }\n", + "\n", + " private _onInputChanged() {\n", + " this.model.set('value', this._emailInput.value);\n", + " this.model.save_changes();\n", + " }\n", + "\n", + " private _emailInput: HTMLInputElement;\n", + "}\n", + "```" ] }, { @@ -733,35 +682,26 @@ } }, "source": [ - "## Test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "## Test\n", + "\n", + "To instantiate a new widget:\n", + "\n", + "```python\n", "email = Email(value='john.doe@domain.com', disabled=False)\n", - "email" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "email.value" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "email.value = 'jane.doe@domain.com'" + "email\n", + "```\n", + "\n", + "To get the value of the widget:\n", + "\n", + "```python\n", + "email.value\n", + "```\n", + "\n", + "To set the value of the widget:\n", + "\n", + "```python\n", + "email.value = 'jane.doe@domain.com'\n", + "```" ] }, { From 6152f7edd06caf6810697b4aef489c529e71470f Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Mon, 13 Jul 2020 20:22:27 +0200 Subject: [PATCH 04/16] Update EmailWidget to Email --- docs/source/examples/Widget Custom.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 066a76752c..9fd1521fd3 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -17,7 +17,7 @@ } }, "source": [ - "# Building a Custom Widget - Email widget" + "# Building a Custom Widget - Email widget" ] }, { @@ -236,7 +236,7 @@ "\n", "\n", "@register\n", - "class EmailWidget(DOMWidget, ValueWidget):\n", + "class Email(DOMWidget, ValueWidget):\n", " _model_name = Unicode('EmailModel').tag(sync=True)\n", " _model_module = Unicode(module_name).tag(sync=True)\n", " _model_module_version = Unicode(module_version).tag(sync=True)\n", @@ -260,7 +260,7 @@ "To:\n", "\n", "```python\n", - "from .example import EmailWidget\n", + "from .example import Email\n", "```" ] }, @@ -472,9 +472,9 @@ "You should be able to display your widget just like any other widget now:\n", "\n", "```python\n", - "from ipyemail import EmailWidget\n", + "from ipyemail import Email\n", "\n", - "EmailWidget()\n", + "Email()\n", "```" ] }, From c342d06ff698edd0c772634af214f2e7fe94337c Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Mon, 13 Jul 2020 21:14:42 +0200 Subject: [PATCH 05/16] Update cookiecutter prompt --- docs/source/examples/Widget Custom.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 9fd1521fd3..fc38d1adc0 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -88,14 +88,14 @@ "When prompted, enter the desired values like the following for all of the cookiecutter prompts:\n", "\n", "```bash\n", - "author_name []:\n", - "author_email []:\n", + "author_name []: Your Name\n", + "author_email []: your@name.net\n", "github_project_name []: ipyemail\n", - "github_organization_name []:\n", + "github_organization_name []: \n", "python_package_name [ipyemail]:\n", "npm_package_name [ipyemail]:\n", "npm_package_version [0.1.0]:\n", - "project_short_description [A Custom Jupyter Widget Library]:\n", + "project_short_description [A Custom Jupyter Widget Library]: An Custom Email Widget\n", "```\n", "\n", "Change to the directory the cookiecutter created and list the files.\n", From b871c12fa7bdaf03dabba44cffc9f5d6a0e00867 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Tue, 14 Jul 2020 10:20:41 +0200 Subject: [PATCH 06/16] Minor wording fixes for custom widget --- docs/source/examples/Widget Custom.ipynb | 27 ++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index fc38d1adc0..4b50015bfe 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -30,7 +30,7 @@ "\n", "To create a custom widget, you need to define the widget both in the browser and in the Python kernel.\n", "\n", - "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter" + "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter: https://github.com/jupyter-widgets/widget-ts-cookiecutter" ] }, { @@ -50,7 +50,7 @@ "Next create a conda environment that includes:\n", "\n", "1. the latest release of JupyterLab or the classic notebook\n", - "2. [cookiecutter](https://github.com/cookiecutter/cookiecutter), the tool you will use to bootstrap the custom widget\n", + "2. [cookiecutter](https://github.com/cookiecutter/cookiecutter): the tool you will use to bootstrap the custom widget\n", "3. [NodeJS](https://nodejs.org): the JavaScript runtime you'll use to\n", " compile the web assets (e.g., TypeScript, CSS) for the custom widget\n", "\n", @@ -85,7 +85,7 @@ "cookiecutter https://github.com/jupyter-widgets/widget-ts-cookiecutter\n", "```\n", "\n", - "When prompted, enter the desired values like the following for all of the cookiecutter prompts:\n", + "When prompted, enter the desired values as follows:\n", "\n", "```bash\n", "author_name []: Your Name\n", @@ -98,14 +98,14 @@ "project_short_description [A Custom Jupyter Widget Library]: An Custom Email Widget\n", "```\n", "\n", - "Change to the directory the cookiecutter created and list the files.\n", + "Change to the directory the cookiecutter created and list the files:\n", "\n", "```bash\n", "cd ipyemail\n", "ls\n", "```\n", "\n", - "You should see a list like the following.\n", + "You should see a list like the following:\n", "\n", "```bash\n", "appveyor.yml css examples ipyemail.json MANIFEST.in pytest.ini readthedocs.yml setup.cfg src tsconfig.json\n", @@ -127,7 +127,7 @@ "If you are using JupyterLab:\n", "\n", "```bash\n", - "# install the widget manager to display Widgets in the JupyterLab interface\n", + "# install the widget manager to display the widgets in JupyterLab\n", "jupyter labextension install @jupyter-widgets/jupyterlab-manager --no-build\n", "\n", "# install the local extension\n", @@ -161,7 +161,7 @@ "\n", "![hello-world](./images/custom-widget-hello.png)\n", "\n", - "The next step will walk you through how to modify the existing code to transform the widget into an email widget." + "The next steps will walk you through how to modify the existing code to transform the widget into an email widget." ] }, { @@ -225,8 +225,9 @@ "\n", "Instead, you must tell it yourself by defining specially named trait attributes, `_view_name`, `_view_module`, and `_view_module_version` (as seen below) and optionally `_model_name` and `_model_module`.\n", "\n", + "First let's rename `ipyemail/example.py` to `ipyemail/widget.py`.\n", "\n", - "In `ipyemail/example.py`, replace the example code with the following:\n", + "In `ipyemail/widget.py`, replace the example code with the following:\n", "\n", "```python\n", "from ipywidgets import DOMWidget, ValueWidget, register\n", @@ -254,13 +255,13 @@ "In `ipyemail/__init__.py`, change the import from:\n", "\n", "```python\n", - "from .example import ExampleWidget\n", + "from .widget import ExampleWidget\n", "```\n", "\n", "To:\n", "\n", "```python\n", - "from .example import Email\n", + "from .widget import Email\n", "```" ] }, @@ -433,7 +434,7 @@ "\n", "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.\n", "\n", - "In `src/widget.ts`, define the `email_input` attribute:\n", + "In `src/widget.ts`, define the `_emailInput` attribute:\n", "\n", "```typescript\n", "export class EmailView extends DOMWidgetView {\n", @@ -441,7 +442,7 @@ "}\n", "```\n", "\n", - "Then, add the following logic for the `render`:\n", + "Then, add the following logic for the `render` method:\n", "\n", "```typescript\n", "render: function() { \n", @@ -503,8 +504,8 @@ "We want to be able to avoid user to write an invalid email address, so we need a validator using traitlets.\n", "\n", "```python\n", - "from traitlets import Unicode, Bool, validate, TraitError\n", "from ipywidgets import DOMWidget, ValueWidget, register\n", + "from traitlets import Unicode, Bool, validate, TraitError\n", "\n", "from ._frontend import module_name, module_version\n", "\n", From ae89a88606479d0c6bd83729f3d45bf126c68618 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Tue, 14 Jul 2020 10:33:30 +0200 Subject: [PATCH 07/16] Add Learn More to custom widget tutorial --- docs/source/examples/Widget Custom.ipynb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 4b50015bfe..ab81aae695 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -709,13 +709,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## More advanced uses: Packaging and distributing Jupyter widgets\n", + "## Learn more\n", "\n", - "A template project is available in the form of a cookie cutter: https://github.com/jupyter/widget-cookiecutter\n", + "As we have seen in this tutorial, starting from a cookiecutter project is really useful to quickly prototype a custom widget.\n", "\n", - "This project is meant to help custom widget authors get started with the packaging and the distribution of Jupyter interactive widgets.\n", + "Two cookiecutter projects are currently available:\n", + "\n", + "- [widget-ts-cookiecutter](https://github.com/jupyter-widgets/widget-ts-cookiecutter): To create a custom widget in TypeScript\n", + "- [widget-cookiecutter](https://github.com/jupyter-widgets/widget-cookiecutter): To create a custom widget in JavaScript\n", + "\n", + "If you want to learn more about building custom widgets, you can also check out the rich ecosystem of third-party widgets:\n", "\n", - "It produces a project for a Jupyter interactive widget library following the current best practices for using interactive widgets. An implementation for a placeholder \"Hello World\" widget is provided." + "- [bqplot](https://github.com/bqplot/bqplot): Interactive 2-D plotting\n", + "- [ipyleaflet](https://github.com/jupyter-widgets/ipyleaflet): Interactive maps\n", + "- [ipycanvas](https://github.com/martinRenou/ipycanvas): Interactive Canvas\n", + "- [ipyvolume](https://github.com/maartenbreddels/ipyvolume): Interactive 3-D plotting\n", + "- [ipywebrtc](https://github.com/maartenbreddels/ipywebrtc): WebRTC and MediaStream API in Jupyter\n", + "- [ipysheet](https://github.com/QuantStack/ipysheet): Spreadsheet Widget" ] }, { From 3a2202c10231dbf30da0191fe09629c9e2dd7e9a Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Tue, 14 Jul 2020 10:51:31 +0200 Subject: [PATCH 08/16] Minor tweaks to the custom widget tutorial --- docs/source/examples/Widget Custom.ipynb | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index ab81aae695..36d0058f9b 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -370,8 +370,7 @@ "The TypeScript cookiecutter generates a file `src/widget.ts`. Open the file and rename `ExampleModel` to `EmailModel` and `ExampleView` to `EmailView`:\n", "\n", "```typescript\n", - "export\n", - "class EmailModel extends DOMWidgetModel {\n", + "export class EmailModel extends DOMWidgetModel {\n", " defaults() {\n", " return {...super.defaults(),\n", " _model_name: EmailModel.model_name,\n", @@ -392,14 +391,13 @@ " static model_name = 'EmailModel';\n", " static model_module = MODULE_NAME;\n", " static model_module_version = MODULE_VERSION;\n", - " static view_name = 'EmailView'; // Set to null if no view\n", - " static view_module = MODULE_NAME; // Set to null if no view\n", + " static view_name = 'EmailView';\n", + " static view_module = MODULE_NAME;\n", " static view_module_version = MODULE_VERSION;\n", "}\n", "\n", "\n", - "export\n", - "class EmailView extends DOMWidgetView {\n", + "export class EmailView extends DOMWidgetView {\n", " render() {\n", " this.el.classList.add('custom-widget');\n", "\n", @@ -445,7 +443,7 @@ "Then, add the following logic for the `render` method:\n", "\n", "```typescript\n", - "render: function() { \n", + "render() { \n", " this._emailInput = document.createElement('input');\n", " this._emailInput.type = 'email';\n", " this._emailInput.value = 'example@example.com';\n", @@ -501,7 +499,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We want to be able to avoid user to write an invalid email address, so we need a validator using traitlets.\n", + "We want to be able to avoid the user to write an invalid email address, so we need a validator using traitlets.\n", "\n", "```python\n", "from ipywidgets import DOMWidget, ValueWidget, register\n", @@ -570,8 +568,7 @@ "By replacing the string literal with a call to `model.get`, the view will now display the value of the back end upon display. However, it will not update itself to a new value when the value changes.\n", "\n", "```typescript\n", - "export\n", - "class EmailView extends DOMWidgetView {\n", + "export class EmailView extends DOMWidgetView {\n", " render() {\n", " this._emailInput = document.createElement('input');\n", " this._emailInput.type = 'email';\n", @@ -604,8 +601,7 @@ "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).\n", "\n", "```typescript\n", - "export\n", - "class EmailView extends DOMWidgetView {\n", + "export class EmailView extends DOMWidgetView {\n", " render() {\n", " this._emailInput = document.createElement('input');\n", " this._emailInput.type = 'email';\n", @@ -639,8 +635,7 @@ "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we can do it using the `model.set` method.\n", "\n", "```typescript\n", - "export\n", - "class EmailView extends DOMWidgetView {\n", + "export class EmailView extends DOMWidgetView {\n", " render() {\n", " this._emailInput = document.createElement('input');\n", " this._emailInput.type = 'email';\n", From 3f2c5af1f3dd03add7b03f854a51571fc5132359 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 3 Feb 2021 10:04:05 +0100 Subject: [PATCH 09/16] Add note on mutable types and traitlets --- docs/source/examples/Widget Custom.ipynb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 36d0058f9b..323d977ef7 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -280,7 +280,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Traitlets is an IPython library for defining type-safe properties on configurable objects. For this tutorial you do not need to worry about the *configurable* piece of the traitlets machinery. The `sync=True` keyword argument tells the widget framework to handle synchronizing that value to the browser. Without `sync=True`, attributes of the widget won't be synchronized with the front-end." + "Traitlets is an IPython library for defining type-safe properties on configurable objects. For this tutorial you do not need to worry about the *configurable* piece of the traitlets machinery. The `sync=True` keyword argument tells the widget framework to handle synchronizing that value to the browser. Without `sync=True`, attributes of the widget won't be synchronized with the front-end.\n", + "\n", + "
\n", + "Syncing mutable types\n", + " \n", + "Please keep in mind that mutable types will not necessarily be synced when they are modified. For example appending an element to a `list` will not cause the changes to sync. Instead a new list must be created and assigned to the trait for the changes to be synced.\n", + "
" ] }, { @@ -749,7 +755,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.1" + "version": "3.9.1" } }, "nbformat": 4, From 2335df7ccfde914a513c2db0d0a883218f25002e Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 3 Feb 2021 10:32:26 +0100 Subject: [PATCH 10/16] Add a screenshot for the end result --- docs/source/examples/Widget Custom.ipynb | 32 +++++++++++++++--- .../examples/images/custom-widget-result.png | Bin 0 -> 62706 bytes 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 docs/source/examples/images/custom-widget-result.png diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 323d977ef7..c10cc99ac2 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -93,7 +93,7 @@ "github_project_name []: ipyemail\n", "github_organization_name []: \n", "python_package_name [ipyemail]:\n", - "npm_package_name [ipyemail]:\n", + "npm_package_name [ipyemail]: jupyter-email\n", "npm_package_version [0.1.0]:\n", "project_short_description [A Custom Jupyter Widget Library]: An Custom Email Widget\n", "```\n", @@ -124,7 +124,7 @@ "\n", "You also need to enable the widget frontend extension.\n", "\n", - "If you are using JupyterLab:\n", + "If you are using JupyterLab 2.x:\n", "\n", "```bash\n", "# install the widget manager to display the widgets in JupyterLab\n", @@ -134,7 +134,7 @@ "jupyter labextension install .\n", "```\n", "\n", - "If you are using the classic notebook:\n", + "If you are using the Classic Notebook:\n", "\n", "```bash\n", "jupyter nbextension install --sys-prefix --symlink --overwrite --py ipyemail\n", @@ -467,14 +467,27 @@ } }, "source": [ - "## Test" + "## Test\n", + "\n", + "First, run the following command to recreate the frontend bundle:\n", + "\n", + "```bash\n", + "npm run build\n", + "```\n", + "\n", + "If you use JupyterLab, you might want to use `jlpm` as the npm client. `jlpm` uses `yarn` under the hood as the package manager. The main difference compared to `npm` is that `jlpm` will generate a `yarn.lock` file for the dependencies, instead of `package-lock.json`. With `jlpm` the command is:\n", + "\n", + "\n", + "```bash\n", + "jlpm run build\n", + "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You should be able to display your widget just like any other widget now:\n", + "After reloading the page, you should be able to display your widget just like any other widget now:\n", "\n", "```python\n", "from ipyemail import Email\n", @@ -706,6 +719,15 @@ "```" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The end result should look like the following:\n", + " \n", + "![end-result](./images/custom-widget-result.png)" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/source/examples/images/custom-widget-result.png b/docs/source/examples/images/custom-widget-result.png new file mode 100644 index 0000000000000000000000000000000000000000..39ccf8e0165ca89e2ac02ea28ce854f409be4d34 GIT binary patch literal 62706 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 MXz6Qaow$7GpJG1j+5i9m literal 0 HcmV?d00001 From 9ea6a2a529f46f39cefdbce3246ea8a1cc5ce816 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 09:50:31 +0100 Subject: [PATCH 11/16] Apply suggestions from code review Co-authored-by: Ian Hunt-Isaak --- docs/source/examples/Widget Custom.ipynb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index c10cc99ac2..65a176fae3 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -197,13 +197,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To define a widget, you must inherit from the `DOMWidget`, `ValueWidget`, or `Widget` base class. If you intend for your widget to be displayed, you'll want to inherit from `DOMWidget`. If you intend for your widget to be used as an input for [interact](./Using%20Interact.ipynb), you'll want to inherit from `ValueWidget`. Your widget should inherit from `ValueWidget` if it has a single ovious output (for example, the output of an `IntSlider` is clearly the current value of the slider).\n", + "To define a widget, you must inherit from the `DOMWidget`, `ValueWidget`, or `Widget` base class. If you intend for your widget to be displayed, you'll want to inherit from `DOMWidget`. If you intend for your widget to be used as an input for [interact](./Using%20Interact.ipynb), you'll want to inherit from `ValueWidget`. Your widget should inherit from `ValueWidget` if it has a single obvious output (for example, the output of an `IntSlider` is clearly the current value of the slider).\n", "\n", "Both the `DOMWidget` and `ValueWidget` classes inherit from the `Widget` class. The `Widget` class is useful for cases in which the widget is not meant to be displayed directly in the notebook, but instead as a child of another rendering environment. Here are some examples:\n", "\n", "- If you wanted to create a [three.js](https://threejs.org/) widget (three.js is a popular WebGL library), you would implement the rendering window as a `DOMWidget` and any 3D objects or lights meant to be rendered in that window as `Widget`\n", "- If you wanted to create a widget that displays directly in the notebook for usage with `interact` (like `IntSlider`), you should multiple inherit from both `DOMWidget` and `ValueWidget`. \n", - "- If you wanted to create a widget that provides a value to `interact` but is controlled and viewed by another widget or an external source, you should inherit from only `ValueWidget`" + "- If you wanted to create a widget that provides a value to `interact` but does not need to be displayed, you should inherit from only `ValueWidget`" ] }, { @@ -242,10 +242,12 @@ " _model_module = Unicode(module_name).tag(sync=True)\n", " _model_module_version = Unicode(module_version).tag(sync=True)\n", "\n", - " _view_name = Unicode('EmailView').tag(sync=True)\n", - " _view_module = Unicode(module_name).tag(sync=True)\n", - " _view_module_version = Unicode(module_version).tag(sync=True)\n", - "```" + " _view_name = Unicode('EmailView').tag(sync=True)\n", + " _view_module = Unicode(module_name).tag(sync=True)\n", + " _view_module_version = Unicode(module_version).tag(sync=True)\n", + "\n", + " value = Unicode('example@example.com').tag(sync=True)\n", + "```" ] }, { @@ -255,7 +257,7 @@ "In `ipyemail/__init__.py`, change the import from:\n", "\n", "```python\n", - "from .widget import ExampleWidget\n", + "from .example import ExampleWidget\n", "```\n", "\n", "To:\n", @@ -362,7 +364,9 @@ "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." + "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 Backbone.js `Model` in the front end. Each front end `Model` handles the widget data and state, and can have any number of associate `View`s. In the context of a widget the `Views` are what render objects for the user to interact with, and the Model handles communication with the Python objects.\n", + "\n", + "On the first state push from python the synced traitlets are added automatically. 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.\n" ] }, { @@ -651,7 +655,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we can do it using the `model.set` method.\n", + "This allows us to update the value from the Python kernel to the views. Now to get the value updated from the front-end to the Python kernel (when the input is not disabled) we set the value on the frontend model using `model.set` and then sync the frontend model with the Python object using `model.save_changes`.\n", "\n", "```typescript\n", "export class EmailView extends DOMWidgetView {\n", From eeae710d2d8603bd4b6c43b3222cea51ead36b5f Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 10:51:40 +0100 Subject: [PATCH 12/16] Mention spectate to track changes to mutable data types --- docs/source/examples/Widget Custom.ipynb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 65a176fae3..3da1af7209 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -242,12 +242,12 @@ " _model_module = Unicode(module_name).tag(sync=True)\n", " _model_module_version = Unicode(module_version).tag(sync=True)\n", "\n", - " _view_name = Unicode('EmailView').tag(sync=True)\n", - " _view_module = Unicode(module_name).tag(sync=True)\n", - " _view_module_version = Unicode(module_version).tag(sync=True)\n", - "\n", - " value = Unicode('example@example.com').tag(sync=True)\n", - "```" + " _view_name = Unicode('EmailView').tag(sync=True)\n", + " _view_module = Unicode(module_name).tag(sync=True)\n", + " _view_module_version = Unicode(module_version).tag(sync=True)\n", + "\n", + " value = Unicode('example@example.com').tag(sync=True)\n", + "```" ] }, { @@ -288,6 +288,8 @@ "Syncing mutable types\n", " \n", "Please keep in mind that mutable types will not necessarily be synced when they are modified. For example appending an element to a `list` will not cause the changes to sync. Instead a new list must be created and assigned to the trait for the changes to be synced.\n", + " \n", + "An alternative would be to use a third-party library such as [spectate](https://github.com/rmorshea/spectate), which tracks changes to mutable data types.\n", "" ] }, From 9323a50f8d88dcbe9d4ecb2225b0deadd3c61ec4 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 10:53:19 +0100 Subject: [PATCH 13/16] Add .... to omit the previous code snippets --- docs/source/examples/Widget Custom.ipynb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 3da1af7209..fe372746d0 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -449,6 +449,12 @@ "```typescript\n", "export class EmailView extends DOMWidgetView {\n", " private _emailInput: HTMLInputElement;\n", + " \n", + " render() {\n", + " // .....\n", + " }\n", + " \n", + " // .....\n", "}\n", "```\n", "\n", From ce95560d1d07ed50a9ebf7ea9b374fd21f93ec45 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 10:54:39 +0100 Subject: [PATCH 14/16] Add full code for the render method for completeness --- docs/source/examples/Widget Custom.ipynb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index fe372746d0..37415c7899 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -467,6 +467,11 @@ " this._emailInput.value = 'example@example.com';\n", " this._emailInput.disabled = true;\n", " this.el.appendChild(this._emailInput);\n", + " \n", + " this.el.classList.add('custom-widget');\n", + "\n", + " this.value_changed();\n", + " this.model.on('change:value', this.value_changed, this);\n", "},\n", "```" ] From f86e603935f6c846bdb935a0e09e54e4f3a155f9 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 11:00:00 +0100 Subject: [PATCH 15/16] Link to the low level docs --- docs/source/examples/Widget Custom.ipynb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index 37415c7899..d803291e36 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -24,12 +24,6 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The widget framework is built on top of the Comm framework (short for communication). The Comm framework is a framework that allows the kernel to send/receive JSON messages to/from the front end (as seen below).\n", - "\n", - "![Widget layer](images/WidgetArch.png)\n", - "\n", - "To create a custom widget, you need to define the widget both in the browser and in the Python kernel.\n", - "\n", "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter: https://github.com/jupyter-widgets/widget-ts-cookiecutter" ] }, @@ -172,7 +166,15 @@ } }, "source": [ - "## Implementing the widget" + "## Implementing the widget\n", + "\n", + "The widget framework is built on top of the Comm framework (short for communication). The Comm framework is a framework that allows the kernel to send/receive JSON messages to/from the front end (as seen below).\n", + "\n", + "![Widget layer](images/WidgetArch.png)\n", + "\n", + "To learn more about how the underlying Widget protocol works, check out the [Low Level Widget](Widget%20Low%20Level.ipynb) documentation.\n", + "\n", + "To create a custom widget, you need to define the widget both in the browser and in the Python kernel.\n" ] }, { From cb18de1be840a55547029cd1ff163384529ccdbc Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 17 Feb 2021 11:00:55 +0100 Subject: [PATCH 16/16] Add the screenshot to the top of the tutorial --- docs/source/examples/Widget Custom.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/examples/Widget Custom.ipynb b/docs/source/examples/Widget Custom.ipynb index d803291e36..4dd640f11a 100644 --- a/docs/source/examples/Widget Custom.ipynb +++ b/docs/source/examples/Widget Custom.ipynb @@ -24,7 +24,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter: https://github.com/jupyter-widgets/widget-ts-cookiecutter" + "This tutorial shows how to build a simple email widget using the TypeScript widget cookiecutter: https://github.com/jupyter-widgets/widget-ts-cookiecutter\n", + "\n", + "![end-result](./images/custom-widget-result.png)" ] }, {