diff --git a/examples/PlainPython/Menu/README.md b/examples/PlainPython/Menu/README.md
new file mode 100644
index 00000000..7458449a
--- /dev/null
+++ b/examples/PlainPython/Menu/README.md
@@ -0,0 +1,20 @@
+# Menu example
+
+This example illustrates how to implement a dropdown menu in vuetify using `v-slot:activator` with `v-bind` and `v-on` like this one:
+
+```html
+
+
+
+ Dropdown
+
+
+
+ {{ item.title }}
+
+
+
+
+```
+
+And demonstrate how to pass variables from Vue to a Python method.
diff --git a/examples/PlainPython/Menu/app.py b/examples/PlainPython/Menu/app.py
new file mode 100644
index 00000000..d9eac957
--- /dev/null
+++ b/examples/PlainPython/Menu/app.py
@@ -0,0 +1,31 @@
+from trame import state
+from trame.html import vuetify
+from trame.layouts import SinglePage
+
+layout = SinglePage("Menu example")
+
+state.menu_items = ["one", "two", "three"]
+
+
+def print_item(item):
+ print("Clicked on", item)
+
+
+with layout.toolbar:
+ vuetify.VSpacer()
+ with vuetify.VMenu():
+ with vuetify.Template(v_slot_activator="{ on, attrs }"):
+ with vuetify.VBtn(icon=True, v_bind="attrs", v_on="on", v_on_click="test"):
+ vuetify.VIcon("mdi-dots-vertical")
+ with vuetify.VList(), vuetify.VListItem(
+ v_for="(item, i) in menu_items",
+ key="i",
+ value=["item"],
+ ):
+ vuetify.VBtn(
+ "{{ item }}",
+ click=(print_item, "[item]"),
+ )
+
+if __name__ == "__main__":
+ layout.start()
diff --git a/trame/html/__init__.py b/trame/html/__init__.py
index 4a1232e8..6c7f0dca 100644
--- a/trame/html/__init__.py
+++ b/trame/html/__init__.py
@@ -15,6 +15,35 @@ def py2js_key(key):
return key.replace("_", "-")
+def js2py_key(key):
+ return key.replace("-", "_")
+
+
+def build_attr_names(name_prefix, key_names, kwargs):
+ """Used to generate a list of attr_names with a common name_prefix."""
+ attr_names = []
+ for key_name in key_names:
+ safe_name_prefix = js2py_key(name_prefix)
+ safe_name = js2py_key(key_name).replace(".", "_")
+ if "" in safe_name:
+ safe_header, safe_tail = safe_name.split("")
+ header, tail = key_name.split("")
+ for key in kwargs:
+ if key.startswith(header):
+ dyna_name = key[len(header) : -len(tail)]
+ attr_names.append(
+ (
+ f"{safe_name_prefix}_{safe_header}{dyna_name}{safe_tail}",
+ f"{name_prefix}:{header}{dyna_name}{tail}",
+ )
+ )
+ else:
+ attr_names.append(
+ (f"{safe_name_prefix}_{safe_name}", f"{name_prefix}:{key_name}")
+ )
+ return attr_names
+
+
class ElementContextManager:
def __init__(self):
self.element_stack = []
@@ -33,6 +62,36 @@ def add_child(self, elem):
HTML_CTX = ElementContextManager()
+key_names = [
+ "delete",
+ "down",
+ "enter",
+ "esc",
+ "left",
+ "right",
+ "space",
+ "tab",
+ "up",
+]
+v_on_names = [
+ "click.capture",
+ "click.once",
+ "click.prevent",
+ "click.prevent.self",
+ "click.self.prevent",
+ "click.self",
+ "click.stop.prevent",
+ "click.stop",
+ "click",
+ "scroll.passive",
+ "scroll",
+ "submit.prevent",
+ "submit",
+ *["keyup." + k for k in key_names],
+ *["keydown." + k for k in key_names],
+]
+v_bind_names = ["class", "style"]
+
class AbstractElement:
"""
@@ -72,6 +131,8 @@ class AbstractElement:
:param v_if: See |vue_doc_link| for more info
:param v_show: See |vue_doc_link| for more info
:param v_for: See |vue_doc_link| for more info
+ :param v_on: See |vue_doc_link| for more info
+ :param v_bind: See |vue_doc_link| for more info
:param key: See |vue_doc_link| for more info
Events - See |mdn_event_link| for more info
@@ -113,17 +174,21 @@ def __init__(self, _elem_name, children=None, **kwargs):
"style",
("key", ":key"),
# default vue.js directives
- "v_text",
- "v_html",
- "v_show",
- "v_if",
- "v_else",
+ "v_bind",
"v_else_if",
+ "v_else",
"v_for",
+ "v_html",
+ "v_if",
"v_model",
- "v_pre",
+ "v_on",
"v_once",
+ "v_pre",
+ "v_show",
+ "v_text",
]
+ self._attr_names += build_attr_names("v-on", v_on_names, kwargs)
+ self._attr_names += build_attr_names("v-bind", v_bind_names, kwargs)
self._event_names += [
"click",
"mousedown",
@@ -505,22 +570,7 @@ class Template(AbstractElement):
def __init__(self, children=None, **kwargs):
super().__init__("template", children, **kwargs)
self._attr_names += ["v_slot"]
- for slot_name in Template.slot_names:
- safe_name = slot_name.replace("-", "_").replace(".", "_")
- if "" in safe_name:
- safe_header, safe_tail = safe_name.split("")
- header, tail = slot_name.split("")
- for key in kwargs:
- if key.startswith(header):
- dyna_name = key[len(header) : -len(tail)]
- self._attr_names.append(
- (
- f"v_slot_{safe_header}{dyna_name}{safe_tail}",
- f"v-slot:{header}{dyna_name}{tail}",
- )
- )
- else:
- self._attr_names.append((f"v_slot_{safe_name}", f"v-slot:{slot_name}"))
+ self._attr_names += build_attr_names("v-slot", Template.slot_names, kwargs)
class StateChange(AbstractElement):