Skip to content

Commit 699cc66

Browse files
committed
rework js module interface + fix docs
Because of changes to the JS interface, examples embedded in the documentation had stopped working. Those issues have been fixed now. After resolving those problems though, a new issue cropped up in which components from import sources were being unmounted on every render. To fix that, the interface had to be changed. There is no longer an exportsMount flag to indicate that the module should be rendered in isolation. Now, if there exists a render() and unmount() functions as named exports, components from that module will be rendered in isolation.
1 parent ca952f3 commit 699cc66

20 files changed

+221
-368
lines changed

docs/main.py

+51-47
Original file line numberDiff line numberDiff line change
@@ -6,72 +6,76 @@
66
from sanic import Sanic, response
77

88
import idom
9-
from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL
9+
from idom.client.manage import web_modules_dir
1010
from idom.server.sanic import PerClientStateServer
1111
from idom.widgets.utils import multiview
1212

1313

14+
HERE = Path(__file__).parent
1415
IDOM_MODEL_SERVER_URL_PREFIX = "/_idom"
1516

16-
IDOM_CLIENT_IMPORT_SOURCE_URL.set_default(
17-
# set_default because scripts/live_docs.py needs to overwrite this
18-
f"{IDOM_MODEL_SERVER_URL_PREFIX}{IDOM_CLIENT_IMPORT_SOURCE_URL.default}"
19-
)
2017

18+
def make_app():
19+
app = Sanic(__name__)
20+
app.static("/docs", str(HERE / "build"))
21+
app.static("/_modules", str(web_modules_dir()))
2122

22-
here = Path(__file__).parent
23+
@app.route("/")
24+
async def forward_to_index(request):
25+
return response.redirect("/docs/index.html")
2326

24-
app = Sanic(__name__)
25-
app.static("/docs", str(here / "build"))
27+
return app
2628

2729

28-
@app.route("/")
29-
async def forward_to_index(request):
30-
return response.redirect("/docs/index.html")
30+
def make_component():
31+
mount, component = multiview()
3132

33+
examples_dir = HERE / "source" / "examples"
34+
sys.path.insert(0, str(examples_dir))
3235

33-
mount, component = multiview()
36+
original_run = idom.run
37+
try:
38+
for file in examples_dir.iterdir():
39+
if (
40+
not file.is_file()
41+
or not file.suffix == ".py"
42+
or file.stem.startswith("_")
43+
):
44+
continue
3445

35-
examples_dir = here / "source" / "examples"
36-
sys.path.insert(0, str(examples_dir))
46+
# Modify the run function so when we exec the file
47+
# instead of running a server we mount the view.
48+
idom.run = partial(mount.add, file.stem)
3749

50+
with file.open() as f:
51+
try:
52+
exec(
53+
f.read(),
54+
{
55+
"__file__": str(file),
56+
"__name__": f"__main__.examples.{file.stem}",
57+
},
58+
)
59+
except Exception as error:
60+
raise RuntimeError(f"Failed to execute {file}") from error
61+
finally:
62+
idom.run = original_run
3863

39-
original_run = idom.run
40-
try:
41-
for file in examples_dir.iterdir():
42-
if not file.is_file() or not file.suffix == ".py" or file.stem.startswith("_"):
43-
continue
44-
45-
# Modify the run function so when we exec the file
46-
# instead of running a server we mount the view.
47-
idom.run = partial(mount.add, file.stem)
48-
49-
with file.open() as f:
50-
try:
51-
exec(
52-
f.read(),
53-
{
54-
"__file__": str(file),
55-
"__name__": f"__main__.examples.{file.stem}",
56-
},
57-
)
58-
except Exception as error:
59-
raise RuntimeError(f"Failed to execute {file}") from error
60-
finally:
61-
idom.run = original_run
62-
63-
64-
PerClientStateServer(
65-
component,
66-
{
67-
"redirect_root_to_index": False,
68-
"url_prefix": IDOM_MODEL_SERVER_URL_PREFIX,
69-
},
70-
app,
71-
)
64+
return component
7265

7366

7467
if __name__ == "__main__":
68+
app = make_app()
69+
70+
PerClientStateServer(
71+
make_component(),
72+
{
73+
"redirect_root_to_index": False,
74+
"url_prefix": IDOM_MODEL_SERVER_URL_PREFIX,
75+
},
76+
app,
77+
)
78+
7579
app.run(
7680
host="0.0.0.0",
7781
port=int(os.environ.get("PORT", 5000)),

docs/source/_exts/interactive_widget.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77

88
_IDOM_EXAMPLE_HOST = os.environ.get("IDOM_DOC_EXAMPLE_SERVER_HOST", "")
9-
_IDOM_EXAMPLE_PATH = os.environ.get("IDOM_DOC_EXAMPLE_SERVER_PATH", "/_idom")
109
_IDOM_STATIC_HOST = os.environ.get("IDOM_DOC_STATIC_SERVER_HOST", "/docs").rstrip("/")
1110

1211

@@ -28,7 +27,7 @@ def run(self):
2827
<div id="{container_id}" class="interactive widget-container center-content" style="" />
2928
<script async type="module">
3029
import loadWidgetExample from "{_IDOM_STATIC_HOST}/_static/js/load-widget-example.js";
31-
loadWidgetExample("{_IDOM_EXAMPLE_HOST}", "{_IDOM_EXAMPLE_PATH}", "{container_id}", "{view_id}");
30+
loadWidgetExample("{_IDOM_EXAMPLE_HOST}", "{container_id}", "{view_id}");
3231
</script>
3332
</div>
3433
""",

docs/source/_static/js/load-widget-example.js

+27-19
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
const IDOM_CLIENT_REACT_PATH = "/client/_snowpack/pkg/idom-client-react.js";
1+
const LOC = window.location;
2+
const HTTP_PROTO = LOC.protocol;
3+
const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:";
4+
const IDOM_MODULES_PATH = "/_modules";
5+
const IDOM_CLIENT_REACT_PATH = IDOM_MODULES_PATH + "/idom-client-react.js";
26

3-
export default function loadWidgetExample(
4-
idomServerHost,
5-
idomServerPath,
6-
mountID,
7-
viewID
8-
) {
9-
const loc = window.location;
10-
const idom_url = "//" + (idomServerHost || loc.host) + idomServerPath;
11-
const http_proto = loc.protocol;
12-
const ws_proto = http_proto === "https:" ? "wss:" : "ws:";
7+
export default function loadWidgetExample(idomServerHost, mountID, viewID) {
8+
const idom_url = "//" + (idomServerHost || LOC.host);
9+
const http_idom_url = HTTP_PROTO + idom_url;
10+
const ws_idom_url = WS_PROTO + idom_url;
1311

14-
const mount = document.getElementById(mountID);
12+
const mountEl = document.getElementById(mountID);
1513
const enableWidgetButton = document.createElement("button");
16-
enableWidgetButton.innerHTML = "Enable Widget";
14+
enableWidgetButton.appendChild(document.createTextNode("Enable Widget"));
1715
enableWidgetButton.setAttribute("class", "enable-widget-button");
1816

1917
enableWidgetButton.addEventListener("click", () => {
2018
{
21-
import(http_proto + idom_url + IDOM_CLIENT_REACT_PATH).then((module) => {
19+
import(http_idom_url + IDOM_CLIENT_REACT_PATH).then((module) => {
2220
{
2321
fadeOutAndThen(enableWidgetButton, () => {
2422
{
25-
mount.removeChild(enableWidgetButton);
26-
mount.setAttribute("class", "interactive widget-container");
23+
mountEl.removeChild(enableWidgetButton);
24+
mountEl.setAttribute("class", "interactive widget-container");
2725
module.mountLayoutWithWebSocket(
28-
mount,
29-
ws_proto + idom_url + `/stream?view_id=${viewID}`
26+
mountEl,
27+
ws_idom_url + `/_idom/stream?view_id=${viewID}`,
28+
(source, sourceType) =>
29+
loadImportSource(http_idom_url, source, sourceType)
3030
);
3131
}
3232
});
@@ -55,5 +55,13 @@ export default function loadWidgetExample(
5555
}
5656
}
5757

58-
mount.appendChild(enableWidgetButton);
58+
mountEl.appendChild(enableWidgetButton);
59+
}
60+
61+
function loadImportSource(baseUrl, source, sourceType) {
62+
if (sourceType == "NAME") {
63+
return import(baseUrl + IDOM_MODULES_PATH + "/" + source + ".js");
64+
} else {
65+
return import(source);
66+
}
5967
}

docs/source/examples/material_ui_slider.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@
88

99
@idom.component
1010
def ViewSliderEvents():
11-
event, set_event = idom.hooks.use_state(None)
11+
(event, value), set_data = idom.hooks.use_state((None, 50))
1212

1313
return idom.html.div(
1414
material_ui.Slider(
1515
{
16-
"color": "primary",
16+
"color": "primary" if value < 50 else "secondary",
1717
"step": 10,
1818
"min": 0,
1919
"max": 100,
2020
"defaultValue": 50,
2121
"valueLabelDisplay": "auto",
22-
"onChange": lambda *event: set_event(event),
22+
"onChange": lambda event, value: set_data([event, value]),
2323
}
2424
),
25-
idom.html.pre(json.dumps(event, indent=2)),
25+
idom.html.pre(json.dumps([event, value], indent=2)),
2626
)
2727

2828

docs/source/examples/super_simple_chart.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import { h, Component, render } from "https://unpkg.com/preact?module";
1+
import {
2+
h,
3+
Component,
4+
render as preactRender,
5+
} from "https://unpkg.com/preact?module";
26
import htm from "https://unpkg.com/htm?module";
37

48
const html = htm.bind(h);
59

6-
export function mount(element, component, props) {
7-
const root = render(html`<${component} ...${props} />`, element);
8-
return () => {
9-
const Nothing = () => null;
10-
render(html`<${Nothing} />`, element, root);
11-
};
10+
export function render(element, component, props) {
11+
preactRender(html`<${component} ...${props} />`, element);
12+
}
13+
14+
export function unmount(element) {
15+
preactRender(null, element);
1216
}
1317

1418
export function SuperSimpleChart(props) {

docs/source/faq.rst

+6-3
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,12 @@ Yes, but with some restrictions:
4848

4949
1. The Javascript in question must be distributed as an ECMAScript Module
5050
(`ESM <https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/>`__)
51-
2. The module must export a ``mount(element, component, props)`` function
52-
3. Set ``exports_mount=True`` when creating your :class:`~idom.client.module.Module`
53-
instance.
51+
2. The module must export the following functions:
52+
53+
- ``render(element: HTMLElement, component: any, props: Object) => void``
54+
- ``unmount(element: HTMLElement) => void``
55+
56+
5457

5558
These restrictions apply because the Javascript from the CDN must be able to run
5659
natively in the browser, the module must be able to run in isolation from the main

noxfile.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ def install_requirements_file(session: Session, name: str) -> None:
195195

196196

197197
def install_idom_dev(session: Session, extras: str = "stable") -> None:
198-
session.install("-e", f".[{extras}]")
198+
if "--no-install" not in session.posargs:
199+
session.install("-e", f".[{extras}]")
200+
else:
201+
session.posargs.remove("--no-install")
199202
if "--no-restore" not in session.posargs:
200203
session.run("idom", "restore")
201204
else:

scripts/live_docs.py

+8-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import importlib
21
import os
32

43
from sphinx_autobuild.cli import (
@@ -10,13 +9,12 @@
109
get_parser,
1110
)
1211

13-
from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL
12+
from docs.main import IDOM_MODEL_SERVER_URL_PREFIX, make_app, make_component
1413
from idom.server.sanic import PerClientStateServer
1514

1615

1716
# these environment variable are used in custom Sphinx extensions
18-
os.environ["IDOM_DOC_EXAMPLE_SERVER_HOST"] = example_server_host = "127.0.0.1:5555"
19-
os.environ["IDOM_DOC_EXAMPLE_SERVER_PATH"] = ""
17+
os.environ["IDOM_DOC_EXAMPLE_SERVER_HOST"] = "127.0.0.1:5555"
2018
os.environ["IDOM_DOC_STATIC_SERVER_HOST"] = ""
2119

2220
_running_idom_servers = []
@@ -27,19 +25,15 @@ def wrap_builder(old_builder):
2725
def new_builder():
2826
[s.stop() for s in _running_idom_servers]
2927

30-
# we need to set this before `docs.main` does
31-
IDOM_CLIENT_IMPORT_SOURCE_URL.current = (
32-
f"http://{example_server_host}{IDOM_CLIENT_IMPORT_SOURCE_URL.default}"
28+
server = PerClientStateServer(
29+
make_component(),
30+
{"cors": True, "url_prefix": IDOM_MODEL_SERVER_URL_PREFIX},
31+
make_app(),
3332
)
34-
35-
from docs import main
36-
37-
importlib.reload(main)
38-
39-
server = PerClientStateServer(main.component, {"cors": True})
40-
_running_idom_servers.append(server)
4133
server.run_in_thread("127.0.0.1", 5555, debug=True)
34+
_running_idom_servers.append(server)
4235
server.wait_until_started()
36+
4337
old_builder()
4438

4539
return new_builder

scripts/one_example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def main():
2929
return
3030

3131
idom_run = idom.run
32-
idom.run = lambda component: idom_run(component)
32+
idom.run = lambda component: idom_run(component, port=8000)
3333

3434
with example_file.open() as f:
3535
exec(

src/idom/client/_private.py

+3-12
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@
1313
BACKUP_BUILD_DIR = APP_DIR / "build"
1414

1515
# the path relative to the build that contains import sources
16-
IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX = "/_snowpack/pkg"
16+
IDOM_CLIENT_IMPORT_SOURCE_INFIX = "_snowpack/pkg"
1717

1818

19-
def _run_build_dir_init_only_once(): # pragma: no cover
20-
"""Initialize the runtime build directory
21-
22-
This should only be called *once*
23-
"""
19+
def _run_build_dir_init_only_once() -> None: # pragma: no cover
20+
"""Initialize the runtime build directory - this should only be called once"""
2421
if not IDOM_CLIENT_BUILD_DIR.current.exists():
2522
# populate the runtime build directory if it doesn't exist
2623
shutil.copytree(BACKUP_BUILD_DIR, IDOM_CLIENT_BUILD_DIR.current, symlinks=True)
@@ -38,12 +35,6 @@ def get_user_packages_file(app_dir: Path) -> Path:
3835
return app_dir / "packages" / "idom-app-react" / "src" / "user-packages.js"
3936

4037

41-
def web_modules_dir() -> Path:
42-
return IDOM_CLIENT_BUILD_DIR.current.joinpath(
43-
*IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX[1:].split("/")
44-
)
45-
46-
4738
def restore_build_dir_from_backup() -> None:
4839
target = IDOM_CLIENT_BUILD_DIR.current
4940
if target.exists():

0 commit comments

Comments
 (0)