diff --git a/_quarto.yml b/_quarto.yml index 18a5d78e..266d3fe6 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -106,7 +106,7 @@ website: - icon: youtube href: https://www.youtube.com/playlist?list=PL9HYL-VRX0oRbLoj3FyL5zeASU5FMDgVe aria-label: Shiny YouTube Playlist - + sidebar: - id: components collapse-level: 2 @@ -189,6 +189,10 @@ website: - docs/ipywidgets.qmd - docs/ui-html.qmd - docs/workflow-server.qmd + - section: "Extending" + contents: + - docs/custom-component-one-off.qmd + - docs/custom-components-pkg.qmd - section: "Framework Comparisons" contents: - docs/comp-streamlit.qmd diff --git a/docs/custom-component-one-off.qmd b/docs/custom-component-one-off.qmd new file mode 100644 index 00000000..7a6e122d --- /dev/null +++ b/docs/custom-component-one-off.qmd @@ -0,0 +1,362 @@ +--- +title: "Custom JavaScript component" +--- + +In this post, you will learn how to create a custom element and accompanying output binding in Shiny. This is useful if you want to create an output that is not currently in Shiny for your app. + +:::{.callout-note} + +This post talks about making a one-off component for a given app. If you plan on reusing your component or distributing it for others to use, see the accompanying post on [making a custom component package.](custom-components-pkg.html) +::: + +:::{.callout-warning} +The code shown here is simplified to get the point across, but before you use it in your own app, you should make sure to add error handling and other features to make it robust. +::: + +# The problem + +You found a new table library that you really want to use in your Shiny app. The library is [Tabulator](https://tabulator.info/), which is a JavaScript library for making tables with data. But there's a problem: there's no way to easily use it from a Shiny app. To do this, we'll need to write some Python code that will let us use the library from the Python side of Shiny, and wrap the library's JavaScript code to make it talk to JavaScript side of Shiny. + +# The solution + +To implement a custom Tabulator element for your app, you'll need to write three things: + +1. A JavaScript script that renders the element on the client side using the Tabulator library +2. An `output_tabulator()` function for placing the element in your app's UI +3. A `render_tabulator()` decorator for passing table data to the JavaScript code rendering the element on the server side + + +## The JavaScript code + +First things first: to use a custom JavaScript library we need to write... some JavaScript. + +To do this we will create a new folder called `tabulator/` that has the following structure: + +:::{.callout-note} +This example uses plain JavaScript with no build step. For an example using typescript and with a build-step see the accompanying article on [making a custom component package.](custom-components-pkg.html) +::: + +``` +tabulator/ + tabulator_esm.min.js + tabulator.min.css + tableComponent.js +``` + +Both `tabulator_esm.min.js` and `tabulator.min.css` are downloaded from [tabulator's website.](https://tabulator.info/docs/5.5/install#sources-download) `tableComponent.js` is the script that we will write that contains the code for rendering the table to our Shiny app. + +:::{.callout-note} +The code in this article will be abbreviated to show the relevant parts. If you want to see the full code, see the [accompanying repo.](https://github.com/posit-dev/pyshiny-output-binding-example) +::: + + +To create an output binding in Shiny, we create a new instance of the `Shiny.OutputBinding` class. + +```{.javascript filename="tableComponent.js"} +class TabulatorOutputBinding extends Shiny.OutputBinding { + // Find element to render in + find(scope) { ... } + + // Render output element in the found element + renderValue(el, payload) { ... } +} + +// Register the binding +Shiny.outputBindings.register( + new TabulatorOutputBinding(), + "shiny-tabulator-output" +); +``` + +This class has two methods that we need to implement: `find()` and `renderValue()`. The `find()` method is used to identify the element that will contain the rendered table. The `renderValue()` method is used to render the table in the element. After making that class we need to register it with Shiny so it can find and send data to instances of our output. + +### The `find()` method + + +Now that we have the scaffolding set up we can start by filling in the `find` method. This function is passed a `scope` object, which is a `jQuery` selection and should return the element you wish to render your output into. + +```{.javascript filename="tableComponent.js"} +class TabulatorOutputBinding extends Shiny.OutputBinding { + find(scope) { + return scope.find(".shiny-tabulator-output"); + } + + renderValue(el, payload) {...} +} + +Shiny.outputBindings.register(...); +``` + +Note that we're using the class `".shiny-tabulator-output"` here to mark the element that we want to render the table in. This is the same class that we will use in our `output_tabulator()` function in our app's server code. You can use any valid CSS selector here, but it's common to use a class name that descibes the output. + +### The `renderValue()` method + +Next, we fill in the main logic for rendering our table in to the `renderValue` method. This method gets passed two arguments: `el`, which is an [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) identified by our find function, and `payload`, which is the data that the server has provided from the render function (more on this [soon](#the-render_tabulator-decorator)). + +```{.javascript filename="tableComponent.js"} +// Import the Tabulator library +import { Tabulator } from "./tabulator_esm.min.js"; + +class TabulatorOutputBinding extends Shiny.OutputBinding { + find(scope) { ... } + + renderValue(el, payload) { + // Unpack the info we get from the associated render function + const { columns, data, type_hints } = payload; + + // Convert the column names to a format that Tabulator expects + const columnsDef = columns.map((col, i) => { + return { + title: col, + field: col, + hozAlign: type_hints[i] === "numeric" ? "right" : "left", + }; + }); + + // Data comes in as a series of rows with each row having as many elements + // as there are columns in the data. We need to map this to a series of + // objects with keys corresponding to the column names. + function zipRowWithColumns(row) { + const obj = {}; + row.forEach((val, i) => { + obj[columns[i]] = val; + }); + return obj; + } + + // Instantiate a new Tabulator table in the element. + // This will also destroy any existing table in the element + // so we don't have to worry about adding and removing tables. + new Tabulator(el, { + data: data.map(zipRowWithColumns), + layout: "fitColumns", + columns: columnsDef, + }); + } +} + +Shiny.outputBindings.register(...); +``` + +The implementation of this function is not terribly important and draws directly from the [tabulator docs](https://tabulator.info/docs/5.5/quickstart). + +What matters is that we take our data, transform it in some way, and then instantiate our table with the `new Tabulator(el, {...})` call. In this case we take data in the form of the rows of a passed data frame, the column names, and the types of those columns (this is determined by [the render decorator](#the-render_tabulator-decorator)), and construct a js object in the form of `data = [{col1: foo1, col2: bar1, ...}, {col1: foo2, col2: bar2, ...}]`. We also combine the column names and types to create the `columnsDef` object that Tabulator expects. + +Don't worry too much about following this particular format because it will change depending on the component that you are wrapping. + +:::{.callout-note} + +This code relies on the `Shiny` object existing in the JavaScript context, but you may want to wrap all the above code in an `if (Shiny)` statement so it only runs if the `Shiny` object exists. This is useful if you're writing a component that might be used in a non-Shiny context because your code won't error out trying to access the non-existant `Shiny` variable and break the document. + +```{.javascript filename="tableComponent.js"} +if (Shiny) { + class TabulatorOutputBinding extends Shiny.OutputBinding { ... } + + Shiny.outputBindings.register(...); +} +``` +::: + +To see the full JavaScript code, see `tabulator/tableComponent.js` in the [accompanying repo.](https://github.com/posit-dev/pyshiny-output-binding-example/blob/main/tabulator/tableComponent.js) + +## The `output_tabulator()` function + +Next we need an HTML element to target with our JavaScript code. When we set up [the find method](#the-find-method) for our binding, we chose the class `shiny-tabulator-output` as the mark of a tabualtor output, so we need to add that class. We also need to allow the user to set the ID of the element so that Shiny knows which element to target with which output. By wrapping the `id` argument in `resolve_id()` we make sure it will work in the context of modules. We'll also add a height argument so that the user can set the height of the table. + +```{.python filename="app.py"} +from shiny import ui, App +from shiny.module import resolve_id + +from htmltools import HTMLDependency + +tabulator_dep = HTMLDependency( + "tabulator", + "5.5.2", + source={"subdir": "tabulator"}, + script={"src": "tableComponent.js", "type": "module"}, + stylesheet={"href": "tabulator.min.css"}, + all_files=True, +) + +def output_tabulator(id, height="200px"): + return ui.div( + tabulator_dep, + # Use resolve_id so that our component will work in a module + id=resolve_id(id), + class_="shiny-tabulator-output", + style=f"height: {height}", + ) +``` + + +:::{.callout-note} +We use the `HTMLDependency` function to bind up the assets needed for tabulator that we made in the previous step to make sure th at they're included in our app whenever the `output_tabulator()` function is called (but not more than once). + +Note the use of `all_files=True` here. This makes it so we can do the ESM import of the Tabulator library. Otherwise `tabulator_esm.min.js` would not be hosted and the JS library wouldn't be able to find it. +::: + +Now, the `output_tabulator()` function can be called anywhere we want to render a table in our app. + +## The `render_tabulator()` decorator + +Now we've got the client-side logic finished, we need to write a custom render decorator that sends our data into the component. + +A render function's job is to take the result of calling the decorated function, transform it into the format our client-side code wants (in many cases this may be as simple as just returning the object unchanged), and then returning that client-side friendly data which will be passed to our client's `renderValue()` method. + +To do this we can leverage some tools provided by Shiny in the `shiny.render.transformer` subpackage. + +```{.python filename="app.py"} +from shiny.render.transformer import ( + output_transformer, + resolve_value_fn, + TransformerMetadata, + ValueFn, +) + + +@output_transformer +async def render_tabulator( + _meta: TransformerMetadata, + _fn: ValueFn[pd.DataFrame | None], +): + res = await resolve_value_fn(_fn) + if res is None: + return None + + if not isinstance(res, pd.DataFrame): + # Throw an error if the value is not a dataframe + raise TypeError(f"Expected a pandas.DataFrame, got {type(res)}. ") + + # Get data from dataframe as a list of lists where each inner list is a + # row, column names as array of strings and types of each column as an + # array of strings + return { + "data": res.values.tolist(), + "columns": res.columns.tolist(), + "type_hints": res.dtypes.astype(str).tolist(), + } +``` +:::{.callout-note} +In the code above we use types so that we can get some type checking in our IDE, but these are not required. +::: + +The `output_transformer` decorator is a decorator factory (it's a decorator that creates decorators!) that takes a function that returns a dictionary of data to be passed to the client side. The function that it decorates is passed two arguments: `_meta` and `_fn`. + +`_meta` is a dictionary of metadata about the function that is being decorated. We don't use it in our example. + +`_fn` is the decorated function, i.e. the function that goes below the `@render_tabulator()` decorator in your app's server code. In this case we are expecting the function to return either a pandas dataframe or `None`. + + +```{.python} +... + res = await resolve_value_fn(_fn) +... +``` + +`resolve_value_fn()` is a helper provided in `shiny.render.transformer` for resolving the value of a function that may or may not be async. + + +:::{.callout-note} +# Why an asynchronous function? +It is required by Shiny that the output decorator function be `async`. This allows users of the render bindings to provide either synchronous or asynchronous functions to the decorator. This ensures that the function will work whether or not the end user has defined their render function asynchronously. + +If you don't need any async behavior you can simply write your function as you would a standard synchronous function after `await`ing `resolve_value_fn()`. +::: + + +Next, we check to make sure that the value returned by the function is a dataframe. If it's not, we throw an error. This is not strictly required, but is a best practice. + +```python +... +if not isinstance(res, pd.DataFrame): + # Throw an error if the value is not a dataframe + raise TypeError(f"Expected a pandas.DataFrame, got {type(res)}. ") +... +``` + +Finally, we return a dictionary of data that we want to pass to the client side. In this case we return the data as a list of lists, the column names as an array of strings, and the types of each column as an array of strings using methods provided by pandas. + +```python +... +return { + "data": res.values.tolist(), + "columns": res.columns.tolist(), + "type_hints": res.dtypes.astype(str).tolist(), +} +... +``` + +This returned value is then what gets sent to the client side and is available in the `payload` argument of the `renderValue()` method of our `TabulatorOutputBinding` class. + +# The result + +Now we have all the components neccesary to use our tabulator output component. Here's an app that uses it to render some number of rows of the indomitable `mtcars` dataset. + +```{.python filename="app.py"} +from shiny import ui, App +from pathlib import Path +import pandas as pd + +# Code for the custom output +... + +# App code +app_ui = ui.page_fluid( + ui.input_slider("n", "Number of rows to show", 1, 20, 10), + output_tabulator("tabulatorTable"), +) + + +def server(input, output, session): + @render_tabulator + def tabulatorTable(): + return pd.read_csv(Path(__file__).parent / "mtcars.csv").head(input.n()) + + +app = App(app_ui, server) +``` + +Which results in the following app: + + + + + + +:::{.sourceCode} +![Our app with custom Tabulator component.](./custom-components-result.png){fig-alt="Screenshot of resulting app" class="app-screenshot"} +::: + + +To see the full app script, see `app.py` in the accompanying repo for this post [here.](https://github.com/posit-dev/pyshiny-output-binding-example/blob/main/app.py) diff --git a/docs/custom-components-color-picker.png b/docs/custom-components-color-picker.png new file mode 100644 index 00000000..9e7b3d5d Binary files /dev/null and b/docs/custom-components-color-picker.png differ diff --git a/docs/custom-components-pkg-example-app.png b/docs/custom-components-pkg-example-app.png new file mode 100644 index 00000000..965da2f5 Binary files /dev/null and b/docs/custom-components-pkg-example-app.png differ diff --git a/docs/custom-components-pkg.qmd b/docs/custom-components-pkg.qmd new file mode 100644 index 00000000..5dd781a4 --- /dev/null +++ b/docs/custom-components-pkg.qmd @@ -0,0 +1,424 @@ +--- +title: Custom components package +filters: + - line-highlight +--- + + +While there are a large number of pre-built [components available for Shiny,](components/) there are times when you may want to create your own. In this article we’ll walk through the process of creating a custom input component package for Shiny. We’ll be using React and Typescript to build the component, but the process is similar for other languages and frameworks. + +::: {.callout-note} +If you just want to build a one-off component for a single app, a full package may be overkill. See the accompanying article [Custom JavaScript component](custom-component-one-off.html) for a simpler approach. +::: + +## What we’ll build + +The component we are going to build is a color picker that returns the hex-code of the chosen color as a string for the user to use in their app. The component will be built using React and Typescript and will be packaged as a python package that can be deployed to `pypi` and installed with `pip` so other users can easily use it in their apps. + +::: {.callout-note} +The example here uses typescript. If you don't want to use typescript, don't worry! Javascript works just fine. To make this example JavaScript you can simply erase the type annotations, or run the typescript compiler on the source code to strip them out automatically. +::: + +The component itself is based on the library [react-color](https://casesandberg.github.io/react-color/). We’ll be using the `SketchPicker` component from that library to build our custom component. The full code is as follows. + +```{.ts filename="srcts/index.tsx" } +import { SketchPicker } from "react-color"; +import React from "react"; + +function ColorPickerReact({ + initialValue, + onNewValue, +}: { + // The initial value for the color picker + initialValue: string; + // A callback that should be called whenever the color is changed + onNewValue: (x: string) => void; +}) { + const [currentColor, setCurrentColor] = React.useState(initialValue); + + return ( + { + setCurrentColor(color.hex); + onNewValue(color.hex); + }} + /> + ); +} + +``` + +:::{.sourceCode} +![Output of `ColorPickerReact` component](custom-components-color-picker.png){width=200px fig-alt="Screenshot of color picker component" class="app-screenshot pad-top"} +::: + + +Your component may look very different, but at the end of the day it just needs to be a self-contained react component. + +::: {.callout-tip} +## What about an output binding? + +This article touches on building an input component. However, it's also possible to build output components. The process and project structure is very similar to inputs. Throughout this article look for the "What about an output binding?" tips for more information on how to build an output binding. You can generate an output template with `shiny create -t js-output`. +::: + + +## The quick version + +If you just want to get up and running with the code, you can start with one of the available templates available with the `shiny create -t js-react` command and then run the commands provided after the template is created. You can also see the full list of JavaScript extension templates with `shiny create -t js-component`. + + + + +## Development workflow + +While there are lots of ways to develop components with live-feedback (e.g. Storybook, dev servers like `vite`, etc) an easy way to develop a component with our package structure is to use the example app, an editable mode pip install, and the watch mode for our build step. We can do this with the following steps: + +1. Install the package in [“editable mode”](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) with `pip install -e .` + +2. Run the bundler in watch mode with `npm run watch`. This will watch the `srcts` directory for changes and automatically rebuild the JavaScript when it detects a change. + +3. Run the example app in live-reload mode. If you're using VScode, the [Shiny for Python extension](https://marketplace.visualstudio.com/items?itemName=Posit.shiny-python) enables this automatically when pressing the run button above the app script. + +Now you can update your component JavaScript/python functions and your app will automatically reload with the changes. Happy developing! + +4. Once you're happy with your component, you can deploy to PyPi. For instructions on doing this see the [Python Packaging User Guide.](https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives) + +If you want to understand what's going on under the hood, read on! + +::: {.callout-note} +The component we're creating here uses React, but there are templates for building components with plain JavaScript as well. The general concepts we talk about here apply to all of the templates so feel free to use whichever one you prefer. +::: + + +## Project structure + +The template from above contains the color picker component above, along with all the scaffolding neccesary to build and package it as a python package.Let's take a look at the files that are created and talk through why they are there. + + + +```{.bash filename="my-color-picker/" } +├── package.json +├── package-lock.json +├── srcts +│   └── index.tsx +├── example-app +│   └── app.py +├── fancy_color_picker +│   ├── __init__.py +│   ├── distjs +│   │   └── index.js +│   └── fancy_color_picker.py +├── pyproject.toml +├── README.md +└── tsconfig.json +``` + + +### `package.json` + +This is the standard `package.json` file for a JavaScript project. It contains the dependencies and build commands for the JavaScript code. The important sections are: +- A dependency on the `shiny-bindings-react` package. This is a JavaScript package with helpers for making it easier to create input and output bindings using React. Later we use the function `makeReactInput()` from this package to make Shiny aware of the component and its role as an input binding. +- `build` command. The build command (and accompanying `watch` command) use `esbuild` to transpile the typescript to JavaScript and bundle the dependencies (like `shiny-bindings-react` and `react` itself. + +The `.package-lock.json` file is generated by npm and contains the exact versions of the dependencies used in the project. You shouldn't need to modify this file by hand. + +### `srcts/index.tsx` + +This is where all the typescript/JavaScript code lives. We talked about the react component - `ColorPickerReact` - above, but it's worth touching on the code that binds that react component with Shiny so it functions as an input: + +```ts +import { SketchPicker } from "react-color"; +import type { ColorResult } from "react-color"; +import React from "react"; + +import { makeReactInput } from "@posit-dev/shiny-bindings-react"; + +// Generates a new input binding that renders the supplied react component +// into the root of the webcomponent. +makeReactInput({ + tagName: "fancy-color-picker", + initialValue: "#fff", + renderComp: ({ initialValue, onNewValue }) => ( + onNewValue(color)} + /> + ), +}); + +// Color Picker React component +function ColorPickerReact(...) { ... } +``` + +In here we declare the `tagName` of for our component. This name is used to generate the correct markup with python later. Under the hood `makeReactInput()` generates a [webcomponent](https://developer.mozilla.org/en-US/docs/Web/Web_Components) to hold our input. A webcomponent is a custom HTML element that allows us to bind custom markup and JavaScript logic to a point in our app by simply writing the custom tag into the app's HTML. The `tagName` argument provided here represents the name of that custom component we're generating. In this case we're registering the component as ``. + +::: {.callout-note} +This example uses the webcomponent based interface provided by the [`@posit-dev/shiny-bindings-react` package,](https://github.com/posit-dev/shiny-bindings/blob/main/packages/react/README.md) but if you want lower level access you can use the built-in class-based interface. See the [Shiny for R documentation](https://shiny.posit.co/r/articles/build/building-inputs/) for more details. +::: + +Next we provide an `initialValue` for the component. This is the value that will be used when the component is first rendered. In this case we're using `#fff`. + +Finally, we provide a `renderComp` function. This function is called whenever the component needs to be rendered. It is passed an object with two properties, `onNewValue` and `initialValue`. `onNewValue` is a callback that should be called whenever the value of the component changes. In this case we're just passing the value of the color picker to the callback. This will send the value to Shiny and update the value of the input. `initialValue` is the value that should be used to initialize the component. In this case we're just passing the value we were given to the `ColorPickerReact` component. + + +::: {.callout-tip collapse="true"} +## What about an output binding? + +The component we're building here is an input. However, you may be interested in building an output binding. There is a template for this but the process is not too different. Just instead of using `makeReactInput()` you would use `makeReactOutput()`. Here's how we would do it for a simple output that displays a color: + +```{.ts filename="index.tsx"} +// Simple react output binding that renders a div with the background color +makeReactOutput<{ value: string }>({ + tagName: "fancy-color-shower", + renderComp: ({ value }) => ( +
+ ), +}); +``` +::: + +### `fancy_color_picker/` + + +#### `distjs/*` + +This is where the bundled JavaScript from `srcts` gets placed. You shouldn’t ever need to modify anything in here by hand. It will be automatically generated when you run `npm run build`. It is important to note the path though, as we will need to tell Shiny where to find this JavaScript when we declare the `HTMLDependency`... + + +#### `fancy_color_picker.py` + +```{.python} +from pathlib import PurePath +from htmltools import HTMLDependency, Tag +from shiny.module import resolve_id + +# This object is used to let Shiny know where the dependencies needed to run +# our component all live. In this case, we're just using a single JavaScript +# file but we could also include CSS. +fancy_color_picker_deps = HTMLDependency( + "fancy_color_picker", + "1.0.0", + source={ + "package": "fancy_color_picker", + "subdir": str(PurePath(__file__).parent / "distjs"), + }, + script={"src": "index.js", "type": "module"}, +) + + +def fancy_color_picker(id: str): + """ + A shiny input. + """ + return Tag( + # This is the name of the custom tag we created with our webcomponent + "fancy-color-picker", + fancy_color_picker_deps, + # Use resolve_id so that our component will work in a module + id=resolve_id(id), + ) +``` + + +This is the main python script for the package. It contains the code that tells Shiny about the component and how to render it. The important parts are: + +##### `fancy_color_picker_deps` + +```{.python } +fancy_color_picker_deps = HTMLDependency( + "fancy_color_picker", + "1.0.0", + source={ + "package": "fancy_color_picker", + "subdir": str(PurePath(__file__).parent / "distjs"), + }, + script={"src": "index.js", "type": "module"}, +) +``` + +This sets up an "html-dependency" for our component. HTMLDependencies are Shiny's way of keeping track of what resources are needed for the currently displayed elements. This html dependency is telling Shiny that whenever there is a `fancy_color_picker` on the page in an app, it needs to also have the bundled JavaScript at `distjs/index.js` as well. + +::: {.callout-note} +Here we just declare JavaScript dependencies, but you can also include style sheets with the `stylesheet` argument. +::: + + +##### `fancy_color_picker` + +```{.python} +def fancy_color_picker(id: str): + """ + A shiny input. + """ + return Tag( + # This is the name of the custom tag we created with our webcomponent + "fancy-color-picker", + fancy_color_picker_deps, + # Use resolve_id so that our component will work in a module + id=resolve_id(id), + ) +``` + +This is the actual UI function for our component. Aka the one that gets called by the user in their app’s UI to add our component to their app. + +Because `makeReactInput()` [works by creating a webcomponent](#srctsindex.tsx), to render our input we just need to pass the tag name we set up in the `tagName` argument to `makeReactInput().` Next, we pass the `fancy_color_picker_deps` html dependency we just made and the ID of the binding and we’re good to go! + +::: {.callout-note} +By using the `resolve_id(id)` function here when declaring our ID, we make sure that the component works [Shiny modules](docs/workflow-modules) where the ID of the component needs to be prefixed with the module name. +::: + + +::: {.callout-tip collapse="true"} +## What about an output binding? + +Like with the JavaScript, the process for setting up the python code for an output binding is not too different. Although there is a bit of extra work because we need to build both the ui _and_ server components. Here's how we would do that for the color shower output binding we defined above: + +```{.python filename="fancy_color_picker.py"} +@output_transformer() +async def render_color( + _meta: TransformerMetadata, + _fn: ValueFn[str | None], +): + res = await resolve_value_fn(_fn) + if res is None: + return None + + if not isinstance(res, str): + # Throw an error if the value is not a string + raise TypeError(f"Expected a string, got {type(res)}. ") + + # Send the results to the client. Make sure that this is a serializable + # object and matches what is expected in the JavaScript code. + return {"value": res} + + +def output_color(id: str): + """ + Show a color + """ + return Tag( + "fancy-color-shower", + fancy_color_picker_deps, + id=resolve_id(id), + ) +``` + +Make sure you add these to your `__init__.py` file so they are exposed to users of your package. Again, there is an output binding template in the `Shiny create` menu that can get you up and running quickly. +::: + +#### `__init__.py` + +```{.python} +from .fancy_color_picker import fancy_color_picker + +__all__ = [ + "fancy_color_picker", +] +``` + +This is how we tell python what functions/ variables our package exposes. In this case it’s a single function, `fancy_color_picker`. If you were to add more components you would also need to register them here for them to be importable by users in their apps. For more information on the structure of these files see the [python docs site.](https://packaging.python.org/tutorials/packaging-projects/#creating-the-package-files) + + +### `pyproject.toml` + +This file is used to tell python/pypi about our package. It contains the name of the package, the version, and the dependencies. A deep dive into the structure of this file is outside the scope of this article, but you can find more information in the [Python Packaging Authority docs.]https://packaging.python.org/en/latest/guides/writing-pyproject-toml/) + + +### `tsconfig.json` +This file is used to configure typescript, which we are using to write our component. Like the `pyproject.toml` file, a deep dive into the structure of this file is outside the scope of this article, but you can find more information in the [typescript docs.](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + + +### `example-app/app.py` + +```{.python} +from fancy_color_picker import fancy_color_picker + +from shiny import App, render, ui + +app_ui = ui.page_fluid( + fancy_color_picker("myComponent"), + ui.output_text("valueOut"), +) + +def server(input, output, session): + @render.text + def valueOut(): + return f"Value from input is {input.myComponent()}" + +app = App(app_ui, server) +``` + + +This is a simple example app that can be used to test the component while developing. It uses the `fancy_color_picker` function we defined in `fancy_color_picker.py` to add the component to the app. It also uses the `render.text` decorator to render the value of the input to the page. + +:::{.sourceCode} +![Example app running with color picker](custom-components-pkg-example-app.png){fig-alt="Screenshot of example app running" class="app-screenshot"} +::: + + +::: {.callout-tip collapse="true"} +## What about an output binding? + +In our output binding example we defined an output that conveniently _displays_ colors. If we were packaging up two components like this we could/should modify the example app to showcase both of them. + +```{.python filename="app.py"} +from fancy_color_picker import fancy_color_picker, output_color, render_color + +from shiny import App, ui + +app_ui = ui.page_fluid( + fancy_color_picker("myComponent"), + output_color("myColor"), +) + +def server(input, output, session): + @render_color + def myColor(): + return input.myComponent() + +app = App(app_ui, server) +``` + +::: + + + diff --git a/docs/custom-components-result.png b/docs/custom-components-result.png new file mode 100644 index 00000000..f7d29000 Binary files /dev/null and b/docs/custom-components-result.png differ