diff --git a/examples/vue2/nested_reactive.py b/examples/vue2/nested_reactive.py
new file mode 100644
index 0000000..d6a9876
--- /dev/null
+++ b/examples/vue2/nested_reactive.py
@@ -0,0 +1,119 @@
+# Deep Reactive does not work with vue2
+
+from trame.app import get_server
+from trame.widgets import html, client
+from trame.ui.html import DivLayout
+
+server = get_server()
+server.client_type = "vue2"
+state, ctrl = server.state, server.controller
+
+state.nested_1 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+state.nested_2 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+
+state.nested_3 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+
+
+@state.change("nested_1")
+def update_nested_1(nested_1, **kwargs):
+ print("nested 1")
+
+
+@state.change("nested_2")
+def update_nested_2(nested_2, **kwargs):
+ print("nested 2")
+
+
+@state.change("nested_3")
+def update_nested_3(nested_3, **kwargs):
+ print("nested 3")
+
+
+with DivLayout(server) as layout:
+ # Workaround / old fashion
+ html.Input(
+ type="range",
+ value=("nested_2.a",),
+ raw_attrs=[
+ "@input=\"nested_2.a = $event.target.value; flushState('nested_2')\""
+ ],
+ min=1,
+ max=10,
+ step=1,
+ )
+ with html.Div("Nested 1 - {{ nested_1.a }}"):
+ html.Input(
+ type="range",
+ value=("nested_1.a",),
+ raw_attrs=[
+ "@input=\"nested_1.a = $event.target.value; flushState('nested_1')\""
+ ],
+ min=1,
+ max=10,
+ step=1,
+ )
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_1.sliders",
+ key="i",
+ type="range",
+ value=("s.value",),
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ raw_attrs=[
+ "@input=\"s.value = $event.target.value; flushState('nested_1')\""
+ ],
+ )
+
+ # New deep reactive
+ with client.DeepReactive("nested_2") as dr:
+ with html.Div("Nested 2 - {{ nested_2.a }}"):
+ html.Input(type="range", v_model="nested_2.a", min=1, max=10, step=1)
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_2.sliders",
+ key="i",
+ type="range",
+ v_model="s.value",
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ )
+
+ # Not working
+ with html.Div("Nested 3 - {{ nested_3.a }}"):
+ html.Input(type="range", v_model="nested_3.a", min=1, max=10, step=1)
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_3.sliders",
+ key="i",
+ type="range",
+ v_model="s.value",
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ )
+
+server.start()
diff --git a/examples/vue3/nested_reactive.py b/examples/vue3/nested_reactive.py
new file mode 100644
index 0000000..8480e97
--- /dev/null
+++ b/examples/vue3/nested_reactive.py
@@ -0,0 +1,118 @@
+from trame.app import get_server
+from trame.widgets import html, client
+from trame.ui.html import DivLayout
+
+server = get_server()
+server.client_type = "vue3"
+state, ctrl = server.state, server.controller
+
+state.nested_1 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+state.nested_2 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+
+state.nested_3 = {
+ "sliders": [
+ {"value": 10, "min": 1, "max": 20, "step": 2},
+ {"value": 1, "min": 1, "max": 20, "step": 1},
+ {"value": 5, "min": 1, "max": 20, "step": 5},
+ ],
+ "a": 10,
+}
+
+
+@state.change("nested_1")
+def update_nested_1(nested_1, **kwargs):
+ print("nested 1")
+
+
+@state.change("nested_2")
+def update_nested_2(nested_2, **kwargs):
+ print("nested 2")
+
+
+@state.change("nested_3")
+def update_nested_3(nested_3, **kwargs):
+ print("nested 3")
+
+
+with DivLayout(server) as layout:
+ # Workaround / old fashion
+ html.Div("{{ nested_2 }}")
+ html.Input(
+ type="range",
+ value=("nested_2.a",),
+ raw_attrs=[
+ "@input=\"nested_2.a = $event.target.value; flushState('nested_2')\""
+ ],
+ min=1,
+ max=10,
+ step=1,
+ )
+ with html.Div("Nested 1 - {{ nested_1.a }}"):
+ html.Input(
+ type="range",
+ value=("nested_1.a",),
+ raw_attrs=[
+ "@input=\"nested_1.a = $event.target.value; flushState('nested_1')\""
+ ],
+ min=1,
+ max=10,
+ step=1,
+ )
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_1.sliders",
+ key="i",
+ type="range",
+ value=("s.value",),
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ raw_attrs=[
+ "@input=\"s.value = $event.target.value; flushState('nested_1')\""
+ ],
+ )
+
+ # New deep reactive
+ with client.DeepReactive("nested_2") as dr:
+ with html.Div("Nested 2 - {{ nested_2.a }}"):
+ html.Input(type="range", v_model="nested_2.a", min=1, max=10, step=1)
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_2.sliders",
+ key="i",
+ type="range",
+ v_model="s.value",
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ )
+
+ # Not working
+ with html.Div("Nested 3 - {{ nested_3.a }}"):
+ html.Input(type="range", v_model="nested_3.a", min=1, max=10, step=1)
+ with html.Div("Sliders"):
+ html.Input(
+ v_for="s, i in nested_3.sliders",
+ key="i",
+ type="range",
+ v_model="s.value",
+ min=("s.min",),
+ max=("s.max",),
+ step=("s.step",),
+ )
+
+server.start()
diff --git a/trame_client/widgets/trame.py b/trame_client/widgets/trame.py
index 26eba62..7bd3d40 100644
--- a/trame_client/widgets/trame.py
+++ b/trame_client/widgets/trame.py
@@ -9,6 +9,7 @@
"Getter",
"ClientStateChange",
"ClientTriggers",
+ "DeepReactive",
"LifeCycleMonitor",
"SizeObserver",
]
@@ -259,6 +260,35 @@ def call(self, method, *args):
self.server.js_call(self.__name, "emit", method, *args)
+# -----------------------------------------------------------------------------
+# TrameDeepReactive
+# -----------------------------------------------------------------------------
+class DeepReactive(AbstractElement):
+ """Create a deeply reactive state from state variable name.
+ The provided name can not be reactive.
+ It needs to be statically defined in Python like in the example blow.
+
+ This component only works with client_type="vue3".
+
+ with DeepReactive(my_nested_var):
+ html.Input(v_model=my_nested_var.txt_1)
+ html.Input(v_model=my_nested_var.txt_2)
+ """
+
+ def __init__(
+ self,
+ name=None,
+ children=None,
+ **kwargs,
+ ):
+ super().__init__("trame-deep-reactive", children, name=name, **kwargs)
+ self._attr_names += [
+ "name",
+ ]
+
+ self._attributes["slot"] = f'v-slot="{{ value: {name} }}"'
+
+
# -----------------------------------------------------------------------------
# TrameLifeCycleMonitor
# -----------------------------------------------------------------------------
diff --git a/vue2-app/src/components/TrameDeepReactive.js b/vue2-app/src/components/TrameDeepReactive.js
new file mode 100644
index 0000000..dd3cb5a
--- /dev/null
+++ b/vue2-app/src/components/TrameDeepReactive.js
@@ -0,0 +1,47 @@
+const { inject, reactive, onBeforeMount, onBeforeUnmount, watch } = window.Vue;
+
+export default {
+ props: ['name'],
+ setup(props) {
+ const trame = inject('trame');
+ const value = reactive({});
+ let trameStr = null;
+ let refStr = null;
+
+ function pushChange() {
+ console.log('pushChange');
+ refStr = JSON.stringify(value);
+ if (refStr !== trameStr) {
+ trame.state.set(props.name, JSON.parse(refStr));
+ }
+ }
+
+ function fetchValue() {
+ console.log('fetchValue');
+ trameStr = JSON.stringify(trame.state.get(props.name));
+ if (trameStr !== refStr) {
+ Object.assign(value, JSON.parse(trameStr));
+ }
+ }
+
+ function updateState(keys) {
+ if (keys.includes(props.name)) {
+ fetchValue();
+ }
+ }
+
+ onBeforeMount(() => {
+ trame.$on('stateChange', updateState);
+ fetchValue();
+ });
+
+ onBeforeUnmount(() => {
+ trame.$off('stateChange', updateState);
+ });
+
+ watch(value, pushChange);
+
+ return { value };
+ },
+ template: '',
+};
diff --git a/vue2-app/src/components/index.js b/vue2-app/src/components/index.js
index d729e8b..967b7e5 100644
--- a/vue2-app/src/components/index.js
+++ b/vue2-app/src/components/index.js
@@ -6,6 +6,7 @@ import TrameTemplate from './ServerTemplate';
import TrameStateResolver from './StateResolver';
import TrameStyle from './Style';
import TrameScript from './TrameScript';
+import TrameDeepReactive from './TrameDeepReactive';
import TrameExec from './TrameExec';
import TrameGetter from './Getter';
import TrameClientStateChange from './ClientStateChange';
@@ -22,6 +23,7 @@ export default {
TrameStateResolver,
TrameScript,
TrameStyle,
+ TrameDeepReactive,
TrameExec,
TrameGetter,
TrameClientStateChange,
diff --git a/vue3-app/src/components/TrameDeepReactive.js b/vue3-app/src/components/TrameDeepReactive.js
new file mode 100644
index 0000000..122e7a4
--- /dev/null
+++ b/vue3-app/src/components/TrameDeepReactive.js
@@ -0,0 +1,45 @@
+const { inject, reactive, onBeforeMount, onBeforeUnmount, watch } = window.Vue;
+
+export default {
+ props: ["name"],
+ setup(props) {
+ const trame = inject("trame");
+ const value = reactive({});
+ let trameStr = null;
+ let refStr = null;
+
+ function pushChange() {
+ refStr = JSON.stringify(value);
+ if (refStr !== trameStr) {
+ trame.state.set(props.name, JSON.parse(refStr));
+ }
+ }
+
+ function fetchValue() {
+ trameStr = JSON.stringify(trame.state.get(props.name));
+ if (trameStr !== refStr) {
+ Object.assign(value, JSON.parse(trameStr));
+ }
+ }
+
+ function updateState({ type, keys }) {
+ if (type === "dirty-state" && keys.includes(props.name)) {
+ fetchValue();
+ }
+ }
+
+ onBeforeMount(() => {
+ trame.state.addListener(updateState);
+ fetchValue();
+ });
+
+ onBeforeUnmount(() => {
+ trame.state.removeListener(updateState);
+ });
+
+ watch(value, pushChange);
+
+ return { value };
+ },
+ template: '',
+};
diff --git a/vue3-app/src/components/index.js b/vue3-app/src/components/index.js
index 24297d3..9f68931 100644
--- a/vue3-app/src/components/index.js
+++ b/vue3-app/src/components/index.js
@@ -8,6 +8,7 @@ import TrameStyle from "./TrameStyle";
import TrameTemplate from "./TrameTemplate";
import TrameClientStateChange from "./TrameClientStateChange";
import TrameClientTriggers from "./TrameClientTriggers";
+import TrameDeepReactive from "./TrameDeepReactive";
import TrameLifeCycleMonitor from "./TrameLifeCycleMonitor";
import TrameSizeObserver from "./TrameSizeObserver";
@@ -22,6 +23,7 @@ export default {
TrameTemplate,
TrameClientStateChange,
TrameClientTriggers,
+ TrameDeepReactive,
TrameLifeCycleMonitor,
TrameSizeObserver,
};