Skip to content

Commit bb5e3f3

Browse files
committed
refactor client to use loadImportSource param
1 parent d18f27d commit bb5e3f3

File tree

14 files changed

+279
-402
lines changed

14 files changed

+279
-402
lines changed

docs/source/examples/victory_chart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
victory = idom.install("victory", fallback="loading...")
55
bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
6-
idom.run(idom.component(lambda: victory.VictoryBar({"style": bar_style})))
6+
idom.run(idom.component(lambda: victory.VictoryBar({"style": bar_style})), port=8000)

src/idom/client/_private.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX = "/_snowpack/pkg"
1616

1717

18+
def get_use_packages_file(app_dir: Path) -> Path:
19+
return app_dir / "packages" / "idom-app-react" / "src" / "user-packages.js"
20+
21+
1822
def web_modules_dir() -> Path:
1923
return IDOM_CLIENT_BUILD_DIR.current.joinpath(
2024
*IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX[1:].split("/")

src/idom/client/app/index.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
<body>
99
<div id="app"></div>
1010
<script type="module">
11-
import { mountLayoutWithWebSocket } from "./src/index.js";
12-
13-
mountLayoutWithWebSocket(document.getElementById("app"));
11+
import { mount } from "idom-app-react";
12+
mount(document.getElementById("app"));
1413
</script>
1514
</body>
1615
</html>

src/idom/client/app/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/idom/client/app/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"description": "A client application for IDOM implemented in React",
44
"version": "0.1.0",
55
"author": "Ryan Morshead",
6-
"main": "index.js",
76
"workspaces": [
87
"./packages/*"
98
],
@@ -14,7 +13,7 @@
1413
},
1514
"scripts": {
1615
"build": "snowpack build",
17-
"format": "prettier --write ./src",
16+
"format": "npm --workspaces run format",
1817
"test": "npm --workspaces test"
1918
},
2019
"devDependencies": {

src/idom/client/app/packages/idom-app-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"version": "0.1.0",
55
"author": "Ryan Morshead",
66
"license": "MIT",
7+
"main": "src/index.js",
78
"repository": {
89
"type": "git",
910
"url": "https://github.com/idom-team/idom"

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

Lines changed: 12 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { mountLayout } from "idom-client-react";
1+
import { mountLayoutWithWebSocket } from "idom-client-react";
22
import { unmountComponentAtNode } from "react-dom";
33

4-
const maxReconnectTimeout = 45;
5-
const initialReconnectTimeoutRange = 5;
6-
7-
const userPackages = import("./user-packages.js").then((module) => {
8-
for (const pkgName in module.default) {
9-
module.default[pkgName].then((pkg) => {
10-
console.log(`Loaded module '${pkgName}'`);
11-
});
12-
}
13-
});
4+
export function mount(mountPoint) {
5+
mountLayoutWithWebSocket(
6+
mountPoint,
7+
getWebSocketEndpoint(),
8+
loadImportSource,
9+
shouldReconnect() ? 45 : 0
10+
);
11+
}
1412

15-
function defaultWebSocketEndpoint() {
13+
function getWebSocketEndpoint() {
1614
const uri = document.location.hostname + ":" + document.location.port;
1715
const url = (uri + document.location.pathname).split("/").slice(0, -1);
1816
url[url.length - 1] = "stream";
@@ -28,72 +26,8 @@ function defaultWebSocketEndpoint() {
2826
return protocol + "//" + url.join("/") + "?" + queryParams.user.toString();
2927
}
3028

31-
export function mountLayoutWithWebSocket(
32-
element,
33-
endpoint = defaultWebSocketEndpoint(),
34-
importSourceURL = "./",
35-
mountState = {
36-
everMounted: false,
37-
reconnectAttempts: 0,
38-
reconnectTimeoutRange: initialReconnectTimeoutRange,
39-
}
40-
) {
41-
const socket = new WebSocket(endpoint);
42-
43-
let updateLayout;
44-
45-
socket.onopen = (event) => {
46-
console.log(`Connected.`);
47-
if (mountState.everMounted) {
48-
unmountComponentAtNode(element);
49-
}
50-
mountLayout(
51-
element,
52-
(update) => {
53-
updateLayout = update;
54-
},
55-
(event) => {
56-
socket.send(JSON.stringify(event));
57-
},
58-
importSourceURL
59-
);
60-
_setOpenMountState(mountState);
61-
};
62-
63-
socket.onmessage = (event) => {
64-
updateLayout(pathPrefix, patch);
65-
};
66-
67-
socket.onclose = (event) => {
68-
if (!shouldReconnect()) {
69-
console.log(`Connection lost.`);
70-
return;
71-
}
72-
const reconnectTimeout = _nextReconnectTimeout(mountState);
73-
console.log(`Connection lost, reconnecting in ${reconnectTimeout} seconds`);
74-
setTimeout(function () {
75-
mountState.reconnectAttempts++;
76-
mountLayoutWithWebSocket(element, endpoint, importSourceURL, mountState);
77-
}, reconnectTimeout * 1000);
78-
};
79-
}
80-
81-
function _setOpenMountState(mountState) {
82-
mountState.everMounted = true;
83-
mountState.reconnectAttempts = 0;
84-
mountState.reconnectTimeoutRange = initialReconnectTimeoutRange;
85-
}
86-
87-
function _nextReconnectTimeout(mountState) {
88-
const timeout = Math.floor(Math.random() * mountState.reconnectTimeoutRange);
89-
mountState.reconnectTimeoutRange =
90-
(mountState.reconnectTimeoutRange + 5) % maxReconnectTimeout;
91-
if (mountState.reconnectAttempts == 3) {
92-
window.alert(
93-
"Server connection was lost. Attempts to reconnect are being made in the background."
94-
);
95-
}
96-
return timeout;
29+
function loadImportSource(source) {
30+
return import("./user-packages.js").then((module) => module.default[source]);
9731
}
9832

9933
function shouldReconnect() {

src/idom/client/app/packages/idom-client-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "idom-client-react",
33
"description": "A client for IDOM implemented in React",
4-
"version": "0.7.4",
4+
"version": "0.8.0",
55
"author": "Ryan Morshead",
66
"license": "MIT",
77
"type": "module",
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import * as react from "react";
2+
import * as reactDOM from "react-dom";
3+
import htm from "htm";
4+
5+
import serializeEvent from "./event-to-object";
6+
7+
import { applyPatchInplace, joinUrl } from "./utils";
8+
9+
const html = htm.bind(react.createElement);
10+
const LayoutConfigContext = react.createContext({
11+
sendEvent: undefined,
12+
loadImportSource: undefined,
13+
});
14+
15+
export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
16+
const [model, patchModel] = useInplaceJsonPatch({});
17+
18+
react.useEffect(() => saveUpdateHook(patchModel), [patchModel]);
19+
20+
if (model.tagName) {
21+
return html`
22+
<${LayoutConfigContext.Provider} value=${{ sendEvent, loadImportSource }}>
23+
<${Element} model=${model} />
24+
<//>
25+
`;
26+
} else {
27+
return html`<div />`;
28+
}
29+
}
30+
31+
function Element({ model }) {
32+
if (model.importSource) {
33+
return html`<${ImportedElement} model=${model} />`;
34+
} else {
35+
return html`<${StandardElement} model=${model} />`;
36+
}
37+
}
38+
39+
function ImportedElement({ model }) {
40+
const config = react.useContext(LayoutConfigContext);
41+
const mountPoint = react.useRef(null);
42+
43+
react.useEffect(() => {
44+
config.loadImportSource(model.importSource.source).then((module) => {
45+
mountImportSource(mountPoint.current, module, model, config);
46+
});
47+
});
48+
49+
return html`<div ref=${mountPoint} />`;
50+
}
51+
52+
function StandardElement({ model }) {
53+
const config = react.useContext(LayoutConfigContext);
54+
const children = elementChildren(model);
55+
const attributes = elementAttributes(model, config.sendEvent);
56+
if (model.children && model.children.length) {
57+
return html`<${model.tagName} ...${attributes}>${children}<//>`;
58+
} else {
59+
return html`<${model.tagName} ...${attributes} />`;
60+
}
61+
}
62+
63+
function elementChildren(model) {
64+
if (!model.children) {
65+
return [];
66+
} else {
67+
return model.children.map((child) => {
68+
switch (typeof child) {
69+
case "object":
70+
return html`<${Element} key=${child.key} model=${child} />`;
71+
case "string":
72+
return child;
73+
}
74+
});
75+
}
76+
}
77+
78+
function elementAttributes(model, sendEvent) {
79+
const attributes = Object.assign({}, model.attributes);
80+
81+
if (model.eventHandlers) {
82+
Object.keys(model.eventHandlers).forEach((eventName) => {
83+
const eventSpec = model.eventHandlers[eventName];
84+
attributes[eventName] = eventHandler(sendEvent, eventSpec);
85+
});
86+
}
87+
88+
return attributes;
89+
}
90+
91+
function eventHandler(sendEvent, eventSpec) {
92+
return function () {
93+
const data = Array.from(arguments).map((value) => {
94+
if (typeof value === "object" && value.nativeEvent) {
95+
if (eventSpec["preventDefault"]) {
96+
value.preventDefault();
97+
}
98+
if (eventSpec["stopPropagation"]) {
99+
value.stopPropagation();
100+
}
101+
return serializeEvent(value);
102+
} else {
103+
return value;
104+
}
105+
});
106+
const sentEvent = new Promise((resolve, reject) => {
107+
const msg = {
108+
data: data,
109+
target: eventSpec["target"],
110+
};
111+
sendEvent(msg);
112+
resolve(msg);
113+
});
114+
};
115+
}
116+
117+
function mountImportSource(element, module, model, config) {
118+
if (model.importSource.exportsMount) {
119+
const props = elementAttributes(model, config.sendEvent);
120+
if (model.children) {
121+
props.children = model.children;
122+
}
123+
module.mount(element, module[model.tagName], props);
124+
} else {
125+
reactDOM.render(
126+
react.createElement(
127+
module[model.tagName],
128+
elementAttributes(model, config.sendEvent),
129+
...elementChildren(model)
130+
),
131+
element
132+
);
133+
}
134+
}
135+
136+
function useInplaceJsonPatch(doc) {
137+
const ref = react.useRef(doc);
138+
const forceUpdate = useForceUpdate();
139+
140+
const applyPatch = react.useCallback(
141+
(path, patch) => {
142+
applyPatchInplace(ref.current, path, patch);
143+
forceUpdate();
144+
},
145+
[ref, forceUpdate]
146+
);
147+
148+
return [ref.current, applyPatch];
149+
}
150+
151+
function useForceUpdate() {
152+
const [, updateState] = react.useState();
153+
return react.useCallback(() => updateState({}), []);
154+
}

0 commit comments

Comments
 (0)