diff --git a/examples/custom_vue_component/README.md b/examples/custom_vue_component/README.md
new file mode 100644
index 000000000..820a26157
--- /dev/null
+++ b/examples/custom_vue_component/README.md
@@ -0,0 +1,21 @@
+# Custom Vue Component
+
+This example demonstrates how to create and use custom Vue components in NiceGUI.
+One component is implemented using JavaScript, the other using Vue's Single-File Component (SFC) syntax.
+
+## Counter Component (counter.js)
+
+The `Counter` component is a simple counter that increments a value.
+On change, it emits an event with the new value.
+A reset method allows to reset the counter to 0.
+
+The JavaScript code in `counter.js` defines the front-end logic of the component using JavaScript.
+
+## OnOff Component (on_off.vue)
+
+The `OnOff` component is a simple toggle that switches between on and off.
+On change, it emits an event with the new value.
+A reset method is also provided to reset the toggle to off.
+
+The Single-File Component in `on_off.vue` defines the front-end logic of the component using Vue 2.
+In contrast to the JavaScript code in `counter.js`, it splits the template, script, and style into separate sections.
diff --git a/examples/custom_vue_component/counter.js b/examples/custom_vue_component/counter.js
index 16b40fd16..f005a9920 100644
--- a/examples/custom_vue_component/counter.js
+++ b/examples/custom_vue_component/counter.js
@@ -1,9 +1,13 @@
// NOTE: Make sure to reload the browser with cache disabled after making changes to this file.
export default {
template: `
- `,
+
+ `,
+ props: {
+ title: String,
+ },
data() {
return {
value: 0,
@@ -18,7 +22,4 @@ export default {
this.value = 0;
},
},
- props: {
- title: String,
- },
};
diff --git a/examples/custom_vue_component/main.py b/examples/custom_vue_component/main.py
index bd373eecf..f6b104629 100755
--- a/examples/custom_vue_component/main.py
+++ b/examples/custom_vue_component/main.py
@@ -1,17 +1,16 @@
#!/usr/bin/env python3
from counter import Counter
+from on_off import OnOff
from nicegui import ui
-ui.markdown('''
-#### Try the new click counter!
+with ui.row(align_items='center'):
+ counter = Counter('Count', on_change=lambda e: ui.notify(f'The value changed to {e.args}.'))
+ ui.button('Reset', on_click=counter.reset).props('outline')
-Click to increment its value.
-''')
-with ui.card():
- counter = Counter('Clicks', on_change=lambda e: ui.notify(f'The value changed to {e.args}.'))
+with ui.row(align_items='center'):
+ on_off = OnOff('State', on_change=lambda e: ui.notify(f'The value changed to {e.args}.'))
+ ui.button('Reset', on_click=on_off.reset).props('outline')
-ui.button('Reset', on_click=counter.reset).props('small outline')
-
-ui.run()
+ui.run(uvicorn_reload_includes='*.py,*.js,*.vue')
diff --git a/examples/custom_vue_component/on_off.py b/examples/custom_vue_component/on_off.py
new file mode 100644
index 000000000..90328b8a8
--- /dev/null
+++ b/examples/custom_vue_component/on_off.py
@@ -0,0 +1,14 @@
+from typing import Callable, Optional
+
+from nicegui.element import Element
+
+
+class OnOff(Element, component='on_off.vue'):
+
+ def __init__(self, title: str, *, on_change: Optional[Callable] = None) -> None:
+ super().__init__()
+ self._props['title'] = title
+ self.on('change', on_change)
+
+ def reset(self) -> None:
+ self.run_method('reset')
diff --git a/examples/custom_vue_component/on_off.vue b/examples/custom_vue_component/on_off.vue
new file mode 100644
index 000000000..f5d91b1d5
--- /dev/null
+++ b/examples/custom_vue_component/on_off.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
diff --git a/nicegui/elements/keyboard.js b/nicegui/elements/keyboard.js
index ee9257e5e..f86c1bff4 100644
--- a/nicegui/elements/keyboard.js
+++ b/nicegui/elements/keyboard.js
@@ -2,10 +2,15 @@ export default {
mounted() {
for (const event of this.events) {
document.addEventListener(event, (evt) => {
+ // https://github.com/zauberzeug/nicegui/issues/4290
+ if (!(evt instanceof KeyboardEvent)) return;
+
// https://stackoverflow.com/a/36469636/3419103
const focus = document.activeElement;
if (focus && this.ignore.includes(focus.tagName.toLowerCase())) return;
+
if (evt.repeat && !this.repeating) return;
+
this.$emit("key", {
action: event,
altKey: evt.altKey,
diff --git a/nicegui/elements/leaflet.js b/nicegui/elements/leaflet.js
index 1d554143f..cb1f3cb16 100644
--- a/nicegui/elements/leaflet.js
+++ b/nicegui/elements/leaflet.js
@@ -10,12 +10,14 @@ export default {
draw_control: Object,
resource_path: String,
hide_drawn_items: Boolean,
+ additional_resources: Array,
},
async mounted() {
await this.$nextTick(); // NOTE: wait for window.path_prefix to be set
await Promise.all([
loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.css`),
loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.js`),
+ ...this.additional_resources.map((resource) => loadResource(resource)),
]);
if (this.draw_control) {
await Promise.all([
diff --git a/nicegui/elements/leaflet.py b/nicegui/elements/leaflet.py
index c779d2041..5cf41e56a 100644
--- a/nicegui/elements/leaflet.py
+++ b/nicegui/elements/leaflet.py
@@ -1,6 +1,6 @@
import asyncio
from pathlib import Path
-from typing import Any, Dict, List, Tuple, Union, cast
+from typing import Any, Dict, List, Optional, Tuple, Union, cast
from typing_extensions import Self
@@ -27,6 +27,7 @@ def __init__(self,
options: Dict = {}, # noqa: B006
draw_control: Union[bool, Dict] = False,
hide_drawn_items: bool = False,
+ additional_resources: Optional[List[str]] = None,
) -> None:
"""Leaflet map
@@ -37,6 +38,7 @@ def __init__(self,
:param draw_control: whether to show the draw toolbar (default: False)
:param options: additional options passed to the Leaflet map (default: {})
:param hide_drawn_items: whether to hide drawn items on the map (default: False, *added in version 2.0.0*)
+ :param additional_resources: additional resources like CSS or JS files to load (default: None)
"""
super().__init__()
self.add_resource(Path(__file__).parent / 'lib' / 'leaflet')
@@ -51,6 +53,7 @@ def __init__(self,
self._props['options'] = {**options}
self._props['draw_control'] = draw_control
self._props['hide_drawn_items'] = hide_drawn_items
+ self._props['additional_resources'] = additional_resources or []
self.on('init', self._handle_init)
self.on('map-moveend', self._handle_moveend)
diff --git a/poetry.lock b/poetry.lock
index 95875eecb..6ea44956c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -281,13 +281,13 @@ files = [
[[package]]
name = "certifi"
-version = "2024.12.14"
+version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
- {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
+ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
+ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
diff --git a/website/documentation/content/leaflet_documentation.py b/website/documentation/content/leaflet_documentation.py
index d41b8e1f5..94b01c273 100644
--- a/website/documentation/content/leaflet_documentation.py
+++ b/website/documentation/content/leaflet_documentation.py
@@ -196,4 +196,18 @@ async def page():
m.run_map_method('fitBounds', [[bounds['_southWest'], bounds['_northEast']]])
ui.timer(0, page, once=True) # HIDE
+
+@doc.demo('Leaflet Plugins', '''
+ You can add plugins to the map by passing the URLs of JS and CSS files to the `additional_resources` parameter.
+ This demo shows how to add the [Leaflet.RotatedMarker](https://github.com/bbecquet/Leaflet.RotatedMarker) plugin.
+ It allows you to rotate markers by a given `rotationAngle`.
+''')
+def leaflet_plugins() -> None:
+ m = ui.leaflet((51.51, -0.09), additional_resources=[
+ 'https://unpkg.com/leaflet-rotatedmarker@0.2.0/leaflet.rotatedMarker.js',
+ ])
+ m.marker(latlng=(51.51, -0.091), options={'rotationAngle': -30})
+ m.marker(latlng=(51.51, -0.090), options={'rotationAngle': 30})
+
+
doc.reference(ui.leaflet)