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}, + )