From c9235f916b70e14b9f3ed5cfce0795217f61befe Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 11:12:32 -0700 Subject: [PATCH 1/9] enhance docs --- docs/faq.md | 11 ++ docs/getting-started/quickstart.md | 2 +- docs/guides/debugging.md | 47 +++++++- docs/guides/event-handlers.md | 110 ++++++++++++++++++ docs/guides/interactivity.md | 62 ++++++---- docs/guides/layouts.md | 27 +++++ docs/guides/performance.md | 46 ++++++++ docs/guides/state-management.md | 14 +-- mesop/examples/__init__.py | 3 + .../boilerplate_free_event_handlers.py | 22 ++++ mkdocs.yml | 27 +++-- 11 files changed, 326 insertions(+), 45 deletions(-) create mode 100644 docs/guides/event-handlers.md create mode 100644 docs/guides/layouts.md create mode 100644 docs/guides/performance.md create mode 100644 mesop/examples/boilerplate_free_event_handlers.py diff --git a/docs/faq.md b/docs/faq.md index a761a7255..c68989a8c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -20,6 +20,17 @@ Although Mesop is pre-v1, we take backwards-compatibilty seriously and avoid bac Occasionally, we will do minor clean-up for our APIs, but we will provide warnings/deprecation notices and provide at least 1 release to migrate to the newer APIs. +### Which modules should I import from Mesop? + +Only import from these two modules: + +```py +import mesop as me +import mesop.labs as mel +``` + +All other modules are considered internal implementation details and may change without notice in future releases. + ### Is Mesop an official Google product? No, Mesop is not an official Google product and Mesop is a 20% project maintained by a small core team of Google engineers with contributions from the broader community. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 6cdb7835b..0500bb29b 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -34,6 +34,6 @@ If you make changes to the code, the Mesop app should be automatically hot reloa Learn more about the core concepts of Mesop as you learn how to build your own Mesop app: - + Core Concepts diff --git a/docs/guides/debugging.md b/docs/guides/debugging.md index 957ecf039..56d539cb1 100644 --- a/docs/guides/debugging.md +++ b/docs/guides/debugging.md @@ -1,8 +1,51 @@ # Debugging -VS Code is recomended for debugging your Mesop app, but you should be able to debug Mesop in other IDEs. +This guide will show you several ways of debugging your Mesop app: -## Debugging in VS Code +- [Debugging with server logs](#debugging-with-server-logs) +- [Debugging with Chrome DevTools](#debugging-with-chrome-devtools) +- [Debugging with VS Code](#debugging-with-vs-code) + +You can use the first two methods to debug your Mesop app both locally and in production, and the last one to debug your Mesop app locally. + +## Debugging with server logs + +If your Mesop app is not working properly, we recommend checking the server logs first. + +If you're running Mesop locally, you can check the terminal. If you're running Mesop in production, you will need to use your cloud provider's console to check the logs. + +## Debugging with Chrome DevTools + +[Chrome DevTools](https://developer.chrome.com/docs/devtools) is a powerful set of web developer tools built directly into the Google Chrome browser. It can be incredibly useful for debugging Mesop applications, especially when it comes to inspecting the client-server interactions. + +Here's how you can use Chrome DevTools to debug your Mesop app: + +1. Open your Mesop app in Google Chrome. + +1. Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut top open Chrome DevTools: + - Windows/Linux: Ctrl + Shift + I + - macOS: Cmd + Option + I + +1. To debug general errors: + - Go to the Console tab. + - Look for any console error messages. + - You can also modify the [log levels](https://developer.chrome.com/docs/devtools/console/reference#level) to display Mesop debug logs by clicking on "Default levels" and selecting "Verbose". + +1. To debug network issues: + - Go to the [Network tab](https://developer.chrome.com/docs/devtools/network/overview). + - Reload your page to see all network requests. + - Look for any failed requests (they'll be in red). + - Click on a request to see detailed information about headers, response, etc. + +1. For JavaScript errors: + - Check the Console tab for any error messages. + - You can set breakpoints in your JavaScript code using the Sources tab. + +Remember, while Mesop abstracts away much of the frontend complexity, using these tools can still be valuable for debugging and optimizing your app's performance. + +## Debugging with VS Code + +VS Code is recomended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs. **Pre-requisite:** ensure VS Code is downloaded. diff --git a/docs/guides/event-handlers.md b/docs/guides/event-handlers.md new file mode 100644 index 000000000..d8cc331ef --- /dev/null +++ b/docs/guides/event-handlers.md @@ -0,0 +1,110 @@ +# Event Handlers + +Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions (callbacks). + +## How it works + +Let's take a look at a simple example of an event handler: + +```py title="Simple event handler" +def counter(): + me.button("Increment", on_click=on_click) + +def on_click(event: me.ClickEvent): + state = me.state(State) + state.count += 1 + +@me.stateclass +class State: + count: int = 0 +``` + +Although this example looks simple, there's a lot going on under the hood. + +When the counter function is called, it creates an instance of the button component and binds the `on_click` event handler to it. Because components (and the entire Mesop UI) is serialized and sent to the client, we need a way of serializing the event handler so that when the button is clicked, the correct event handler is called on the server. + +We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function. + +Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page made be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault tolerant and allows for easy scaling of the app. + +## Patterns + +### Reusing event handler logic + +You can share event handler logic by extracting the common logic into a separate function. For example, you will often want to use the same logic for the `on_enter` event handler for an input component and the `on_click` event handler for a "send" button component. + +```py title="Reusing event handler logic" +def on_enter(event: me.InputEnterEvent): + state = me.state(State) + state.value = event.value + call_api() + +def on_click(event: me.ClickEvent): + # Assumes that state.value has been set by an on_blur event handler + call_api() + +def call_api(): + # Put your common event handler logic here + pass +``` + +### Boilerplate-free event handlers + +If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern: + +```py title="Boilerplate-free event handlers" +def app(): + me.input(label="Name", key="name", on_blur=update_state) + me.input(label="Address", key="address", on_blur=update_state) + +@me.stateclass +class State: + name: str + address: str + +def update_state(event: me.InputBlurEvent): + state = me.state(State) + setattr(state, event.key, event.value) +``` + +The downside with this approach is that you lose type-safety. Generally, defining a separate event handler, although more verbose, is easier to maintain. + +## Troubleshooting + +### Avoid using closure variables in event handler + +One subtle mistake when building a reusable component is to have the event handler use a closure variable like the following example: + +```py title="Bad example of using closure variable" +@me.component +def link_component(url: str): + def on_click(event: me.ClickEvent): + me.navigate(url) + return me.button(url, on_click=on_click) + +def app(): + link_component("/1") + link_component("/2") +``` + +The problem with this above example is that Mesop only stores the last event handler. This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior. + +Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example: + +```py title="Good example of using key" +@me.component +def link_component(url: str): + def on_click(event: me.ClickEvent): + me.navigate(event.key) + return me.button(url, key=url, on_click=on_click) +``` + +For more info on using component keys, please refer to the [Component Key docs](../components/index.md#component-key). + +## Next steps + +Explore advanced interactivity patterns like streaming and async: + + + Interactivity + diff --git a/docs/guides/interactivity.md b/docs/guides/interactivity.md index 148b9e299..f9fb733cc 100644 --- a/docs/guides/interactivity.md +++ b/docs/guides/interactivity.md @@ -44,7 +44,7 @@ If you notice a race condition with user input (e.g. [input](../components/input See the following example using this **anti-pattern** :warning:: -```py title="Bad example" +```py title="Bad example: setting the value and using on_input" @me.stateclass class State: input_value: str @@ -63,50 +63,62 @@ The problem is that the input value now has a race condition because it's being 1. The server is setting the input value based on state. 2. The client is setting the input value based on what the user is typing. -The way to fix this is by *not* setting the input value from the server. +There's several ways to fix this which are shown below. -The above example **corrected** would look like this :white_check_mark:: +#### Option 1: Use `on_blur` instead of `on_input` -```py title="Good example" hl_lines="7" +You can use the `on_blur` event instead of `on_input` to only update the input value when the user loses focus on the input field. + +This is also more performant because it sends much fewer network requests. + +```py title="Bad example: setting the value and using on_input" @me.stateclass class State: input_value: str def app(): state = me.state(State) - me.input(on_input=on_input) + me.input(value=state.input_value, on_input=on_input) def on_input(event: me.InputEvent): state = me.state(State) state.input_value = event.value ``` -### Avoid using closure variables in event handler +#### Option 2: Do not set the input value from the server -One subtle mistake when building a reusable component is to have the event handler use a closure variable like the following example: +If you don't need to set the input value from the server, then you can remove the `value` attribute from the input component. -```py title="Bad example of using closure variable" -@me.component -def link_component(url: str): - def on_click(event: me.ClickEvent): - me.navigate(url) - return me.button(url, on_click=on_click) +```py title="Good example: not setting the value" hl_lines="7" +@me.stateclass +class State: + input_value: str def app(): - link_component("/1") - link_component("/2") + state = me.state(State) + me.input(on_input=on_input) + +def on_input(event: me.InputEvent): + state = me.state(State) + state.input_value = event.value ``` -The problem with this above example is that Mesop only stores the last event handler. This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior. +#### Option 3: Use two separate variables for initial and current input value -Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example: +If you need set the input value from the server *and* you need to use `on_input`, then you can use two separate variables for the initial and current input value. -```py title="Good example of using key" -@me.component -def link_component(url: str): - def on_click(event: me.ClickEvent): - me.navigate(event.key) - return me.button(url, key=url, on_click=on_click) -``` +```py title="Good example: using two separate variables for initial and current input value" hl_lines="9" +@me.stateclass +class State: + initial_input_value: str = "initial_value" + current_input_value: str -For more info on using component keys, please refer to the [Component Key docs](../components/index.md#component-key). +@me.page() +def app(): + state = me.state(State) + me.input(value=state.initial_input_value, on_input=on_input) + +def on_input(event: me.InputEvent): + state = me.state(State) + state.current_input_value = event.value +``` diff --git a/docs/guides/layouts.md b/docs/guides/layouts.md new file mode 100644 index 000000000..5c72a2331 --- /dev/null +++ b/docs/guides/layouts.md @@ -0,0 +1,27 @@ +# Layouts + +Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app. Instead, it provides a set of tools to help you build your app's layout. + +## Basics + +The crux of doing layouts in Mesop is the [Box component](../components/box.md) and using the [Style API](../api/style.md) which are Pythonic wrappers around the CSS layout model. + +### Rows and columns + +```python title="Row" +import mesop as me + +@me.page() +def row(): + with me.box(style=me.Style(display="flex", flex_direction="row")): + me.text("Left") + me.text("Right") +``` + +### Grids + +... + +## Examples + +... diff --git a/docs/guides/performance.md b/docs/guides/performance.md new file mode 100644 index 000000000..8a2b371fb --- /dev/null +++ b/docs/guides/performance.md @@ -0,0 +1,46 @@ +# Performance + +Occasionally, you may run into performance issues with your Mesop app. Here are some tips to help you improve your app's performance. + +## Determine the root cause + +The first step in debugging performance issues is to identify the cause of the issue. Follow the [Debugging with DevTools guide](./debugging.md#debugging-with-devtools) and use the Console and Network tab to identify the issue. + +## Common issues + +### State is too large + +If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large. + +Because the state class is serialized and sent back and forth between the client and server, you should try to keep the state class reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app. + +The following are recommendations to help you avoid large state payloads: + +#### Store state in memory + +Mesop has a feature that allows you to store state in memory rather than passing the +full state on every request. This may help improve performance when dealing with large +state objects. The caveat is that storing state in memory contains its own set of +problems that you must carefully consider. See the [config section](../api/config.md#mesop_state_session_backend) +for details on how to use this feature. + +If you are running Mesop on a single replica or you can enable [session affinity](https://cloud.google.com/run/docs/configuring/session-affinity), then this is a good option. + +#### Store state externally + +You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store in them in a bucket service like [Google Cloud Storage](https://cloud.google.com/storage) and send [signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls) to the client so that it can directly fetch the images without going through the Mesop server. + +### Too many users + +If you notice that your Mesop app is running slowly when you have many concurrent users, you can try to scale your Mesop app. + +#### Use Cloud Run + +Cloud Run is a managed Google Cloud service that can scale your Mesop app to handle more concurrent users. +You can use the [autoscaling feature](https://cloud.google.com/run/docs/about-instance-autoscaling) to scale your Mesop app up and down based on the traffic to your app. Follow Mesop's [Cloud Run deployment guide](./deployment.md#deploying-to-cloud-run) to deploy your Mesop app to Cloud Run. + +You can also use other Cloud services which provide autoscaling features. + +#### Adjust your gunicorn settings + +If you're using [gunicorn](https://docs.gunicorn.org/) to serve your Mesop app, you can adjust gunicorn settings to [increase the number of workers](https://docs.gunicorn.org/en/latest/design.html#how-many-workers). This can help to increase the number of concurrent users your Mesop app can handle. diff --git a/docs/guides/state-management.md b/docs/guides/state-management.md index 5edf9dbea..a599740db 100644 --- a/docs/guides/state-management.md +++ b/docs/guides/state-management.md @@ -176,12 +176,12 @@ If you didn't explicitly annotate NestedState as a dataclass, then you would get ### State performance issues -Because the state class is serialized and sent back and forth between the client and server, you should try to keep the state class reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app. Instead, you should try to store large data outside of the state class (e.g. in-memory, filesystem, database, external service) and retrieve the data as needed for rendering. +Take a look at the [performance guide](./performance.md#state-is-too-large) to learn how to identify and fix State-related performance issues. -#### Storing state in memory +## Next steps -Mesop has a feature that allows you to store state in memory rather than passing the -full state on every request. This may help improve performance when dealing with large -state objects. The caveat is that storing state in memory contains its own set of -problems that you must carefully consider. See the [config section](../api/config.md#mesop_state_session_backend) -for details on how to use this feature. +Event handlers complement state management by providing a way to update your state in response to user interactions. + + + Event handlers + diff --git a/mesop/examples/__init__.py b/mesop/examples/__init__.py index cfb2f12d2..487d02a47 100644 --- a/mesop/examples/__init__.py +++ b/mesop/examples/__init__.py @@ -3,6 +3,9 @@ allowed_iframe_parents as allowed_iframe_parents, ) from mesop.examples import async_await as async_await +from mesop.examples import ( + boilerplate_free_event_handlers as boilerplate_free_event_handlers, +) from mesop.examples import box as box from mesop.examples import buttons as buttons from mesop.examples import checkbox_and_radio as checkbox_and_radio diff --git a/mesop/examples/boilerplate_free_event_handlers.py b/mesop/examples/boilerplate_free_event_handlers.py new file mode 100644 index 000000000..0d4426bd0 --- /dev/null +++ b/mesop/examples/boilerplate_free_event_handlers.py @@ -0,0 +1,22 @@ +import mesop as me + + +@me.page(path="/examples/boilerplate_free_event_handlers") +def page(): + state = me.state(State) + me.text("Boilerplate-free event handlers") + me.input(label="Name", key="name", on_blur=update_state) + me.input(label="Address", key="address", on_blur=update_state) + me.text(f"Name: {state.name}") + me.text(f"Address: {state.address}") + + +@me.stateclass +class State: + name: str + address: str + + +def update_state(event: me.InputBlurEvent): + state = me.state(State) + setattr(state, event.key, event.value) diff --git a/mkdocs.yml b/mkdocs.yml index d7a44b614..63b5099ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,16 +16,23 @@ nav: - 4 - Integrating AI APIs: codelab/4.md - 5 - Wrapping it up: codelab/5.md - Guides: - - State Management: guides/state-management.md - - Interactivity: guides/interactivity.md - - Multi-Pages: guides/multi-pages.md - - Auth: guides/auth.md - - Deployment: guides/deployment.md - - Debugging: guides/debugging.md - - Theming: guides/theming.md - - Web Security: guides/web-security.md - - Labs: guides/labs.md - - Testing: guides/testing.md + - Fundamentals: + - State Management: guides/state-management.md + - Event Handlers: guides/event-handlers.md + - Interactivity: guides/interactivity.md + - Layouts: guides/layouts.md + - Enhancements: + - Multi-Pages: guides/multi-pages.md + - Auth: guides/auth.md + - Theming: guides/theming.md + - Development: + - Debugging: guides/debugging.md + - Testing: guides/testing.md + - Labs: guides/labs.md + - Production: + - Deployment: guides/deployment.md + - Performance: guides/performance.md + - Web Security: guides/web-security.md - Components: - Types: - Overview: components/index.md From fbd1221befc55ed975da8f78c8407913abd47a71 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 11:13:25 -0700 Subject: [PATCH 2/9] Created using Colab --- notebooks/mesop_layout_colab.ipynb | 351 +++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 notebooks/mesop_layout_colab.ipynb diff --git a/notebooks/mesop_layout_colab.ipynb b/notebooks/mesop_layout_colab.ipynb new file mode 100644 index 000000000..d7309eb92 --- /dev/null +++ b/notebooks/mesop_layout_colab.ipynb @@ -0,0 +1,351 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "private_outputs": true, + "toc_visible": true, + "authorship_tag": "ABX9TyNwHyYFeXQEa85fsB5yTiTt", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Mesop Layout\n", + "\n", + "- https://github.com/google/mesop\n", + "- https://google.github.io/mesop/guides/layout\n", + "\n", + "Mesop is a Python-based UI framework that allows you to rapidly build web apps like demos and internal apps. This Colab walks you through common layout patterns." + ], + "metadata": { + "id": "8r6nOL0xPfyU" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Getting Started" + ], + "metadata": { + "id": "ZCtrRuMdPpJG" + } + }, + { + "cell_type": "code", + "source": [ + "!pip install mesop" + ], + "metadata": { + "id": "buRfwUlqPons" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "import mesop.labs as mel\n", + "\n", + "me.colab_run()" + ], + "metadata": { + "id": "hnyh1SX_P1XV" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Rows & Columns\n" + ], + "metadata": { + "id": "jNb8UJbpPwNj" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Row\n", + "\n", + "This is a basic row" + ], + "metadata": { + "id": "EKtdsEFKP_te" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4NGHpjd5PZld" + }, + "outputs": [], + "source": [ + "@me.page(path=\"/row\")\n", + "def row():\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\")):\n", + " me.text(\"Left\")\n", + " me.text(\"Right\")\n", + "\n", + "me.colab_show(path=\"/row\", height=100)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Row with spacing" + ], + "metadata": { + "id": "799y-fhGB2h2" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/row-with-spacing\")\n", + "def row():\n", + " # Try using \"space-between\" instead of \"space-around\"\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", justify_content=\"space-around\")):\n", + " me.text(\"Left\")\n", + " me.text(\"Right\")\n", + "\n", + "me.colab_show(path=\"/row-with-spacing\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "cJApeWgoB08H" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Row with alignment" + ], + "metadata": { + "id": "amrmzDjUCZsi" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/row-with-alignment\")\n", + "def row():\n", + " # Try commenting out align_items=\"center\" and see what it looks like\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", align_items=\"center\")):\n", + " me.box(style=me.Style(background=\"red\", height=50, width=\"50%\"))\n", + " me.box(style=me.Style(background=\"blue\", height=100, width=\"50%\"))\n", + "\n", + "me.colab_show(path=\"/row-with-alignment\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "6ZgQCDt5CcY7" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Grid" + ], + "metadata": { + "id": "soEezch7DAxT" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Side-by-side" + ], + "metadata": { + "id": "m9I06DnsDD-E" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/grid-sxs\")\n", + "def row():\n", + " # 1fr means 1 fraction, so each side is the same size.\n", + " # Try changing one of the 1fr to 2fr and see what it looks like\n", + " with me.box(style=me.Style(display=\"grid\", grid_template_columns=\"1fr 1fr\")):\n", + " me.text(\"A bunch of text\")\n", + " me.text(\"Some more text\")\n", + "\n", + "me.colab_show(path=\"/grid-sxs\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "m2fIwI5IDDim" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Header - Body - Footer layout" + ], + "metadata": { + "id": "A1whheFYDXEX" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/grid-header-body-footer\")\n", + "def app():\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_rows=\"auto 1fr auto\",\n", + " height=\"100%\"\n", + " )):\n", + " # Header\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24)\n", + " )):\n", + " me.text(\"Header\")\n", + "\n", + " # Body\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Body Content\")\n", + " # Add more body content here\n", + "\n", + " # Footer\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24)\n", + " )):\n", + " me.text(\"Footer\")\n", + "\n", + "me.colab_show(path=\"/grid-header-body-footer\", height=400)" + ], + "metadata": { + "id": "ZP7l_mMQDWoK" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Sidebar layout" + ], + "metadata": { + "id": "rxyuPKCeEmUo" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/sidebar-layout\")\n", + "def app():\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_columns=\"250px 1fr\",\n", + " height=\"100%\"\n", + " )):\n", + " # Sidebar\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Sidebar\")\n", + "\n", + " # Main content\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Main Content\")\n", + "\n", + "me.colab_show(path=\"/sidebar-layout\", height=400)" + ], + "metadata": { + "id": "nXhALOkrEnmp" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Responsive UI\n", + "\n", + "This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes." + ], + "metadata": { + "id": "hRz2iKGGFBfI" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/responsive-ui\")\n", + "def app():\n", + " is_desktop = me.viewport_size().width > 640\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_columns=\"250px 1fr\" if is_desktop else \"1fr\",\n", + " height=\"100%\"\n", + " )):\n", + " if is_desktop:\n", + " # Sidebar\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Sidebar\")\n", + "\n", + " # Main content\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Main Content\")\n", + "\n", + "me.colab_show(path=\"/responsive-ui\", height=400)" + ], + "metadata": { + "id": "GLdBlUtNFFiw" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file From 80327f33495e1d94dd3853dc7d7f0a8b40eef849 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 11:22:31 -0700 Subject: [PATCH 3/9] Created using Colab --- mesop_layout_colab.ipynb | 389 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 mesop_layout_colab.ipynb diff --git a/mesop_layout_colab.ipynb b/mesop_layout_colab.ipynb new file mode 100644 index 000000000..645221ef5 --- /dev/null +++ b/mesop_layout_colab.ipynb @@ -0,0 +1,389 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "private_outputs": true, + "toc_visible": true, + "authorship_tag": "ABX9TyOsU4T51ozrD5ZHXMc8bXrr", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Mesop Layout\n", + "\n", + "- https://github.com/google/mesop\n", + "- https://google.github.io/mesop/guides/layout\n", + "\n", + "Mesop is a Python-based UI framework that allows you to rapidly build web apps like demos and internal apps. This Colab walks you through common layout patterns." + ], + "metadata": { + "id": "8r6nOL0xPfyU" + } + }, + { + "cell_type": "markdown", + "source": [ + "# Getting Started" + ], + "metadata": { + "id": "ZCtrRuMdPpJG" + } + }, + { + "cell_type": "code", + "source": [ + "!pip install mesop" + ], + "metadata": { + "id": "buRfwUlqPons" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "import mesop.labs as mel\n", + "\n", + "me.colab_run()" + ], + "metadata": { + "id": "hnyh1SX_P1XV" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Rows & Columns\n" + ], + "metadata": { + "id": "jNb8UJbpPwNj" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Row\n", + "\n", + "This is a basic row" + ], + "metadata": { + "id": "EKtdsEFKP_te" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4NGHpjd5PZld" + }, + "outputs": [], + "source": [ + "@me.page(path=\"/row\")\n", + "def row():\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\")):\n", + " me.text(\"Left\")\n", + " me.text(\"Right\")\n", + "\n", + "me.colab_show(path=\"/row\", height=100)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Row with spacing" + ], + "metadata": { + "id": "799y-fhGB2h2" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/row-with-spacing\")\n", + "def row():\n", + " # Try using \"space-between\" instead of \"space-around\"\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", justify_content=\"space-around\")):\n", + " me.text(\"Left\")\n", + " me.text(\"Right\")\n", + "\n", + "me.colab_show(path=\"/row-with-spacing\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "cJApeWgoB08H" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Row with alignment" + ], + "metadata": { + "id": "amrmzDjUCZsi" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/row-with-alignment\")\n", + "def row():\n", + " # Try commenting out align_items=\"center\" and see what it looks like\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", align_items=\"center\")):\n", + " me.box(style=me.Style(background=\"red\", height=50, width=\"50%\"))\n", + " me.box(style=me.Style(background=\"blue\", height=100, width=\"50%\"))\n", + "\n", + "me.colab_show(path=\"/row-with-alignment\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "6ZgQCDt5CcY7" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Row & Columns" + ], + "metadata": { + "id": "F_tnJQYNG-Lk" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/row-and-columns\")\n", + "def app():\n", + " with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", gap=16, height=\"100%\")):\n", + " column(1)\n", + " column(2)\n", + " column(3)\n", + "\n", + "def column(num: int):\n", + " with me.box(style=me.Style(\n", + " flex_grow=1,\n", + " background=\"#e0e0e0\",\n", + " padding=me.Padding.all(16),\n", + " display=\"flex\",\n", + " flex_direction=\"column\",\n", + " )):\n", + " me.box(style=me.Style(background=\"red\", height=100))\n", + " me.box(style=me.Style(background=\"blue\", flex_grow=1))\n", + "\n", + "me.colab_show(path=\"/row-and-columns\", height=300)" + ], + "metadata": { + "id": "OE4arFYIG9ik" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Grid" + ], + "metadata": { + "id": "soEezch7DAxT" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Side-by-side" + ], + "metadata": { + "id": "m9I06DnsDD-E" + } + }, + { + "cell_type": "code", + "source": [ + "@me.page(path=\"/grid-sxs\")\n", + "def row():\n", + " # 1fr means 1 fraction, so each side is the same size.\n", + " # Try changing one of the 1fr to 2fr and see what it looks like\n", + " with me.box(style=me.Style(display=\"grid\", grid_template_columns=\"1fr 1fr\")):\n", + " me.text(\"A bunch of text\")\n", + " me.text(\"Some more text\")\n", + "\n", + "me.colab_show(path=\"/grid-sxs\", height=100, width=\"50%\")" + ], + "metadata": { + "id": "m2fIwI5IDDim" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Header - Body - Footer layout" + ], + "metadata": { + "id": "A1whheFYDXEX" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/grid-header-body-footer\")\n", + "def app():\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_rows=\"auto 1fr auto\",\n", + " height=\"100%\"\n", + " )):\n", + " # Header\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24)\n", + " )):\n", + " me.text(\"Header\")\n", + "\n", + " # Body\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Body Content\")\n", + " # Add more body content here\n", + "\n", + " # Footer\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24)\n", + " )):\n", + " me.text(\"Footer\")\n", + "\n", + "me.colab_show(path=\"/grid-header-body-footer\", height=400)" + ], + "metadata": { + "id": "ZP7l_mMQDWoK" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## Sidebar layout" + ], + "metadata": { + "id": "rxyuPKCeEmUo" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/sidebar-layout\")\n", + "def app():\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_columns=\"250px 1fr\",\n", + " height=\"100%\"\n", + " )):\n", + " # Sidebar\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Sidebar\")\n", + "\n", + " # Main content\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Main Content\")\n", + "\n", + "me.colab_show(path=\"/sidebar-layout\", height=400)" + ], + "metadata": { + "id": "nXhALOkrEnmp" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Responsive UI\n", + "\n", + "This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes." + ], + "metadata": { + "id": "hRz2iKGGFBfI" + } + }, + { + "cell_type": "code", + "source": [ + "import mesop as me\n", + "\n", + "@me.page(path=\"/responsive-ui\")\n", + "def app():\n", + " is_desktop = me.viewport_size().width > 640\n", + " with me.box(style=me.Style(\n", + " display=\"grid\",\n", + " grid_template_columns=\"250px 1fr\" if is_desktop else \"1fr\",\n", + " height=\"100%\"\n", + " )):\n", + " if is_desktop:\n", + " # Sidebar\n", + " with me.box(style=me.Style(\n", + " background=\"#f0f0f0\",\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Sidebar\")\n", + "\n", + " # Main content\n", + " with me.box(style=me.Style(\n", + " padding=me.Padding.all(24),\n", + " overflow_y=\"auto\"\n", + " )):\n", + " me.text(\"Main Content\")\n", + "\n", + "me.colab_show(path=\"/responsive-ui\", height=400)" + ], + "metadata": { + "id": "GLdBlUtNFFiw" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file From 18edc0a600125e63621245ecf7eeef31b3413f13 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 11:38:26 -0700 Subject: [PATCH 4/9] fine --- docs/guides/interactivity.md | 8 ++ docs/guides/layouts.md | 171 ++++++++++++++++++++++-- mesop_layout_colab.ipynb | 248 +++++++++++++++++------------------ 3 files changed, 293 insertions(+), 134 deletions(-) diff --git a/docs/guides/interactivity.md b/docs/guides/interactivity.md index f9fb733cc..625012bf0 100644 --- a/docs/guides/interactivity.md +++ b/docs/guides/interactivity.md @@ -122,3 +122,11 @@ def on_input(event: me.InputEvent): state = me.state(State) state.current_input_value = event.value ``` + +## Next steps + +Learn about layouts to build a customized UI. + + + Layouts + diff --git a/docs/guides/layouts.md b/docs/guides/layouts.md index 5c72a2331..a49e41712 100644 --- a/docs/guides/layouts.md +++ b/docs/guides/layouts.md @@ -1,27 +1,178 @@ # Layouts -Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app. Instead, it provides a set of tools to help you build your app's layout. +Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app so you can build custom layouts. The crux of doing layouts in Mesop is the [Box component](../components/box.md) and using the [Style API](../api/style.md) which are Pythonic wrappers around the CSS layout model. -## Basics +For most Mesop apps, you will use some combination of these types of layouts: -The crux of doing layouts in Mesop is the [Box component](../components/box.md) and using the [Style API](../api/style.md) which are Pythonic wrappers around the CSS layout model. +- [Rows and Columns](#rows-and-columns) +- [Grids](#grids) -### Rows and columns +## Common layout examples -```python title="Row" -import mesop as me +To interact with the examples below, open the Mesop Layouts Colab: [![Open In Colab](../assets/colab.svg)](https://colab.research.google.com/github/google/mesop/blob/main/notebooks/mesop_layout_colab.ipynb) -@me.page() +### Rows and Columns + +#### Basic Row + +```python title="Basic Row" def row(): with me.box(style=me.Style(display="flex", flex_direction="row")): me.text("Left") me.text("Right") ``` +#### Row with Spacing + +```python title="Row with Spacing" +def row(): + with me.box(style=me.Style(display="flex", flex_direction="row", justify_content="space-around")): + me.text("Left") + me.text("Right") +``` + +#### Row with Alignment + +```python title="Row with Alignment" +def row(): + with me.box(style=me.Style(display="flex", flex_direction="row", align_items="center")): + me.box(style=me.Style(background="red", height=50, width="50%")) + me.box(style=me.Style(background="blue", height=100, width="50%")) +``` + +#### Rows and Columns + +```python title="Rows and Columns" +def app(): + with me.box(style=me.Style(display="flex", flex_direction="row", gap=16, height="100%")): + column(1) + column(2) + column(3) + +def column(num: int): + with me.box(style=me.Style( + flex_grow=1, + background="#e0e0e0", + padding=me.Padding.all(16), + display="flex", + flex_direction="column", + )): + me.box(style=me.Style(background="red", height=100)) + me.box(style=me.Style(background="blue", flex_grow=1)) +``` + ### Grids -... +#### Side-by-side Grid + +```python title="Side-by-side Grid" +def grid(): + # 1fr means 1 fraction, so each side is the same size. + # Try changing one of the 1fr to 2fr and see what it looks like + with me.box(style=me.Style(display="grid", grid_template_columns="1fr 1fr")): + me.text("A bunch of text") + me.text("Some more text") +``` + +#### Header Body Footer Grid + +```python title="Header Body Footer Grid" +def app(): + with me.box(style=me.Style( + display="grid", + grid_template_rows="auto 1fr auto", + height="100%" + )): + # Header + with me.box(style=me.Style( + background="#f0f0f0", + padding=me.Padding.all(24) + )): + me.text("Header") + + # Body + with me.box(style=me.Style( + padding=me.Padding.all(24), + overflow_y="auto" + )): + me.text("Body Content") + # Add more body content here + + # Footer + with me.box(style=me.Style( + background="#f0f0f0", + padding=me.Padding.all(24) + )): + me.text("Footer") +``` + +#### Sidebar Layout + +```python title="Sidebar Layout" +def app(): + with me.box(style=me.Style( + display="grid", + grid_template_columns="250px 1fr", + height="100%" + )): + # Sidebar + with me.box(style=me.Style( + background="#f0f0f0", + padding=me.Padding.all(24), + overflow_y="auto" + )): + me.text("Sidebar") + + # Main content + with me.box(style=me.Style( + padding=me.Padding.all(24), + overflow_y="auto" + )): + me.text("Main Content") +``` + +#### Responsive UI + +This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes. + +```python +def app(): + is_desktop = me.viewport_size().width > 640 + with me.box(style=me.Style( + display="grid", + grid_template_columns="250px 1fr" if is_desktop else "1fr", + height="100%" + )): + if is_desktop: + # Sidebar + with me.box(style=me.Style( + background="#f0f0f0", + padding=me.Padding.all(24), + overflow_y="auto" + )): + me.text("Sidebar") + + # Main content + with me.box(style=me.Style( + padding=me.Padding.all(24), + overflow_y="auto" + )): + me.text("Main Content") +``` + +## Learn more + +For a real-world example of using these types of layouts, check out the Mesop Showcase app: + +- [App](https://google.github.io/mesop/showcase/) +- [Code](https://github.com/google/mesop/blob/main/showcase/main.py) + +To learn more about flexbox layouts (rows and columns), check out: + +- [CSS Tricks Guide to Flexbox Layouts](https://css-tricks.com/snippets/css/a-guide-to-flexbox/#aa-flexbox-properties) +- [MDN Flexbox guide](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox) -## Examples +To learn more about grid layouts, check out: -... +- [CSS Tricks Guide to Grid Layouts](https://css-tricks.com/snippets/css/complete-guide-grid/) +- [MDN Grid guide](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Grids) diff --git a/mesop_layout_colab.ipynb b/mesop_layout_colab.ipynb index 645221ef5..df4474a72 100644 --- a/mesop_layout_colab.ipynb +++ b/mesop_layout_colab.ipynb @@ -1,28 +1,10 @@ { - "nbformat": 4, - "nbformat_minor": 0, - "metadata": { - "colab": { - "provenance": [], - "private_outputs": true, - "toc_visible": true, - "authorship_tag": "ABX9TyOsU4T51ozrD5ZHXMc8bXrr", - "include_colab_link": true - }, - "kernelspec": { - "name": "python3", - "display_name": "Python 3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", "metadata": { - "id": "view-in-github", - "colab_type": "text" + "colab_type": "text", + "id": "view-in-github" }, "source": [ "\"Open" @@ -30,6 +12,9 @@ }, { "cell_type": "markdown", + "metadata": { + "id": "8r6nOL0xPfyU" + }, "source": [ "# Mesop Layout\n", "\n", @@ -37,64 +22,61 @@ "- https://google.github.io/mesop/guides/layout\n", "\n", "Mesop is a Python-based UI framework that allows you to rapidly build web apps like demos and internal apps. This Colab walks you through common layout patterns." - ], - "metadata": { - "id": "8r6nOL0xPfyU" - } + ] }, { "cell_type": "markdown", - "source": [ - "# Getting Started" - ], "metadata": { "id": "ZCtrRuMdPpJG" - } + }, + "source": [ + "# Getting Started" + ] }, { "cell_type": "code", - "source": [ - "!pip install mesop" - ], + "execution_count": null, "metadata": { "id": "buRfwUlqPons" }, - "execution_count": null, - "outputs": [] + "outputs": [], + "source": [ + "!pip install mesop" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hnyh1SX_P1XV" + }, + "outputs": [], "source": [ "import mesop as me\n", "import mesop.labs as mel\n", "\n", "me.colab_run()" - ], - "metadata": { - "id": "hnyh1SX_P1XV" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "# Rows & Columns\n" - ], "metadata": { "id": "jNb8UJbpPwNj" - } + }, + "source": [ + "# Rows & Columns\n" + ] }, { "cell_type": "markdown", + "metadata": { + "id": "EKtdsEFKP_te" + }, "source": [ "## Row\n", "\n", "This is a basic row" - ], - "metadata": { - "id": "EKtdsEFKP_te" - } + ] }, { "cell_type": "code", @@ -115,15 +97,20 @@ }, { "cell_type": "markdown", - "source": [ - "## Row with spacing" - ], "metadata": { "id": "799y-fhGB2h2" - } + }, + "source": [ + "## Row with spacing" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cJApeWgoB08H" + }, + "outputs": [], "source": [ "@me.page(path=\"/row-with-spacing\")\n", "def row():\n", @@ -133,24 +120,24 @@ " me.text(\"Right\")\n", "\n", "me.colab_show(path=\"/row-with-spacing\", height=100, width=\"50%\")" - ], - "metadata": { - "id": "cJApeWgoB08H" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "## Row with alignment" - ], "metadata": { "id": "amrmzDjUCZsi" - } + }, + "source": [ + "## Row with alignment" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "6ZgQCDt5CcY7" + }, + "outputs": [], "source": [ "@me.page(path=\"/row-with-alignment\")\n", "def row():\n", @@ -160,24 +147,24 @@ " me.box(style=me.Style(background=\"blue\", height=100, width=\"50%\"))\n", "\n", "me.colab_show(path=\"/row-with-alignment\", height=100, width=\"50%\")" - ], - "metadata": { - "id": "6ZgQCDt5CcY7" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "## Row & Columns" - ], "metadata": { "id": "F_tnJQYNG-Lk" - } + }, + "source": [ + "## Row & Columns" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OE4arFYIG9ik" + }, + "outputs": [], "source": [ "@me.page(path=\"/row-and-columns\")\n", "def app():\n", @@ -198,36 +185,36 @@ " me.box(style=me.Style(background=\"blue\", flex_grow=1))\n", "\n", "me.colab_show(path=\"/row-and-columns\", height=300)" - ], - "metadata": { - "id": "OE4arFYIG9ik" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "# Grid" - ], "metadata": { "id": "soEezch7DAxT" - } + }, + "source": [ + "# Grid" + ] }, { "cell_type": "markdown", - "source": [ - "## Side-by-side" - ], "metadata": { "id": "m9I06DnsDD-E" - } + }, + "source": [ + "## Side-by-side" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "m2fIwI5IDDim" + }, + "outputs": [], "source": [ "@me.page(path=\"/grid-sxs\")\n", - "def row():\n", + "def grid():\n", " # 1fr means 1 fraction, so each side is the same size.\n", " # Try changing one of the 1fr to 2fr and see what it looks like\n", " with me.box(style=me.Style(display=\"grid\", grid_template_columns=\"1fr 1fr\")):\n", @@ -235,24 +222,24 @@ " me.text(\"Some more text\")\n", "\n", "me.colab_show(path=\"/grid-sxs\", height=100, width=\"50%\")" - ], - "metadata": { - "id": "m2fIwI5IDDim" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "## Header - Body - Footer layout" - ], "metadata": { "id": "A1whheFYDXEX" - } + }, + "source": [ + "## Header - Body - Footer layout" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZP7l_mMQDWoK" + }, + "outputs": [], "source": [ "import mesop as me\n", "\n", @@ -286,24 +273,24 @@ " me.text(\"Footer\")\n", "\n", "me.colab_show(path=\"/grid-header-body-footer\", height=400)" - ], - "metadata": { - "id": "ZP7l_mMQDWoK" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", - "source": [ - "## Sidebar layout" - ], "metadata": { "id": "rxyuPKCeEmUo" - } + }, + "source": [ + "## Sidebar layout" + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nXhALOkrEnmp" + }, + "outputs": [], "source": [ "import mesop as me\n", "\n", @@ -330,26 +317,26 @@ " me.text(\"Main Content\")\n", "\n", "me.colab_show(path=\"/sidebar-layout\", height=400)" - ], - "metadata": { - "id": "nXhALOkrEnmp" - }, - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "markdown", + "metadata": { + "id": "hRz2iKGGFBfI" + }, "source": [ "# Responsive UI\n", "\n", "This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes." - ], - "metadata": { - "id": "hRz2iKGGFBfI" - } + ] }, { "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GLdBlUtNFFiw" + }, + "outputs": [], "source": [ "import mesop as me\n", "\n", @@ -378,12 +365,25 @@ " me.text(\"Main Content\")\n", "\n", "me.colab_show(path=\"/responsive-ui\", height=400)" - ], - "metadata": { - "id": "GLdBlUtNFFiw" - }, - "execution_count": null, - "outputs": [] + ] } - ] -} \ No newline at end of file + ], + "metadata": { + "colab": { + "authorship_tag": "ABX9TyOsU4T51ozrD5ZHXMc8bXrr", + "include_colab_link": true, + "private_outputs": true, + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From 14bcf804b4c73efa85b94600d028be34e09fc911 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 11:55:58 -0700 Subject: [PATCH 5/9] done --- docs/guides/event-handlers.md | 60 ++++++++++++++++++++++++++++++++--- docs/guides/interactivity.md | 2 +- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/guides/event-handlers.md b/docs/guides/event-handlers.md index d8cc331ef..25b74c4f7 100644 --- a/docs/guides/event-handlers.md +++ b/docs/guides/event-handlers.md @@ -1,6 +1,6 @@ # Event Handlers -Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions (callbacks). +Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions which are called by the Mesop framework when a user event is received. ## How it works @@ -25,7 +25,57 @@ When the counter function is called, it creates an instance of the button compon We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function. -Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page made be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault tolerant and allows for easy scaling of the app. +Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault tolerant and allows for easy scaling of the app. + +## Types of event handlers + +### Regular functions + +These are the simplest and most common type of event handlers used. It's essentially a regular Python function which is called by the Mesop framework when a user event is received. + +```py title="Regular function" +def on_click(event: me.ClickEvent): + state = me.state(State) + state.count += 1 +``` + +### Generator functions + +Python Generator functions are a powerful tool which allow you to `yield` multiple times in a single event handler. This allows you to render intermediate UI states. + +```py title="Generator function" +def on_click(event: me.ClickEvent): + state = me.state(State) + state.count += 1 + yield + time.sleep(1) + state.count += 1 + yield +``` + +You can learn more about real-world use cases of the generator functions in the [Interactivity guide](./interactivity.md). + +???+ info "Always yield at the end of a generator function" + If you use a `yield` statement in your event handler, then the event handler will be a generator function. You must have a `yield` statement at the end of the event handler (or each return point), otherwise not all of your code will be executed. + +### Async generator functions + +Python async generator functions allow you to do concurrent work using Python's `async` and `await` language features. If you are using async Python libraries, you can use these types of event handlers. + +```py title="Async generator function" +async def on_click(event: me.ClickEvent): + state = me.state(State) + state.count += 1 + yield + await asyncio.sleep(1) + state.count += 1 + yield +``` + +For a more complete example, please refer to the [Async section of the Interactivity guide](./interactivity.md#async). + +???+ info "Always yield at the end of an async generator function" + Similar to a regular generator function, an async generator function must have a `yield` statement at the end of the event handler (or each return point), otherwise not all of your code will be executed. ## Patterns @@ -50,7 +100,7 @@ def call_api(): ### Boilerplate-free event handlers -If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern: +If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the `key` attribute that's available in most events and uses Python's built-in `setattr` function to dynamically update the state: ```py title="Boilerplate-free event handlers" def app(): @@ -87,7 +137,9 @@ def app(): link_component("/2") ``` -The problem with this above example is that Mesop only stores the last event handler. This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior. +The problem with this above example is that Mesop only stores the last event handler. This is because each event handler has the same id which means that Mesop cannot differentiate between the two instances of the same event handler. + +This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior. Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example: diff --git a/docs/guides/interactivity.md b/docs/guides/interactivity.md index 625012bf0..6ec4cdf29 100644 --- a/docs/guides/interactivity.md +++ b/docs/guides/interactivity.md @@ -1,6 +1,6 @@ # Interactivity -This guide continues from the Counter app example in [Core Concepts](../getting-started/core-concepts.md#counter-app) and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call. +This guide continues from the [event handlers guide](./event-handlers.md) and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call. ## Intermediate loading state From 346e038a075068c447d69df5cb12bd551fbbb3ca Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 12:02:10 -0700 Subject: [PATCH 6/9] fine --- docs/guides/event-handlers.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/guides/event-handlers.md b/docs/guides/event-handlers.md index 25b74c4f7..c13c7d321 100644 --- a/docs/guides/event-handlers.md +++ b/docs/guides/event-handlers.md @@ -98,6 +98,23 @@ def call_api(): pass ``` +If you want to reuse event handler logic between generator functions, you can use the [`yield from`](https://docs.python.org/3/whatsnew/3.3.html#pep-380) syntax. For example, let's say `call_api` in the above example is a generator function. You can use `yield from` to reuse the event handler logic: + +```py title="Reusing event handler logic for generator functions" +def on_enter(event: me.InputEnterEvent): + state = me.state(State) + state.value = event.value + yield from call_api() + +def on_click(event: me.ClickEvent): + yield from call_api() + +def call_api(): + # Do initial work + yield + # Do more work + yield +``` ### Boilerplate-free event handlers If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the `key` attribute that's available in most events and uses Python's built-in `setattr` function to dynamically update the state: From cdae843de663a05d10826ffcbf93a95715e1a398 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 12:15:07 -0700 Subject: [PATCH 7/9] readthrough --- docs/guides/debugging.md | 6 +++--- docs/guides/event-handlers.md | 8 ++++---- docs/guides/interactivity.md | 2 +- docs/guides/layouts.md | 2 ++ docs/guides/performance.md | 25 +++++++++++++------------ 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/guides/debugging.md b/docs/guides/debugging.md index 56d539cb1..d2158c0d5 100644 --- a/docs/guides/debugging.md +++ b/docs/guides/debugging.md @@ -22,7 +22,7 @@ Here's how you can use Chrome DevTools to debug your Mesop app: 1. Open your Mesop app in Google Chrome. -1. Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut top open Chrome DevTools: +1. Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut to open Chrome DevTools: - Windows/Linux: Ctrl + Shift + I - macOS: Cmd + Option + I @@ -45,9 +45,9 @@ Remember, while Mesop abstracts away much of the frontend complexity, using thes ## Debugging with VS Code -VS Code is recomended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs. +VS Code is recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs. -**Pre-requisite:** ensure VS Code is downloaded. +**Pre-requisite:** Ensure VS Code is downloaded. 1. Install the [Python Debugger VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy). diff --git a/docs/guides/event-handlers.md b/docs/guides/event-handlers.md index c13c7d321..c327cf6c5 100644 --- a/docs/guides/event-handlers.md +++ b/docs/guides/event-handlers.md @@ -25,7 +25,7 @@ When the counter function is called, it creates an instance of the button compon We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function. -Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault tolerant and allows for easy scaling of the app. +Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault-tolerant and enables simple scaling. ## Types of event handlers @@ -41,7 +41,7 @@ def on_click(event: me.ClickEvent): ### Generator functions -Python Generator functions are a powerful tool which allow you to `yield` multiple times in a single event handler. This allows you to render intermediate UI states. +Python Generator functions are a powerful tool, which allow you to `yield` multiple times in a single event handler. This allows you to render intermediate UI states. ```py title="Generator function" def on_click(event: me.ClickEvent): @@ -134,13 +134,13 @@ def update_state(event: me.InputBlurEvent): setattr(state, event.key, event.value) ``` -The downside with this approach is that you lose type-safety. Generally, defining a separate event handler, although more verbose, is easier to maintain. +The downside of this approach is that you lose type safety. Generally, defining a separate event handler, although more verbose, is easier to maintain. ## Troubleshooting ### Avoid using closure variables in event handler -One subtle mistake when building a reusable component is to have the event handler use a closure variable like the following example: +One subtle mistake when building a reusable component is having the event handler use a closure variable, as shown in the following example: ```py title="Bad example of using closure variable" @me.component diff --git a/docs/guides/interactivity.md b/docs/guides/interactivity.md index 6ec4cdf29..0dad8c10b 100644 --- a/docs/guides/interactivity.md +++ b/docs/guides/interactivity.md @@ -71,7 +71,7 @@ You can use the `on_blur` event instead of `on_input` to only update the input v This is also more performant because it sends much fewer network requests. -```py title="Bad example: setting the value and using on_input" +```py title="Good example: setting the value and using on_input" @me.stateclass class State: input_value: str diff --git a/docs/guides/layouts.md b/docs/guides/layouts.md index a49e41712..aaf809309 100644 --- a/docs/guides/layouts.md +++ b/docs/guides/layouts.md @@ -135,6 +135,8 @@ def app(): This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes. +Learn more about responsive UI in the [viewport size docs](../api/viewport-size.md). + ```python def app(): is_desktop = me.viewport_size().width > 640 diff --git a/docs/guides/performance.md b/docs/guides/performance.md index 8a2b371fb..80cc0e84e 100644 --- a/docs/guides/performance.md +++ b/docs/guides/performance.md @@ -6,13 +6,13 @@ Occasionally, you may run into performance issues with your Mesop app. Here are The first step in debugging performance issues is to identify the cause of the issue. Follow the [Debugging with DevTools guide](./debugging.md#debugging-with-devtools) and use the Console and Network tab to identify the issue. -## Common issues +## Common performance bottlenecks and solutions -### State is too large +### Optimizing state size for performance If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large. -Because the state class is serialized and sent back and forth between the client and server, you should try to keep the state class reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app. +Because the state object is serialized and sent back and forth between the client and server, you should try to keep the state object reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app. The following are recommendations to help you avoid large state payloads: @@ -20,7 +20,7 @@ The following are recommendations to help you avoid large state payloads: Mesop has a feature that allows you to store state in memory rather than passing the full state on every request. This may help improve performance when dealing with large -state objects. The caveat is that storing state in memory contains its own set of +state objects. The caveat is that, storing state in memory contains its own set of problems that you must carefully consider. See the [config section](../api/config.md#mesop_state_session_backend) for details on how to use this feature. @@ -28,19 +28,20 @@ If you are running Mesop on a single replica or you can enable [session affinity #### Store state externally -You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store in them in a bucket service like [Google Cloud Storage](https://cloud.google.com/storage) and send [signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls) to the client so that it can directly fetch the images without going through the Mesop server. +You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store them in a bucket service like [Google Cloud Storage](https://cloud.google.com/storage) and send [signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls) to the client so that it can directly fetch the images without going through the Mesop server. -### Too many users +### Handling high user load If you notice that your Mesop app is running slowly when you have many concurrent users, you can try to scale your Mesop app. -#### Use Cloud Run +#### Increase the number of replicas -Cloud Run is a managed Google Cloud service that can scale your Mesop app to handle more concurrent users. -You can use the [autoscaling feature](https://cloud.google.com/run/docs/about-instance-autoscaling) to scale your Mesop app up and down based on the traffic to your app. Follow Mesop's [Cloud Run deployment guide](./deployment.md#deploying-to-cloud-run) to deploy your Mesop app to Cloud Run. +To handle more concurrent users, you can scale your Mesop app horizontally by increasing the number of replicas (instances) running your application. This can be achieved through various cloud services that offer autoscaling features: -You can also use other Cloud services which provide autoscaling features. +1. Use a managed service like Google Cloud Run, which automatically scales your app based on traffic. Follow Mesop's [Cloud Run deployment guide](./deployment.md#deploying-to-cloud-run) for details. -#### Adjust your gunicorn settings +2. Manually adjust the number of replicas to a higher number. -If you're using [gunicorn](https://docs.gunicorn.org/) to serve your Mesop app, you can adjust gunicorn settings to [increase the number of workers](https://docs.gunicorn.org/en/latest/design.html#how-many-workers). This can help to increase the number of concurrent users your Mesop app can handle. +3. Tune gunicorn settings. If you're using [gunicorn](https://docs.gunicorn.org/) to serve your Mesop app, you can adjust gunicorn settings to [increase the number of workers](https://docs.gunicorn.org/en/latest/design.html#how-many-workers). This can help to increase the number of concurrent users your Mesop app can handle. + +Whichever platform you choose, make sure to configure the replica settings to match your app's performance requirements and budget constraints. From 195b97326882bf3fdeef61d8e78f04818ba8a7b4 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 12:20:10 -0700 Subject: [PATCH 8/9] more --- docs/guides/performance.md | 2 +- docs/guides/state-management.md | 2 +- mkdocs.yml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/guides/performance.md b/docs/guides/performance.md index 80cc0e84e..b6d447878 100644 --- a/docs/guides/performance.md +++ b/docs/guides/performance.md @@ -8,7 +8,7 @@ The first step in debugging performance issues is to identify the cause of the i ## Common performance bottlenecks and solutions -### Optimizing state size for performance +### Optimizing state size If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large. diff --git a/docs/guides/state-management.md b/docs/guides/state-management.md index a599740db..c1d44ac7e 100644 --- a/docs/guides/state-management.md +++ b/docs/guides/state-management.md @@ -176,7 +176,7 @@ If you didn't explicitly annotate NestedState as a dataclass, then you would get ### State performance issues -Take a look at the [performance guide](./performance.md#state-is-too-large) to learn how to identify and fix State-related performance issues. +Take a look at the [performance guide](./performance.md#optimizing-state-size) to learn how to identify and fix State-related performance issues. ## Next steps diff --git a/mkdocs.yml b/mkdocs.yml index 63b5099ad..1ae459d8e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -143,6 +143,7 @@ theme: - content.code.copy - navigation.path - navigation.instant + - navigation.instant.progress - navigation.tracking - navigation.prune - navigation.tabs @@ -157,6 +158,8 @@ extra_css: markdown_extensions: - attr_list - sane_lists + - toc: + permalink: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg From fa3af04809b4e94f091a2ccde9fcfc100332dc8e Mon Sep 17 00:00:00 2001 From: Will Chen Date: Wed, 14 Aug 2024 12:20:44 -0700 Subject: [PATCH 9/9] fix --- notebooks/mesop_layout_colab.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/mesop_layout_colab.ipynb b/notebooks/mesop_layout_colab.ipynb index d7309eb92..c72806f44 100644 --- a/notebooks/mesop_layout_colab.ipynb +++ b/notebooks/mesop_layout_colab.ipynb @@ -348,4 +348,4 @@ "outputs": [] } ] -} \ No newline at end of file +}