Skip to content

Commit 17bc53c

Browse files
committed
mount() now returns unmount callback
1 parent 4f31dbf commit 17bc53c

File tree

3 files changed

+136
-45
lines changed

3 files changed

+136
-45
lines changed

src/idom/client/app/packages/idom-client-react/src/component.js

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,16 @@ export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
3030

3131
function Element({ model }) {
3232
if (model.importSource) {
33-
return html`<${ImportedElement} model=${model} />`;
33+
if (model.importSource.exportsMount) {
34+
return html`<${MountedImportSource} model=${model} />`;
35+
} else {
36+
return html`<${DynamicImportSource} model=${model} />`;
37+
}
3438
} else {
3539
return html`<${StandardElement} model=${model} />`;
3640
}
3741
}
3842

39-
function ImportedElement({ model }) {
40-
const config = react.useContext(LayoutConfigContext);
41-
const mountPoint = react.useRef(null);
42-
43-
react.useEffect(() => {
44-
config
45-
.loadImportSource(
46-
model.importSource.source,
47-
model.importSource.sourceType
48-
)
49-
.then((module) => {
50-
mountImportSource(mountPoint.current, module, model, config);
51-
});
52-
});
53-
54-
return html`<div ref=${mountPoint} />`;
55-
}
56-
5743
function StandardElement({ model }) {
5844
const config = react.useContext(LayoutConfigContext);
5945
const children = elementChildren(model);
@@ -65,6 +51,73 @@ function StandardElement({ model }) {
6551
}
6652
}
6753

54+
function MountedImportSource({ model }) {
55+
const config = react.useContext(LayoutConfigContext);
56+
const mountPoint = react.useRef(null);
57+
const fallback = model.importSource.fallback;
58+
59+
react.useEffect(() => {
60+
const unmountPromise = loadImportSource(config, model).then((module) => {
61+
const element = mountPoint.current;
62+
const component = module[model.tagName];
63+
const props = elementAttributes(model, config.sendEvent);
64+
const children = model.children;
65+
66+
let args;
67+
if (!children) {
68+
args = [element, component, props, config];
69+
} else {
70+
args = [element, component, props, config, children];
71+
}
72+
73+
reactDOM.unmountComponentAtNode(element);
74+
return module.mount(...args);
75+
});
76+
return () => {
77+
unmountPromise.then((unmount) => (unmount ? unmount() : undefined));
78+
};
79+
}, [model, mountPoint, config]);
80+
81+
return createImportSourceFallback(mountPoint, fallback);
82+
}
83+
84+
function DynamicImportSource({ model }) {
85+
const config = react.useContext(LayoutConfigContext);
86+
const mountPoint = react.useRef(null);
87+
const fallback = model.importSource.fallback;
88+
89+
react.useEffect(() => {
90+
console.log(elementAttributes(model, config.sendEvent));
91+
loadImportSource(config, model).then((module) => {
92+
reactDOM.render(
93+
react.createElement(
94+
module[model.tagName],
95+
elementAttributes(model, config.sendEvent),
96+
...elementChildren(model)
97+
),
98+
mountPoint.current
99+
);
100+
});
101+
return () => {
102+
reactDOM.unmountComponentAtNode(mountPoint.current);
103+
};
104+
}, [model, mountPoint]);
105+
106+
return createImportSourceFallback(mountPoint, fallback);
107+
}
108+
109+
function createImportSourceFallback(mountPointRef, fallback) {
110+
if (!fallback) {
111+
return html`<div ref=${mountPointRef} />`;
112+
} else if (typeof fallback == "string") {
113+
return html`<div ref=${mountPointRef}>${fallback}</div>`;
114+
} else {
115+
return html`<div ref=${mountPointRef}>
116+
<${StandardElement} model=${fallback} />
117+
</div>`;
118+
}
119+
}
120+
68121
function elementChildren(model) {
69122
if (!model.children) {
70123
return [];
@@ -84,10 +137,9 @@ function elementAttributes(model, sendEvent) {
84137
const attributes = Object.assign({}, model.attributes);
85138

86139
if (model.eventHandlers) {
87-
Object.keys(model.eventHandlers).forEach((eventName) => {
88-
const eventSpec = model.eventHandlers[eventName];
140+
for (const [eventName, eventSpec] of Object.entries(model.eventHandlers)) {
89141
attributes[eventName] = eventHandler(sendEvent, eventSpec);
90-
});
142+
}
91143
}
92144

93145
return attributes;
@@ -119,23 +171,11 @@ function eventHandler(sendEvent, eventSpec) {
119171
};
120172
}
121173

122-
function mountImportSource(element, module, model, config) {
123-
if (model.importSource.exportsMount) {
124-
const props = elementAttributes(model, config.sendEvent);
125-
if (model.children) {
126-
props.children = model.children;
127-
}
128-
module.mount(element, module[model.tagName], props);
129-
} else {
130-
reactDOM.render(
131-
react.createElement(
132-
module[model.tagName],
133-
elementAttributes(model, config.sendEvent),
134-
...elementChildren(model)
135-
),
136-
element
137-
);
138-
}
174+
function loadImportSource(config, model) {
175+
return config.loadImportSource(
176+
model.importSource.source,
177+
model.importSource.sourceType
178+
);
139179
}
140180

141181
function useInplaceJsonPatch(doc) {
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
export function mount(element, component, props) {
22
component(element, props);
3+
incrAttribute(element, "mountCount");
4+
return () => {
5+
incrAttribute(element, "unmountCount");
6+
while (element.firstChild) {
7+
element.removeChild(element.lastChild);
8+
}
9+
};
310
}
411

5-
export function SetInnerHtml(element, props) {
6-
element.innerHTML = props.innerHTML;
12+
export function ShowText(element, props) {
13+
const innerEl = document.createElement("h1");
14+
innerEl.setAttribute("id", props.id);
15+
innerEl.appendChild(document.createTextNode(props.text));
16+
element.appendChild(innerEl);
17+
}
18+
19+
function incrAttribute(element, attribute) {
20+
const current = element.getAttribute(attribute);
21+
if (!current) {
22+
element.setAttribute(attribute, 1);
23+
} else {
24+
element.setAttribute(attribute, parseInt(current) + 1);
25+
}
726
}

tests/test_client/test_app.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,50 @@ def NewComponent():
4646
driver.find_element_by_id("new-component-1")
4747

4848

49-
def test_vanilla_js_component_with_mount(driver, display):
49+
def test_vanilla_js_component_with_mount(driver, driver_wait, display):
5050
vanilla_js_component = Module(
5151
"vanilla-js-component",
5252
source_file=HERE / "js" / "vanilla-js-component.js",
5353
exports_mount=True,
5454
)
5555

56+
set_text = idom.Ref(None)
57+
5658
@idom.component
5759
def MakeVanillaHtml():
58-
raw_html = "<h1 id='raw-html'>this was set via innerHTML</h1>"
59-
return vanilla_js_component.SetInnerHtml({"innerHTML": raw_html})
60+
text, set_text.current = idom.hooks.use_state("initial-text")
61+
return vanilla_js_component.ShowText({"text": text, "id": "my-el"})
6062

6163
display(MakeVanillaHtml)
6264

63-
driver.find_element_by_id("raw-html")
65+
parent_element = (
66+
# the reference to the child element changes on each render and become stale
67+
driver.find_element_by_id("my-el")
68+
# the reference to the parent element will stay the same
69+
.find_element_by_xpath("..")
70+
)
71+
72+
driver_wait.until(
73+
lambda d: (
74+
(
75+
driver.find_element_by_id("my-el").get_attribute("innerText")
76+
== "initial-text"
77+
)
78+
and parent_element.get_attribute("mountCount") == "1"
79+
and parent_element.get_attribute("unmountCount") is None
80+
)
81+
)
82+
83+
for i in range(1, 4):
84+
new_text = f"text-{i}"
85+
set_text.current(new_text)
86+
driver_wait.until(
87+
lambda d: (
88+
(
89+
driver.find_element_by_id("my-el").get_attribute("innerText")
90+
== new_text
91+
)
92+
and parent_element.get_attribute("mountCount") == str(i + 1)
93+
and parent_element.get_attribute("unmountCount") == str(i)
94+
)
95+
)

0 commit comments

Comments
 (0)