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..d2158c0d5 100644 --- a/docs/guides/debugging.md +++ b/docs/guides/debugging.md @@ -1,10 +1,53 @@ # 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) -**Pre-requisite:** ensure VS Code is downloaded. +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 to 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 recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs. + +**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 new file mode 100644 index 000000000..c327cf6c5 --- /dev/null +++ b/docs/guides/event-handlers.md @@ -0,0 +1,179 @@ +# Event Handlers + +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 + +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 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 + +### 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 + +### 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 +``` + +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: + +```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 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 having the event handler use a closure variable, as shown in 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 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: + +```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..0dad8c10b 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 @@ -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,70 @@ 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="Good 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 + +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. -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: 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 -```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) +@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 ``` -For more info on using component keys, please refer to the [Component Key docs](../components/index.md#component-key). +## Next steps + +Learn about layouts to build a customized UI. + + + Layouts + diff --git a/docs/guides/layouts.md b/docs/guides/layouts.md new file mode 100644 index 000000000..aaf809309 --- /dev/null +++ b/docs/guides/layouts.md @@ -0,0 +1,180 @@ +# Layouts + +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. + +For most Mesop apps, you will use some combination of these types of layouts: + +- [Rows and Columns](#rows-and-columns) +- [Grids](#grids) + +## Common layout examples + +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) + +### 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. + +Learn more about responsive UI in the [viewport size docs](../api/viewport-size.md). + +```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) + +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/docs/guides/performance.md b/docs/guides/performance.md new file mode 100644 index 000000000..b6d447878 --- /dev/null +++ b/docs/guides/performance.md @@ -0,0 +1,47 @@ +# 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 performance bottlenecks and solutions + +### 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. + +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: + +#### 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 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. + +### 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. + +#### Increase the number of replicas + +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: + +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. + +2. Manually adjust the number of replicas to a higher number. + +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. diff --git a/docs/guides/state-management.md b/docs/guides/state-management.md index 5edf9dbea..c1d44ac7e 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#optimizing-state-size) 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/mesop_layout_colab.ipynb b/mesop_layout_colab.ipynb new file mode 100644 index 000000000..df4474a72 --- /dev/null +++ b/mesop_layout_colab.ipynb @@ -0,0 +1,389 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8r6nOL0xPfyU" + }, + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZCtrRuMdPpJG" + }, + "source": [ + "# Getting Started" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "buRfwUlqPons" + }, + "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()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jNb8UJbpPwNj" + }, + "source": [ + "# Rows & Columns\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EKtdsEFKP_te" + }, + "source": [ + "## Row\n", + "\n", + "This is a basic row" + ] + }, + { + "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", + "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", + " # 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%\")" + ] + }, + { + "cell_type": "markdown", + "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", + " # 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%\")" + ] + }, + { + "cell_type": "markdown", + "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", + " 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)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "soEezch7DAxT" + }, + "source": [ + "# Grid" + ] + }, + { + "cell_type": "markdown", + "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 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", + " me.text(\"A bunch of text\")\n", + " me.text(\"Some more text\")\n", + "\n", + "me.colab_show(path=\"/grid-sxs\", height=100, width=\"50%\")" + ] + }, + { + "cell_type": "markdown", + "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", + "@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)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rxyuPKCeEmUo" + }, + "source": [ + "## Sidebar layout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nXhALOkrEnmp" + }, + "outputs": [], + "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)" + ] + }, + { + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "GLdBlUtNFFiw" + }, + "outputs": [], + "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": { + "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 +} diff --git a/mkdocs.yml b/mkdocs.yml index d7a44b614..1ae459d8e 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 @@ -136,6 +143,7 @@ theme: - content.code.copy - navigation.path - navigation.instant + - navigation.instant.progress - navigation.tracking - navigation.prune - navigation.tabs @@ -150,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 diff --git a/notebooks/mesop_layout_colab.ipynb b/notebooks/mesop_layout_colab.ipynb new file mode 100644 index 000000000..c72806f44 --- /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": [] + } + ] +}