diff --git a/demo/async_action/app.py b/demo/async_action/app.py
new file mode 100644
index 00000000..b72fb077
--- /dev/null
+++ b/demo/async_action/app.py
@@ -0,0 +1,83 @@
+import random
+from dataclasses import field
+
+from async_action_component import AsyncAction, async_action_component
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ boxes: dict[str, list[bool | int | str]] = field(
+ default_factory=lambda: {
+ "box1": [False, 0, "red"],
+ "box2": [False, 0, "orange"],
+ "box3": [False, 0, "yellow"],
+ }
+ )
+ action: str
+ duration: int
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/async_action",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_connect_srcs=["https://cdn.jsdelivr.net"],
+ allowed_script_srcs=["https://cdn.jsdelivr.net"],
+ dangerously_disable_trusted_types=True,
+ ),
+)
+def page():
+ state = me.state(State)
+ action = (
+ AsyncAction(value=state.action, duration_seconds=state.duration)
+ if state.action
+ else None
+ )
+ async_action_component(
+ action=action, on_started=on_started, on_finished=on_finished
+ )
+ with me.box(
+ style=me.Style(
+ display="flex", flex_direction="column", margin=me.Margin.all(15)
+ )
+ ):
+ for key, meta in state.boxes.items():
+ with me.box(style=me.Style(padding=me.Padding.all(15))):
+ me.button("Show " + key, type="flat", key=key, on_click=on_click)
+ if meta[0]:
+ with me.box(
+ style=me.Style(
+ background=str(meta[2]),
+ width=100,
+ height=100,
+ margin=me.Margin(top=15),
+ padding=me.Padding.all(15),
+ )
+ ):
+ me.text(f"{meta[0]} {meta[1]}")
+
+
+def on_click(e: me.ClickEvent):
+ state = me.state(State)
+ state.action = e.key
+ state.duration = random.randint(2, 10)
+ state.boxes[e.key][0] = True
+ state.boxes[e.key][1] = state.duration
+
+
+def on_started(e: me.WebEvent):
+ state = me.state(State)
+ state.action = ""
+
+
+def on_finished(e: me.WebEvent):
+ state = me.state(State)
+ state.action = ""
+ state.boxes[e.value["action"]][0] = False
diff --git a/demo/async_action/async_action_component.js b/demo/async_action/async_action_component.js
new file mode 100644
index 00000000..75a049e7
--- /dev/null
+++ b/demo/async_action/async_action_component.js
@@ -0,0 +1,47 @@
+import {
+ LitElement,
+ html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+class AsyncAction extends LitElement {
+ static properties = {
+ startedEvent: {type: String},
+ finishedEvent: {type: String},
+ // Format: {action: String, duration_seconds: Number}
+ action: {type: Object},
+ isRunning: {type: Boolean},
+ };
+
+ render() {
+ return html`
`;
+ }
+
+ firstUpdated() {
+ if (this.action) {
+ this.runTimeout(this.action);
+ }
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.has('action') && this.action) {
+ this.runTimeout(this.action);
+ }
+ }
+
+ runTimeout(action) {
+ this.dispatchEvent(
+ new MesopEvent(this.startedEvent, {
+ action: action,
+ }),
+ );
+ setTimeout(() => {
+ this.dispatchEvent(
+ new MesopEvent(this.finishedEvent, {
+ action: action.value,
+ }),
+ );
+ }, action.duration_seconds * 1000);
+ }
+}
+
+customElements.define('async-action-component', AsyncAction);
diff --git a/demo/async_action/async_action_component.py b/demo/async_action/async_action_component.py
new file mode 100644
index 00000000..ae7daede
--- /dev/null
+++ b/demo/async_action/async_action_component.py
@@ -0,0 +1,41 @@
+from dataclasses import asdict, dataclass
+from typing import Any, Callable
+
+import mesop as me
+
+
+@dataclass
+class AsyncAction:
+ value: str
+ duration_seconds: int
+
+
+@me.web_component(path="./async_action_component.js")
+def async_action_component(
+ *,
+ on_started: Callable[[me.WebEvent], Any],
+ on_finished: Callable[[me.WebEvent], Any],
+ action: AsyncAction | None = None,
+ key: str | None = None,
+):
+ """Creates an invisibe component that will delay state changes asynchronously.
+
+ Right now this implementation is limited since we basically just pass the key around.
+ But ideally we also pass in some kind of value to update when the time out expires.
+
+ The main benefit of this component is for cases, such as status messages that may
+ appear and disappear after some duration. The primary example here is the example
+ snackbar widget, which right now blocks the UI when using the sleep yield approach.
+
+ The other benefit of this component is that it works generically (rather than say
+ implementing a custom snackbar widget as a web component).
+ """
+ return me.insert_web_component(
+ name="async-action-component",
+ key=key,
+ events={
+ "startedEvent": on_started,
+ "finishedEvent": on_finished,
+ },
+ properties={"action": asdict(action) if action else ""},
+ )
diff --git a/demo/charts/app.py b/demo/charts/app.py
new file mode 100644
index 00000000..dd7f81f8
--- /dev/null
+++ b/demo/charts/app.py
@@ -0,0 +1,203 @@
+from chartjs_component import chartjs_component
+
+import mesop as me
+
+_SUBTITLE_DATE = "Jan - May 2025"
+
+_RED = "rgb(255, 99, 132)"
+_BLUE = "rgb(54, 162, 235)"
+_YELLOW = "rgb(255, 205, 86)"
+_GREEN = "rgb(75, 192, 192)"
+_GREY = "rgb(201, 203, 207)"
+
+_DEFAULT_OPTIONS = {
+ "responsive": True,
+ "aspectRatio": 1,
+ "maintainAspectRatio": True,
+ "scales": {
+ "y": {
+ "beginAtZero": True,
+ }
+ },
+}
+
+_PIE_OPTIONS = {
+ "options": {
+ "plugins": {
+ "legend": {
+ "display": False,
+ },
+ },
+ "responsive": True,
+ "aspectRatio": 1,
+ "maintainAspectRatio": True,
+ },
+}
+_DOUGHNUT_OPTIONS = _PIE_OPTIONS
+_POLAR_AREA_OPTIONS = (
+ {
+ "responsive": True,
+ "aspectRatio": 1,
+ "maintainAspectRatio": True,
+ "scales": {
+ "r": {
+ "beginAtZero": True,
+ }
+ },
+ },
+)
+
+config_line = {
+ "type": "line",
+ "data": {
+ "labels": ["January", "February", "March", "April", "May"],
+ "datasets": [
+ {
+ "label": "Pies",
+ "data": [65, 59, 80, 81, 56],
+ "backgroundColor": _RED,
+ "borderColor": _RED,
+ "fill": False,
+ },
+ {
+ "label": "Donuts",
+ "data": [102, 122, 115, 98, 128],
+ "backgroundColor": _GREEN,
+ "borderColor": _GREEN,
+ "fill": False,
+ },
+ ],
+ },
+ "options": _DEFAULT_OPTIONS,
+}
+
+config_bar = {
+ "type": "bar",
+ "data": {
+ "labels": ["January", "February", "March", "April", "May"],
+ "datasets": [
+ {
+ "label": "Pies",
+ "data": [65, 59, 80, 81, 56],
+ "backgroundColor": _RED,
+ },
+ {
+ "label": "Donuts",
+ "data": [102, 122, 115, 98, 128],
+ "backgroundColor": _GREEN,
+ },
+ ],
+ },
+ "options": _DEFAULT_OPTIONS,
+}
+
+config_area = {
+ "type": "line",
+ "data": {
+ "labels": ["January", "February", "March", "April", "May"],
+ "datasets": [
+ {
+ "label": "Pies",
+ "data": [65, 59, 80, 81, 56],
+ "backgroundColor": _RED,
+ "borderColor": _RED,
+ "fill": True,
+ },
+ {
+ "label": "Donuts",
+ "data": [102, 122, 115, 98, 128],
+ "backgroundColor": _GREEN,
+ "borderColor": _GREEN,
+ "fill": True,
+ },
+ ],
+ },
+ "options": _DEFAULT_OPTIONS,
+}
+
+config_doughnut = {
+ "type": "doughnut",
+ "data": {
+ "labels": ["Apple fritter", "Glazed", "Bear claw", "Cruller"],
+ "datasets": [
+ {
+ "data": [300, 50, 100, 42],
+ "backgroundColor": [
+ _RED,
+ _BLUE,
+ _YELLOW,
+ _GREEN,
+ ],
+ "hoverOffset": 4,
+ }
+ ],
+ },
+ "options": _DOUGHNUT_OPTIONS,
+}
+
+config_pie = {
+ "type": "pie",
+ "data": {
+ "labels": ["Lemon Meringue", "Pumpkin", "Apple", "Banana Cream"],
+ "datasets": [
+ {
+ "data": [300, 50, 100, 42],
+ "backgroundColor": [
+ _RED,
+ _BLUE,
+ _YELLOW,
+ _GREEN,
+ ],
+ "hoverOffset": 4,
+ }
+ ],
+ },
+ "options": _PIE_OPTIONS,
+}
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/charts",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_script_srcs=[
+ "https://cdn.jsdelivr.net",
+ "https://cdn.jsdelivr.net/npm/chart.js",
+ ],
+ ),
+)
+def page():
+ with me.box(
+ style=me.Style(
+ margin=me.Margin.all(15), display="flex", gap=15, flex_wrap="wrap"
+ )
+ ):
+ with me.card():
+ me.card_header(title="Bar Chart", subtitle=_SUBTITLE_DATE)
+ with me.card_content():
+ chartjs_component(config=config_bar)
+
+ with me.card():
+ me.card_header(title="Line Chart", subtitle=_SUBTITLE_DATE)
+ with me.card_content():
+ chartjs_component(config=config_line)
+
+ with me.card():
+ me.card_header(title="Area Chart", subtitle=_SUBTITLE_DATE)
+ with me.card_content():
+ chartjs_component(config=config_area)
+
+ with me.card():
+ me.card_header(title="Donut Chart", subtitle=_SUBTITLE_DATE)
+ with me.card_content():
+ chartjs_component(config=config_doughnut)
+
+ with me.card():
+ me.card_header(title="Pie Chart", subtitle=_SUBTITLE_DATE)
+ with me.card_content():
+ chartjs_component(config=config_pie)
diff --git a/demo/charts/chartjs_component.js b/demo/charts/chartjs_component.js
new file mode 100644
index 00000000..22e61957
--- /dev/null
+++ b/demo/charts/chartjs_component.js
@@ -0,0 +1,44 @@
+import {
+ LitElement,
+ html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+import 'https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js';
+
+class ChartjsComponent extends LitElement {
+ static properties = {
+ config: {type: String},
+ };
+
+ constructor() {
+ super();
+ this.chart = null;
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ firstUpdated() {
+ this.renderChart();
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.has('config')) {
+ this.renderChart();
+ }
+ }
+
+ renderChart() {
+ if (this.chart) {
+ this.chart.destroy();
+ }
+ const ctx = this.querySelector('#chart');
+ this.chart = new Chart(ctx, JSON.parse(this.config));
+ }
+
+ render() {
+ return html``;
+ }
+}
+
+customElements.define('chartjs-component', ChartjsComponent);
diff --git a/demo/charts/chartjs_component.py b/demo/charts/chartjs_component.py
new file mode 100644
index 00000000..91af5ea2
--- /dev/null
+++ b/demo/charts/chartjs_component.py
@@ -0,0 +1,27 @@
+import json
+from typing import Any
+
+import mesop as me
+
+
+@me.web_component(path="./chartjs_component.js")
+def chartjs_component(config: dict[str, Any]):
+ """Creates a Chart.js chart within a Mesop application.
+
+ This components provides a minimal interface to chartjs from Mesop. Right now we
+ can only provide the most basic functionality. More complicated functionality is
+ not yet supported since they require javascript to work.
+
+ Args:
+ config: A dictionary containing the configuration for the Chart.js chart.
+ This should follow the standard Chart.js configuration format, but
+ note that function properties are not supported and all values must
+ be JSON serializable. See the Chart.js documentation for details
+ on available options: https://www.chartjs.org/docs/latest/
+
+ Returns:
+ A Mesop web component representing the Chart.js chart.
+ """
+ return me.insert_web_component(
+ name="chartjs-component", properties={"config": json.dumps(config)}
+ )
diff --git a/demo/hotkeys/app.py b/demo/hotkeys/app.py
new file mode 100644
index 00000000..0b7cd6a3
--- /dev/null
+++ b/demo/hotkeys/app.py
@@ -0,0 +1,91 @@
+"""Proof of concept of a webcomponent wrapper that adds hot key support.
+
+This could be made more reusable if we added on_enter to the text area (but this is
+more a hack since typically hot keys use enter+modifier to submit the input) With on
+enter, the child text area would get refreshed less, but it would still lose focus,
+which is not usable.
+
+Other potential usages:
+
+1. Global app hot keys
+2. Close dialogs with escape
+"""
+
+from hotkeys_component import HotKey, hotkeys_component
+
+import mesop as me
+
+instruction_text = """
+The hotkeys are active when you have focus in the text area.
+
+- Press Escape
+- Press Shift+Enter to submit text
+- Press CMD+s to save
+"""
+
+
+@me.stateclass
+class State:
+ input: str
+ output: str
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/hotkeys",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_connect_srcs=["https://cdn.jsdelivr.net"],
+ allowed_script_srcs=["https://cdn.jsdelivr.net"],
+ dangerously_disable_trusted_types=True,
+ ),
+)
+def page():
+ state = me.state(State)
+ hotkeys = [
+ # Ex: Text input submit
+ HotKey(key="Enter", modifiers=["shift"], action="submit"),
+ # Ex: Custom save override
+ HotKey(key="s", modifiers=["meta"], action="save"),
+ # Ex: Could be used for closing dialogs
+ HotKey(key="Escape", action="close"),
+ ]
+ with me.box(style=me.Style(margin=me.Margin.all(15))):
+ me.text(
+ "Hot key example",
+ type="headline-4",
+ style=me.Style(margin=me.Margin(bottom=5)),
+ )
+ me.markdown(instruction_text)
+
+ with hotkeys_component(hotkeys=hotkeys, on_hotkey_press=on_key_press):
+ me.textarea(
+ on_input=on_input, rows=5, style=me.Style(display="block", width="100%")
+ )
+ with me.box(
+ style=me.Style(
+ margin=me.Margin(top=15),
+ padding=me.Padding.all(10),
+ )
+ ):
+ me.text(state.output)
+
+
+def on_input(e: me.InputEvent | me.InputEnterEvent):
+ state = me.state(State)
+ state.input = e.value
+
+
+def on_key_press(e: me.WebEvent):
+ state = me.state(State)
+
+ if e.value["action"] == "submit":
+ state.output = "Pressed submit hotkey: " + state.input
+ elif e.value["action"] == "save":
+ state.output = "Pressed save hotkey."
+ elif e.value["action"] == "close":
+ state.output = "Pressed close hotkey."
diff --git a/demo/hotkeys/hotkeys_component.js b/demo/hotkeys/hotkeys_component.js
new file mode 100644
index 00000000..50e36788
--- /dev/null
+++ b/demo/hotkeys/hotkeys_component.js
@@ -0,0 +1,74 @@
+import {
+ LitElement,
+ html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+class HotKeys extends LitElement {
+ static properties = {
+ // Format: {key: String, action: String modifiers: String}
+ hotkeys: {type: Object},
+ keyPressEvent: {type: String},
+ triggeredAction: {type: String},
+ };
+
+ render() {
+ return html`
+
+
+
+ `;
+ }
+
+ _onKeyUp(e) {
+ if (!this.keyPressEvent || !this.triggeredAction) {
+ return;
+ }
+ this.dispatchEvent(
+ new MesopEvent(this.keyPressEvent, {
+ action: this.triggeredAction,
+ }),
+ );
+ // Reset the action so it won't get resent multiple times.
+ this.triggeredAction = '';
+ }
+
+ _onKeyDown(e) {
+ if (!this.keyPressEvent) {
+ return;
+ }
+
+ for (const hotkey of this.hotkeys) {
+ if (
+ e.key === hotkey.key &&
+ hotkey.modifiers.every((m) => this._isModifierPressed(e, m))
+ ) {
+ // Prevent default behavior for cases where we want to override browser level
+ // commands, such as Cmd+S.
+ e.preventDefault();
+ // Store the action and wait for key up to send the event.
+ // This is mainly to make text input hot key actions more efficient. If the
+ // event is on key up, then the on_enter event will trigger first, allowing
+ // the input text to be saved. This way we can avoid using the on_input to
+ // to store text (though this wouldn't work for text area unfortunately). This
+ // is problematic due to the way web components force a full rerender of the
+ // child components, which makes on_input unusable in this scenario since
+ // it will keep refreshing the child components. It will also lose focus on the
+ // text area.
+ this.triggeredAction = hotkey.action;
+ }
+ }
+ }
+
+ _isModifierPressed(event, modifierString) {
+ const modifierMap = {
+ 'ctrl': 'ctrlKey',
+ 'shift': 'shiftKey',
+ 'alt': 'altKey',
+ 'meta': 'metaKey',
+ };
+ const modifierProperty = modifierMap[modifierString.toLowerCase()];
+ return modifierProperty ? event[modifierProperty] : false;
+ }
+}
+
+customElements.define('hotkeys-component', HotKeys);
diff --git a/demo/hotkeys/hotkeys_component.py b/demo/hotkeys/hotkeys_component.py
new file mode 100644
index 00000000..cb2f5966
--- /dev/null
+++ b/demo/hotkeys/hotkeys_component.py
@@ -0,0 +1,31 @@
+from dataclasses import asdict, dataclass, field
+from typing import Any, Callable, Literal
+
+import mesop as me
+
+
+@dataclass
+class HotKey:
+ key: str
+ action: str
+ modifiers: list[Literal["ctrl", "alt", "shift", "meta"]] = field(
+ default_factory=list
+ )
+
+
+@me.web_component(path="./hotkeys_component.js")
+def hotkeys_component(
+ *,
+ hotkeys: list[HotKey],
+ on_hotkey_press: Callable[[me.WebEvent], Any],
+ key: str | None = None,
+):
+ return me.insert_web_component(
+ name="hotkeys-component",
+ key=key,
+ events={
+ "keyPressEvent": on_hotkey_press,
+ },
+ # Unable to serialize dataclass, so converting to dict.
+ properties={"hotkeys": [asdict(hotkey) for hotkey in hotkeys]},
+ )
diff --git a/demo/leaflet/app.py b/demo/leaflet/app.py
new file mode 100644
index 00000000..8b027008
--- /dev/null
+++ b/demo/leaflet/app.py
@@ -0,0 +1,68 @@
+"""Proof of concept of a webcomponent wrapper that adds Leaflet map support.
+
+Note: Does not implement full interactions with a map. This is meant as a
+ proof of concept that others can build upon for additional interactions with
+ a web map.
+"""
+
+from typing import Tuple
+
+from leaflet_component import leaflet_map_component
+from pydantic import BaseModel
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+ click_location: str = "No click yet"
+
+
+class MapClick(BaseModel):
+ latlng: Tuple[float, float]
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/leaflet",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_script_srcs=[
+ "https://cdn.jsdelivr.net",
+ "https://unpkg.com",
+ ],
+ dangerously_disable_trusted_types=True,
+ ),
+ stylesheets=[
+ "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
+ ],
+)
+def page():
+ initial_center = (51.505, -0.09) # London
+ initial_zoom = 13
+
+ markers_data = [
+ {"latlng": (51.5, -0.1), "popup_content": "Marker 1: London Eye"},
+ {"latlng": (51.51, -0.08), "popup_content": "Marker 2: Tower Bridge"},
+ ]
+
+ leaflet_map_component(
+ center=initial_center,
+ zoom=initial_zoom,
+ markers=markers_data,
+ on_click=on_map_click,
+ )
+ me.text(f"Map Clicked Location: {me.state(State).click_location}")
+
+
+def on_map_click(e: me.WebEvent):
+ click = MapClick(**e.value)
+ me.state(
+ State
+ ).click_location = (
+ f"Latitude: {click.latlng[0]}, Longitude: {click.latlng[1]}"
+ )
diff --git a/demo/leaflet/leaflet_component.js b/demo/leaflet/leaflet_component.js
new file mode 100644
index 00000000..2caa8f21
--- /dev/null
+++ b/demo/leaflet/leaflet_component.js
@@ -0,0 +1,108 @@
+// leaflet_map_component.js
+import {
+ LitElement,
+ html,
+ css,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+import 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; // Leaflet CDN
+
+export class LeafletMapComponent extends LitElement {
+ static properties = {
+ center: {type: Array}, // Expecting [latitude, longitude]
+ zoom: {type: Number},
+ markers: {type: Array}, // Array of marker objects
+ clickEvent: {type: String}, // Event handler ID for clicks
+ };
+
+ static styles = css`
+ #map {
+ height: 400px; /* Adjust map height as needed */
+ width: 100%;
+ }
+ `;
+
+ constructor() {
+ super();
+ this.center = [0.0, 0.0];
+ this.zoom = 13;
+ this.markers = [];
+ this.clickEvent = '';
+ this._map = null; // Internal Leaflet map instance
+ }
+
+ firstUpdated() {
+ this._initializeMap();
+ }
+
+ updated(changedProperties) {
+ if (changedProperties.has('center') || changedProperties.has('zoom')) {
+ this._updateMapView();
+ }
+ if (changedProperties.has('markers')) {
+ this._updateMarkers();
+ }
+ }
+
+ _initializeMap() {
+ if (this._map) return; // Prevent re-initialization
+
+ this._map = L.map(this.shadowRoot.querySelector('#map')).setView(
+ this.center,
+ this.zoom,
+ );
+
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution:
+ '© OpenStreetMap',
+ }).addTo(this._map);
+
+ this._map.on('click', (event) => {
+ if (this.clickEvent) {
+ this.dispatchEvent(
+ new MesopEvent(this.clickEvent, {
+ latlng: [event.latlng.lat, event.latlng.lng], // Send latlng back to Python
+ }),
+ );
+ }
+ });
+
+ this._updateMarkers(); // Initial marker rendering
+ }
+
+ _updateMapView() {
+ if (this._map) {
+ this._map.setView(this.center, this.zoom);
+ }
+ }
+
+ _updateMarkers() {
+ if (!this._map) return;
+
+ // Clear existing markers
+ if (this._markerLayer) {
+ this._map.removeLayer(this._markerLayer);
+ }
+ this._markerLayer = L.layerGroup().addTo(this._map); // Layer group for efficient marker management
+
+ this.markers.forEach((markerData) => {
+ const marker = L.marker(markerData.latlng);
+ if (markerData.popup_content) {
+ marker.bindPopup(markerData.popup_content);
+ }
+ marker.addTo(this._markerLayer);
+ });
+ }
+
+ render() {
+ return html`
+
+
+ `;
+ }
+}
+
+customElements.define('leaflet-map-component', LeafletMapComponent);
diff --git a/demo/leaflet/leaflet_component.py b/demo/leaflet/leaflet_component.py
new file mode 100644
index 00000000..eadfaf7e
--- /dev/null
+++ b/demo/leaflet/leaflet_component.py
@@ -0,0 +1,41 @@
+from typing import Any, Callable, Dict, List, Tuple
+
+import mesop as me
+
+
+@me.web_component(path="./leaflet_component.js")
+def leaflet_map_component(
+ *,
+ center: Tuple[float, float] = (0.0, 0.0), # [latitude, longitude]
+ zoom: int = 13,
+ markers: List[Dict[str, Any]] | None = None, # List of marker objects
+ on_click: Callable[[me.WebEvent], Any] | None = None,
+ key: str | None = None,
+):
+ """
+ A web component wrapper for a Leaflet map.
+
+ Args:
+ center: Initial map center as a tuple (latitude, longitude).
+ zoom: Initial map zoom level.
+ markers: A list of marker objects, each a dictionary with keys:
+ - latlng: Tuple (latitude, longitude) for marker position.
+ - popup_content: String content for the marker's popup (optional).
+ on_click: Callback function when the map is clicked (optional).
+ key: Unique key for the component.
+ """
+ events = {"clickEvent": on_click}
+
+ properties = {
+ "center": center,
+ "zoom": zoom,
+ # Ensure markers is always a list (empty if None)
+ "markers": markers if markers else [],
+ }
+
+ return me.insert_web_component(
+ name="leaflet-map-component",
+ key=key,
+ events=events,
+ properties=properties,
+ )
diff --git a/demo/main.py b/demo/main.py
index 9a2bc62a..392bb1bd 100644
--- a/demo/main.py
+++ b/demo/main.py
@@ -104,7 +104,13 @@ def _import_demo_subdir(demo_name: str) -> types.ModuleType:
import uploader as uploader
import video as video
+async_action = _import_demo_subdir("async_action")
+charts = _import_demo_subdir("charts")
copy_to_clipboard = _import_demo_subdir("copy_to_clipboard")
+hotkeys = _import_demo_subdir("hotkeys")
+leaflet = _import_demo_subdir("leaflet")
+plotly = _import_demo_subdir("plotly")
+svg_icon = _import_demo_subdir("svg_icon")
@dataclass
@@ -242,6 +248,20 @@ class Section:
Section(
name="Web Components",
examples=[
+ Example(
+ name="async_action",
+ extra_files=[
+ "async_action_component.py",
+ "async_action_component.js",
+ ],
+ ),
+ Example(
+ name="charts",
+ extra_files=[
+ "chartjs_component.py",
+ "chartjs_component.js",
+ ],
+ ),
Example(
name="copy_to_clipboard",
extra_files=[
@@ -249,6 +269,34 @@ class Section:
"copy_to_clipboard_component.js",
],
),
+ Example(
+ name="hotkeys",
+ extra_files=[
+ "hotkeys_component.py",
+ "hotkeys_component.js",
+ ],
+ ),
+ Example(
+ name="leaflet",
+ extra_files=[
+ "leaflet_component.py",
+ "leaflet_component.js",
+ ],
+ ),
+ Example(
+ name="plotly",
+ extra_files=[
+ "plotly_component.py",
+ "plotly_component.js",
+ ],
+ ),
+ Example(
+ name="svg_icon",
+ extra_files=[
+ "svg_icon_component.py",
+ "svg_icon_component.js",
+ ],
+ ),
],
),
Section(
diff --git a/demo/plotly/app.py b/demo/plotly/app.py
new file mode 100644
index 00000000..ad35b8dd
--- /dev/null
+++ b/demo/plotly/app.py
@@ -0,0 +1,28 @@
+from plotly_component import plotly_component
+
+import mesop as me
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/plotly",
+ # CAUTION: this disables an important web security feature and
+ # should not be used for most mesop apps.
+ #
+ # Disabling trusted types because plotly uses DomParser#parseFromString
+ # which violates TrustedHTML assignment.
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_script_srcs=[
+ "https://cdn.jsdelivr.net",
+ "https://cdn.plot.ly",
+ ],
+ dangerously_disable_trusted_types=True,
+ ),
+)
+def page():
+ plotly_component()
diff --git a/demo/plotly/plotly_component.js b/demo/plotly/plotly_component.js
new file mode 100644
index 00000000..64412023
--- /dev/null
+++ b/demo/plotly/plotly_component.js
@@ -0,0 +1,55 @@
+import {
+ LitElement,
+ html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+import 'https://cdn.plot.ly/plotly-2.32.0.min.js';
+
+class PlotlyComponent extends LitElement {
+ createRenderRoot() {
+ return this;
+ }
+
+ firstUpdated() {
+ this.renderPlot();
+ }
+
+ renderPlot() {
+ const trace1 = {
+ x: [1, 2, 3, 4, 5],
+ y: [10, 15, 13, 17, 21],
+ type: 'scatter',
+ mode: 'lines+markers',
+ marker: {color: 'red'},
+ name: 'Line 1',
+ };
+
+ const trace2 = {
+ x: [1, 2, 3, 4, 5],
+ y: [16, 5, 11, 9, 8],
+ type: 'scatter',
+ mode: 'lines+markers',
+ marker: {color: 'blue'},
+ name: 'Line 2',
+ };
+
+ const data = [trace1, trace2];
+
+ const layout = {
+ title: 'Simple Line Chart Example',
+ xaxis: {
+ title: 'X Axis',
+ },
+ yaxis: {
+ title: 'Y Axis',
+ },
+ };
+
+ Plotly.newPlot(document.getElementById('plot'), data, layout);
+ }
+
+ render() {
+ return html``;
+ }
+}
+
+customElements.define('plotly-component', PlotlyComponent);
diff --git a/demo/plotly/plotly_component.py b/demo/plotly/plotly_component.py
new file mode 100644
index 00000000..bc9e3d9b
--- /dev/null
+++ b/demo/plotly/plotly_component.py
@@ -0,0 +1,6 @@
+import mesop as me
+
+
+@me.web_component(path="./plotly_component.js")
+def plotly_component():
+ return me.insert_web_component(name="plotly-component")
diff --git a/demo/requirements.txt b/demo/requirements.txt
index d5fe635f..e0745c55 100644
--- a/demo/requirements.txt
+++ b/demo/requirements.txt
@@ -6,3 +6,4 @@ Werkzeug==3.0.6
matplotlib
numpy
pandas
+pydantic
diff --git a/demo/svg_icon/app.py b/demo/svg_icon/app.py
new file mode 100644
index 00000000..5fb82c25
--- /dev/null
+++ b/demo/svg_icon/app.py
@@ -0,0 +1,25 @@
+from svg_icon_component import svg_icon
+
+import mesop as me
+
+
+def on_load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=on_load,
+ path="/svg_icon",
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://mesop-dev.github.io"],
+ allowed_script_srcs=["https://cdn.jsdelivr.net"],
+ allowed_connect_srcs=["https://cdn.jsdelivr.net"],
+ dangerously_disable_trusted_types=True,
+ ),
+ title="svg-icon",
+)
+def page():
+ with me.box(style=me.Style(width="200px", padding=me.Padding.all(20))):
+ svg_icon(
+ svg=""""""
+ )
diff --git a/demo/svg_icon/svg_icon_component.js b/demo/svg_icon/svg_icon_component.js
new file mode 100644
index 00000000..b87d5ce2
--- /dev/null
+++ b/demo/svg_icon/svg_icon_component.js
@@ -0,0 +1,18 @@
+import {
+ LitElement,
+ html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+export class SVGIconComponent extends LitElement {
+ static properties = {
+ svg: {type: String},
+ };
+
+ render() {
+ const span = document.createElement('span');
+ span.innerHTML = this.svg;
+ return html`${span}`;
+ }
+}
+
+customElements.define('svg-icon', SVGIconComponent);
diff --git a/demo/svg_icon/svg_icon_component.py b/demo/svg_icon/svg_icon_component.py
new file mode 100644
index 00000000..0750077b
--- /dev/null
+++ b/demo/svg_icon/svg_icon_component.py
@@ -0,0 +1,14 @@
+import mesop as me
+
+
+@me.web_component(path="./svg_icon_component.js")
+def svg_icon(
+ *,
+ svg: str | None = None,
+ key: str | None = None,
+):
+ return me.insert_web_component(
+ name="svg-icon",
+ key=key,
+ properties={"svg": svg},
+ )