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, };