{ "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": [ "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)" ] }, { "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 jupyter-packaging cookiecutter nodejs yarn 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 as follows:\n", "\n", "```bash\n", "author_name []: Your Name\n", "author_email []: your@name.net\n", "github_project_name []: ipyemail\n", "github_organization_name []: \n", "python_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]: A Custom Email Widget\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 3.x:\n", "\n", "\n", "```bash\n", "# link your development version of the extension with JupyterLab\n", "jupyter labextension develop . --overwrite\n", "\n", "# rebuild extension Typescript source after making changes\n", "yarn run build\n", "```\n", "\n", "It is also possible to rebuild the widget automatically when there is a new change, using the `watch` script:\n", "\n", "```bash\n", "# watch the source directory in one terminal, automatically rebuilding when needed\n", "yarn run watch\n", "```\n", "\n", "\n", "If you are using JupyterLab 2.x, you will need to install the `@jupyter-widgets/jupyterlab-manager` extension manually:\n", "\n", "```bash\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", "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", "### 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 steps will walk you through how to modify the existing code to transform the widget into an email widget." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "## 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" ] }, { "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 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 does not need to be displayed, 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`.\n", "\n", "First let's rename `ipyemail/example.py` to `ipyemail/widget.py`.\n", "\n", "In `ipyemail/widget.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 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(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", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In `ipyemail/__init__.py`, change the import from:\n", "\n", "```python\n", "from .example import ExampleWidget\n", "```\n", "\n", "To:\n", "\n", "```python\n", "from .widget import Email\n", "```" ] }, { "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.\n", "\n", "