Skip to content

Commit

Permalink
feat(DeepReactive): add vue3 component to enable deep reactivity
Browse files Browse the repository at this point in the history
  • Loading branch information
jourdain committed Oct 20, 2024
1 parent 1557840 commit 3ea4dc9
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 0 deletions.
119 changes: 119 additions & 0 deletions examples/vue2/nested_reactive.py
Original file line number Diff line number Diff line change
@@ -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()
118 changes: 118 additions & 0 deletions examples/vue3/nested_reactive.py
Original file line number Diff line number Diff line change
@@ -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()
30 changes: 30 additions & 0 deletions trame_client/widgets/trame.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"Getter",
"ClientStateChange",
"ClientTriggers",
"DeepReactive",
"LifeCycleMonitor",
"SizeObserver",
]
Expand Down Expand Up @@ -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
# -----------------------------------------------------------------------------
Expand Down
47 changes: 47 additions & 0 deletions vue2-app/src/components/TrameDeepReactive.js
Original file line number Diff line number Diff line change
@@ -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: '<slot :value="value"></slot>',
};
2 changes: 2 additions & 0 deletions vue2-app/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,6 +23,7 @@ export default {
TrameStateResolver,
TrameScript,
TrameStyle,
TrameDeepReactive,
TrameExec,
TrameGetter,
TrameClientStateChange,
Expand Down
45 changes: 45 additions & 0 deletions vue3-app/src/components/TrameDeepReactive.js
Original file line number Diff line number Diff line change
@@ -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: '<slot :value="value"></slot>',
};
Loading

0 comments on commit 3ea4dc9

Please sign in to comment.