Skip to content

Commit

Permalink
rework js module interface + fix docs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rmorshea committed Jun 6, 2021
1 parent ca952f3 commit 699cc66
Show file tree
Hide file tree
Showing 20 changed files with 221 additions and 368 deletions.
98 changes: 51 additions & 47 deletions docs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,76 @@
from sanic import Sanic, response

import idom
from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL
from idom.client.manage import web_modules_dir
from idom.server.sanic import PerClientStateServer
from idom.widgets.utils import multiview


HERE = Path(__file__).parent
IDOM_MODEL_SERVER_URL_PREFIX = "/_idom"

IDOM_CLIENT_IMPORT_SOURCE_URL.set_default(
# set_default because scripts/live_docs.py needs to overwrite this
f"{IDOM_MODEL_SERVER_URL_PREFIX}{IDOM_CLIENT_IMPORT_SOURCE_URL.default}"
)

def make_app():
app = Sanic(__name__)
app.static("/docs", str(HERE / "build"))
app.static("/_modules", str(web_modules_dir()))

here = Path(__file__).parent
@app.route("/")
async def forward_to_index(request):
return response.redirect("/docs/index.html")

app = Sanic(__name__)
app.static("/docs", str(here / "build"))
return app


@app.route("/")
async def forward_to_index(request):
return response.redirect("/docs/index.html")
def make_component():
mount, component = multiview()

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

mount, component = multiview()
original_run = idom.run
try:
for file in examples_dir.iterdir():
if (
not file.is_file()
or not file.suffix == ".py"
or file.stem.startswith("_")
):
continue

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

with file.open() as f:
try:
exec(
f.read(),
{
"__file__": str(file),
"__name__": f"__main__.examples.{file.stem}",
},
)
except Exception as error:
raise RuntimeError(f"Failed to execute {file}") from error
finally:
idom.run = original_run

original_run = idom.run
try:
for file in examples_dir.iterdir():
if not file.is_file() or not file.suffix == ".py" or file.stem.startswith("_"):
continue

# Modify the run function so when we exec the file
# instead of running a server we mount the view.
idom.run = partial(mount.add, file.stem)

with file.open() as f:
try:
exec(
f.read(),
{
"__file__": str(file),
"__name__": f"__main__.examples.{file.stem}",
},
)
except Exception as error:
raise RuntimeError(f"Failed to execute {file}") from error
finally:
idom.run = original_run


PerClientStateServer(
component,
{
"redirect_root_to_index": False,
"url_prefix": IDOM_MODEL_SERVER_URL_PREFIX,
},
app,
)
return component


if __name__ == "__main__":
app = make_app()

PerClientStateServer(
make_component(),
{
"redirect_root_to_index": False,
"url_prefix": IDOM_MODEL_SERVER_URL_PREFIX,
},
app,
)

app.run(
host="0.0.0.0",
port=int(os.environ.get("PORT", 5000)),
Expand Down
3 changes: 1 addition & 2 deletions docs/source/_exts/interactive_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@


_IDOM_EXAMPLE_HOST = os.environ.get("IDOM_DOC_EXAMPLE_SERVER_HOST", "")
_IDOM_EXAMPLE_PATH = os.environ.get("IDOM_DOC_EXAMPLE_SERVER_PATH", "/_idom")
_IDOM_STATIC_HOST = os.environ.get("IDOM_DOC_STATIC_SERVER_HOST", "/docs").rstrip("/")


Expand All @@ -28,7 +27,7 @@ def run(self):
<div id="{container_id}" class="interactive widget-container center-content" style="" />
<script async type="module">
import loadWidgetExample from "{_IDOM_STATIC_HOST}/_static/js/load-widget-example.js";
loadWidgetExample("{_IDOM_EXAMPLE_HOST}", "{_IDOM_EXAMPLE_PATH}", "{container_id}", "{view_id}");
loadWidgetExample("{_IDOM_EXAMPLE_HOST}", "{container_id}", "{view_id}");
</script>
</div>
""",
Expand Down
46 changes: 27 additions & 19 deletions docs/source/_static/js/load-widget-example.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
const IDOM_CLIENT_REACT_PATH = "/client/_snowpack/pkg/idom-client-react.js";
const LOC = window.location;
const HTTP_PROTO = LOC.protocol;
const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:";
const IDOM_MODULES_PATH = "/_modules";
const IDOM_CLIENT_REACT_PATH = IDOM_MODULES_PATH + "/idom-client-react.js";

export default function loadWidgetExample(
idomServerHost,
idomServerPath,
mountID,
viewID
) {
const loc = window.location;
const idom_url = "//" + (idomServerHost || loc.host) + idomServerPath;
const http_proto = loc.protocol;
const ws_proto = http_proto === "https:" ? "wss:" : "ws:";
export default function loadWidgetExample(idomServerHost, mountID, viewID) {
const idom_url = "//" + (idomServerHost || LOC.host);
const http_idom_url = HTTP_PROTO + idom_url;
const ws_idom_url = WS_PROTO + idom_url;

const mount = document.getElementById(mountID);
const mountEl = document.getElementById(mountID);
const enableWidgetButton = document.createElement("button");
enableWidgetButton.innerHTML = "Enable Widget";
enableWidgetButton.appendChild(document.createTextNode("Enable Widget"));
enableWidgetButton.setAttribute("class", "enable-widget-button");

enableWidgetButton.addEventListener("click", () => {
{
import(http_proto + idom_url + IDOM_CLIENT_REACT_PATH).then((module) => {
import(http_idom_url + IDOM_CLIENT_REACT_PATH).then((module) => {
{
fadeOutAndThen(enableWidgetButton, () => {
{
mount.removeChild(enableWidgetButton);
mount.setAttribute("class", "interactive widget-container");
mountEl.removeChild(enableWidgetButton);
mountEl.setAttribute("class", "interactive widget-container");
module.mountLayoutWithWebSocket(
mount,
ws_proto + idom_url + `/stream?view_id=${viewID}`
mountEl,
ws_idom_url + `/_idom/stream?view_id=${viewID}`,
(source, sourceType) =>
loadImportSource(http_idom_url, source, sourceType)
);
}
});
Expand Down Expand Up @@ -55,5 +55,13 @@ export default function loadWidgetExample(
}
}

mount.appendChild(enableWidgetButton);
mountEl.appendChild(enableWidgetButton);
}

function loadImportSource(baseUrl, source, sourceType) {
if (sourceType == "NAME") {
return import(baseUrl + IDOM_MODULES_PATH + "/" + source + ".js");
} else {
return import(source);
}
}
8 changes: 4 additions & 4 deletions docs/source/examples/material_ui_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@

@idom.component
def ViewSliderEvents():
event, set_event = idom.hooks.use_state(None)
(event, value), set_data = idom.hooks.use_state((None, 50))

return idom.html.div(
material_ui.Slider(
{
"color": "primary",
"color": "primary" if value < 50 else "secondary",
"step": 10,
"min": 0,
"max": 100,
"defaultValue": 50,
"valueLabelDisplay": "auto",
"onChange": lambda *event: set_event(event),
"onChange": lambda event, value: set_data([event, value]),
}
),
idom.html.pre(json.dumps(event, indent=2)),
idom.html.pre(json.dumps([event, value], indent=2)),
)


Expand Down
18 changes: 11 additions & 7 deletions docs/source/examples/super_simple_chart.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { h, Component, render } from "https://unpkg.com/preact?module";
import {
h,
Component,
render as preactRender,
} from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export function mount(element, component, props) {
const root = render(html`<${component} ...${props} />`, element);
return () => {
const Nothing = () => null;
render(html`<${Nothing} />`, element, root);
};
export function render(element, component, props) {
preactRender(html`<${component} ...${props} />`, element);
}

export function unmount(element) {
preactRender(null, element);
}

export function SuperSimpleChart(props) {
Expand Down
9 changes: 6 additions & 3 deletions docs/source/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ Yes, but with some restrictions:

1. The Javascript in question must be distributed as an ECMAScript Module
(`ESM <https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/>`__)
2. The module must export a ``mount(element, component, props)`` function
3. Set ``exports_mount=True`` when creating your :class:`~idom.client.module.Module`
instance.
2. The module must export the following functions:

- ``render(element: HTMLElement, component: any, props: Object) => void``
- ``unmount(element: HTMLElement) => void``



These restrictions apply because the Javascript from the CDN must be able to run
natively in the browser, the module must be able to run in isolation from the main
Expand Down
5 changes: 4 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ def install_requirements_file(session: Session, name: str) -> None:


def install_idom_dev(session: Session, extras: str = "stable") -> None:
session.install("-e", f".[{extras}]")
if "--no-install" not in session.posargs:
session.install("-e", f".[{extras}]")
else:
session.posargs.remove("--no-install")
if "--no-restore" not in session.posargs:
session.run("idom", "restore")
else:
Expand Down
22 changes: 8 additions & 14 deletions scripts/live_docs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib
import os

from sphinx_autobuild.cli import (
Expand All @@ -10,13 +9,12 @@
get_parser,
)

from idom.config import IDOM_CLIENT_IMPORT_SOURCE_URL
from docs.main import IDOM_MODEL_SERVER_URL_PREFIX, make_app, make_component
from idom.server.sanic import PerClientStateServer


# these environment variable are used in custom Sphinx extensions
os.environ["IDOM_DOC_EXAMPLE_SERVER_HOST"] = example_server_host = "127.0.0.1:5555"
os.environ["IDOM_DOC_EXAMPLE_SERVER_PATH"] = ""
os.environ["IDOM_DOC_EXAMPLE_SERVER_HOST"] = "127.0.0.1:5555"
os.environ["IDOM_DOC_STATIC_SERVER_HOST"] = ""

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

# we need to set this before `docs.main` does
IDOM_CLIENT_IMPORT_SOURCE_URL.current = (
f"http://{example_server_host}{IDOM_CLIENT_IMPORT_SOURCE_URL.default}"
server = PerClientStateServer(
make_component(),
{"cors": True, "url_prefix": IDOM_MODEL_SERVER_URL_PREFIX},
make_app(),
)

from docs import main

importlib.reload(main)

server = PerClientStateServer(main.component, {"cors": True})
_running_idom_servers.append(server)
server.run_in_thread("127.0.0.1", 5555, debug=True)
_running_idom_servers.append(server)
server.wait_until_started()

old_builder()

return new_builder
Expand Down
2 changes: 1 addition & 1 deletion scripts/one_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def main():
return

idom_run = idom.run
idom.run = lambda component: idom_run(component)
idom.run = lambda component: idom_run(component, port=8000)

with example_file.open() as f:
exec(
Expand Down
15 changes: 3 additions & 12 deletions src/idom/client/_private.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@
BACKUP_BUILD_DIR = APP_DIR / "build"

# the path relative to the build that contains import sources
IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX = "/_snowpack/pkg"
IDOM_CLIENT_IMPORT_SOURCE_INFIX = "_snowpack/pkg"


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


def web_modules_dir() -> Path:
return IDOM_CLIENT_BUILD_DIR.current.joinpath(
*IDOM_CLIENT_IMPORT_SOURCE_URL_INFIX[1:].split("/")
)


def restore_build_dir_from_backup() -> None:
target = IDOM_CLIENT_BUILD_DIR.current
if target.exists():
Expand Down
Loading

0 comments on commit 699cc66

Please sign in to comment.