Skip to content

Commit

Permalink
Support query params (#676)
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwillchen authored Aug 6, 2024
1 parent 38c486f commit 3ffd914
Show file tree
Hide file tree
Showing 23 changed files with 820 additions and 24 deletions.
96 changes: 96 additions & 0 deletions docs/api/query_params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Query Params API

## Overview

Query params, also sometimes called query string, provide a way to manage state in the URLs. They are useful for providing deep-links into your Mesop app.

## Example

Here's a simple working example that shows how you can read and write query params.

```py
@me.page(path="/examples/query_params/page_2")
def page_2():
me.text(f"query_params={me.query_params}")
me.button("Add query param", on_click=add_query_param)
me.button("Navigate", on_click=navigate)

def add_query_param(e: me.ClickEvent):
me.query_params["key"] = "value"

def navigate(e: me.ClickEvent):
me.navigate("/examples/query_params", query_params=me.query_params)
```

## Usage

You can use query parameters from `me.query_params`, which has a dictionary-like interface, where the key is the parameter name and value is the parameter value.

### Get a query param value

```py
value: str = me.query_params['param_name']
```
This will raise a KeyError if the parameter doesn't exist. You can use `in` to check whether a key exists in `me.query_params`:

```py
if 'key' in me.query_params:
print(me.query_params['key'])
```

???+ NOTE "Repeated query params"
If a query param key is repeated, then you will get the _first_ value. If you want all the values use `get_all`.

### Get all values

To get all the values for a particular query parameter key, you can use `me.query_params.get_all`, which returns a sequence of parameter values (currently implemented as a `tuple`).

```py
all_values = me.query_params.get_all('param_name')
```

### Iterate

```py
for key in query_params:
value = query_params[key]
```

### Set query param

```py
query_params['new_param'] = 'value'
```

### Set repeated query param

```py
query_params['repeated_param'] = ['value1', 'value2']
```

### Delete

```py
del query_params['param_to_delete']
```

## Patterns

### Navigate with existing query params

Here's an example of how to navigate to a new page with query parameters:

```py
def click_navigate_button(e: me.ClickEvent):
me.query_params['q'] = "value"
me.navigate('/search', query_params=me.query_params)
```

### Navigate with only new query params

You can also navigate by passing in a dictionary to `query_params` parameter for `me.navigate` if you do _not_ want to keep the existing query parameters.

```py
def click_navigate_button(e: me.ClickEvent):
me.navigate('/search', query_params={"q": "value})
```
1 change: 1 addition & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
MesopUserException as MesopUserException,
)
from mesop.features import page as page
from mesop.features.query_params import query_params as query_params
from mesop.features.theme import set_theme_density as set_theme_density
from mesop.features.theme import set_theme_mode as set_theme_mode
from mesop.features.theme import theme_brightness as theme_brightness
Expand Down
1 change: 1 addition & 0 deletions mesop/commands/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ py_library(
name = "commands",
srcs = glob(["*.py"]),
deps = [
"//mesop/features",
"//mesop/protos:ui_py_pb2",
"//mesop/runtime",
],
Expand Down
32 changes: 30 additions & 2 deletions mesop/commands/navigate.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
from typing import Sequence

from mesop.features.query_params import (
QueryParams,
)
from mesop.features.query_params import (
query_params as me_query_params,
)
from mesop.runtime import runtime
from mesop.utils.url_utils import remove_url_query_param
from mesop.warn import warn


def navigate(url: str) -> None:
def navigate(
url: str,
*,
query_params: dict[str, str | Sequence[str]] | QueryParams | None = None,
) -> None:
"""
Navigates to the given URL.
Args:
url: The URL to navigate to.
query_params: A dictionary of query parameters to include in the URL, or `me.query_params`. If not provided, all current query parameters will be removed.
"""
runtime().context().navigate(url)
cleaned_url = remove_url_query_param(url)
if url != cleaned_url:
warn(
"Used me.navigate to navigate to a URL with query params. The query params have been removed. "
"Instead pass the query params using the keyword argument like this: "
"me.navigate(url, query_params={'key': 'value'})"
)
if isinstance(query_params, QueryParams):
query_params = {key: query_params.get_all(key) for key in query_params}

# Clear the query params because the query params will
# either be replaced with the new query_params or emptied (in server.py).
me_query_params.clear()
runtime().context().navigate(cleaned_url, query_params)
1 change: 1 addition & 0 deletions mesop/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from mesop.examples import on_load_generator as on_load_generator
from mesop.examples import playground as playground
from mesop.examples import playground_critic as playground_critic
from mesop.examples import query_params as query_params
from mesop.examples import readme_app as readme_app
from mesop.examples import scroll_into_view as scroll_into_view
from mesop.examples import starter_kit as starter_kit
Expand Down
111 changes: 111 additions & 0 deletions mesop/examples/query_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import mesop as me


def on_load(e: me.LoadEvent):
me.query_params["on_load"] = "loaded"


@me.page(path="/examples/query_params", on_load=on_load)
def page():
me.text(f"query_params={me.query_params}")
me.button("append query param list", on_click=append_query_param_list)
me.button("URL-encoded query param", on_click=url_encoded_query_param)
me.button(
"navigate URL-encoded query param",
on_click=navigate_url_encoded_query_param,
)
me.button("increment query param directly", on_click=increment_query_param)
me.button("delete list query param", on_click=delete_list_query_param)
me.button("delete all query params", on_click=delete_all_query_params)
me.button("increment query param by navigate", on_click=increment_by_navigate)
me.button("navigate to page 2 with query params", on_click=navigate_page_2)
me.button("navigate to page 2 with dict", on_click=navigate_page_2_new_dict)
me.button(
"navigate to page 2 without query params",
on_click=navigate_page_2_without_query_params,
)


@me.stateclass
class State:
click_append_query_param_list: int


def append_query_param_list(e: me.ClickEvent):
current_list = (
[] if "list" not in me.query_params else me.query_params.get_all("list")
)
state = me.state(State)
state.click_append_query_param_list += 1
me.query_params["list"] = [
*current_list,
f"val{state.click_append_query_param_list}",
]


def url_encoded_query_param(e: me.ClickEvent):
me.query_params["url_encoded"] = ["&should-be-escaped=true"]


def navigate_url_encoded_query_param(e: me.ClickEvent):
me.navigate(
"/examples/query_params/page_2",
query_params={
"url_encoded": "&should-be-escaped=true",
"url_encoded_values": ["value1&a=1", "value2&a=2"],
},
)


def increment_query_param(e: me.ClickEvent):
if "counter" not in me.query_params:
me.query_params["counter"] = "0"

me.query_params["counter"] = str(int(me.query_params["counter"]) + 1)


def delete_list_query_param(e: me.ClickEvent):
del me.query_params["list"]


def delete_all_query_params(e: me.ClickEvent):
for key in list(me.query_params.keys()):
del me.query_params[key]


def navigate_page_2(e: me.ClickEvent):
me.navigate("/examples/query_params/page_2", query_params=me.query_params)


def navigate_page_2_new_dict(e: me.ClickEvent):
me.navigate(
"/examples/query_params/page_2",
query_params={"page2": ["1", "2"], "single": "a"},
)


def navigate_page_2_without_query_params(e: me.ClickEvent):
me.navigate("/examples/query_params/page_2")


def increment_by_navigate(e: me.ClickEvent):
if "counter" not in me.query_params:
me.query_params["counter"] = "0"

counter = int(me.query_params["counter"]) + 1
me.query_params["counter"] = str(counter)

me.navigate(
"/examples/query_params",
query_params=me.query_params,
)


@me.page(path="/examples/query_params/page_2")
def page_2():
me.text(f"query_params(page_2)={me.query_params}")
me.button("Navigate back", on_click=navigate_back)


def navigate_back(e: me.ClickEvent):
me.navigate("/examples/query_params", query_params=me.query_params)
15 changes: 13 additions & 2 deletions mesop/features/BUILD
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
load("//build_defs:defaults.bzl", "py_library")
load("//build_defs:defaults.bzl", "THIRD_PARTY_PY_PYTEST", "py_library", "py_test")

package(
default_visibility = ["//build_defs:mesop_internal"],
)

py_library(
name = "features",
srcs = glob(["*.py"]),
srcs = glob(
["*.py"],
exclude = ["*_test.py"],
),
deps = [
"//mesop/runtime",
],
)

py_test(
name = "query_params_test",
srcs = ["query_params_test.py"],
deps = [
":features",
] + THIRD_PARTY_PY_PYTEST,
)
38 changes: 38 additions & 0 deletions mesop/features/query_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Callable, Iterator, MutableMapping, Sequence

from mesop.runtime import runtime
from mesop.runtime.context import Context


class QueryParams(MutableMapping[str, str]):
def __init__(self, get_context: Callable[[], Context]):
self._get_context = get_context

def __iter__(self) -> Iterator[str]:
return iter(self._get_context().query_params())

def __len__(self) -> int:
return len(self._get_context().query_params())

def __str__(self) -> str:
return str(self._get_context().query_params())

def __getitem__(self, key: str) -> str:
# Returns the first value associated with the key to match
# the web API:
# https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/get
return self._get_context().query_params()[key][0]

def get_all(self, key: str) -> Sequence[str]:
if key not in self._get_context().query_params():
return tuple()
return tuple(self._get_context().query_params()[key])

def __delitem__(self, key: str) -> None:
self._get_context().set_query_param(key=key, value=None)

def __setitem__(self, key: str, value: str | Sequence[str]) -> None:
self._get_context().set_query_param(key=key, value=value)


query_params = QueryParams(runtime().context)
Loading

0 comments on commit 3ffd914

Please sign in to comment.