Skip to content

rework custom component interface with bind() func #458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 83 additions & 86 deletions docs/source/_static/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -1600,91 +1600,85 @@ function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
}
}

function Element({ model, key }) {
function Element({ model }) {
if (model.importSource) {
return html`<${ImportedElement} model=${model} />`;
} else {
return html`<${StandardElement} model=${model} />`;
}
}

function elementChildren(modelChildren) {
if (!modelChildren) {
return [];
} else {
return modelChildren.map((child) => {
switch (typeof child) {
case "object":
return html`<${Element} key=${child.key} model=${child} />`;
case "string":
return child;
}
});
}
}

function StandardElement({ model }) {
const config = react.useContext(LayoutConfigContext);
const children = elementChildren(model.children);
const attributes = elementAttributes(model, config.sendEvent);
if (model.children && model.children.length) {
return html`<${model.tagName} ...${attributes}>${children}<//>`;
} else {
return html`<${model.tagName} ...${attributes} />`;
}
// Use createElement here to avoid warning about variable numbers of children not
// having keys. Warning about this must now be the responsibility of the server
// providing the models instead of the client rendering them.
return react.createElement(model.tagName, attributes, ...children);
}

function ImportedElement({ model }) {
const config = react.useContext(LayoutConfigContext);
config.sendEvent;

const importSourceFallback = model.importSource.fallback;
const [importSource, setImportSource] = react.useState(null);

if (!importSource) {
// load the import source in the background
loadImportSource$1(config, model.importSource).then(setImportSource);

// display a fallback if one was given
if (!importSourceFallback) {
return html`<div />`;
} else if (typeof importSourceFallback == "string") {
return html`<div>${importSourceFallback}</div>`;
} else {
return html`<${StandardElement} model=${importSourceFallback} />`;
}
} else {
return html`<${RenderImportedElement}
model=${model}
importSource=${importSource}
/>`;
}
}

function RenderImportedElement({ model, importSource }) {
react.useContext(LayoutConfigContext);
const mountPoint = react.useRef(null);
const fallback = model.importSource.fallback;
const importSource = useConst(() =>
loadFromImportSource(config, model.importSource)
);
const sourceBinding = react.useRef(null);

react.useEffect(() => {
if (fallback) {
importSource.then(() => {
reactDom.unmountComponentAtNode(mountPoint.current);
if (mountPoint.current.children) {
mountPoint.current.removeChild(mountPoint.current.children[0]);
}
});
sourceBinding.current = importSource.bind(mountPoint.current);
if (!importSource.data.unmountBeforeUpdate) {
return sourceBinding.current.unmount;
}
}, []);

// this effect must run every time in case the model has changed
react.useEffect(() => {
importSource.then(({ createElement, renderElement }) => {
renderElement(
createElement(
model.tagName,
elementAttributes(model, config.sendEvent),
model.children
),
mountPoint.current
);
});
sourceBinding.current.render(model);
if (importSource.data.unmountBeforeUpdate) {
return sourceBinding.current.unmount;
}
});

react.useEffect(
() => () =>
importSource.then(({ unmountElement }) =>
unmountElement(mountPoint.current)
),
[]
);
return html`<div ref=${mountPoint} />`;
}

if (!fallback) {
return html`<div ref=${mountPoint} />`;
} else if (typeof fallback == "string") {
// need the second div there so we can removeChild above
return html`<div ref=${mountPoint}><div>${fallback}</div></div>`;
function elementChildren(modelChildren) {
if (!modelChildren) {
return [];
} else {
return html`<div ref=${mountPoint}>
<${StandardElement} model=${fallback} />
</div>`;
return modelChildren.map((child) => {
switch (typeof child) {
case "object":
return html`<${Element} key=${child.key} model=${child} />`;
case "string":
return child;
}
});
}
}

Expand Down Expand Up @@ -1715,34 +1709,47 @@ function eventHandler(sendEvent, eventSpec) {
return value;
}
});
new Promise((resolve, reject) => {
const msg = {
data: data,
target: eventSpec["target"],
};
sendEvent(msg);
resolve(msg);
sendEvent({
data: data,
target: eventSpec["target"],
});
};
}

function loadFromImportSource(config, importSource) {
function loadImportSource$1(config, importSource) {
return config
.loadImportSource(importSource.source, importSource.sourceType)
.then((module) => {
if (
typeof module.createElement == "function" &&
typeof module.renderElement == "function" &&
typeof module.unmountElement == "function"
) {
if (typeof module.bind == "function") {
return {
createElement: (type, props, children) =>
module.createElement(module[type], props, children, config),
renderElement: module.renderElement,
unmountElement: module.unmountElement,
data: importSource,
bind: (node) => {
const binding = module.bind(node, config);
if (
typeof binding.render == "function" &&
typeof binding.unmount == "function"
) {
return {
render: (model) => {
binding.render(
module[model.tagName],
elementAttributes(model, config.sendEvent),
model.children
);
},
unmount: binding.unmount,
};
} else {
console.error(
`${importSource.source} returned an impropper binding`
);
}
},
};
} else {
console.error(`${module} does not expose the required interfaces`);
console.error(
`${importSource.source} did not export a function 'bind'`
);
}
});
}
Expand All @@ -1767,16 +1774,6 @@ function useForceUpdate() {
return react.useCallback(() => updateState({}), []);
}

function useConst(func) {
const ref = react.useRef();

if (!ref.current) {
ref.current = func();
}

return ref.current;
}

function mountLayout(mountElement, layoutProps) {
reactDom.render(react.createElement(Layout, layoutProps), mountElement);
}
Expand Down
6 changes: 3 additions & 3 deletions docs/source/custom_js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 4 additions & 13 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,18 @@ Try interacting with the sliders 📈
.. example:: simple_dashboard


Install Javascript Modules
--------------------------
Dynamically Loaded React Components
-----------------------------------

Simply install your javascript library of choice using the ``idom`` CLI:

.. code-block:: bash
idom install victory
Then import the module with :mod:`~idom.web.module`:
This method is not recommended for use in production applications, but it's great while
you're experimenting:

.. example:: victory_chart


Define Javascript Modules
-------------------------

Assuming you already installed ``victory`` as in the :ref:`Install Javascript Modules` section:

Click the bars to trigger an event 👇

.. example:: super_simple_chart
Expand All @@ -77,8 +70,6 @@ Click the bars to trigger an event 👇
Material UI Slider
------------------

Assuming you already installed ``@material-ui/core`` as in the :ref:`Install Javascript Modules` section:

Move the slider and see the event information update 👇

.. example:: material_ui_slider
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/material_ui_button_no_action.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import idom


mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛")
mui = idom.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
Button = idom.web.export(mui, "Button")

idom.run(
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/material_ui_button_on_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import idom


mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛")
mui = idom.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
Button = idom.web.export(mui, "Button")


Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/material_ui_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import idom


mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛")
mui = idom.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
Slider = idom.web.export(mui, "Slider")


Expand Down
8 changes: 7 additions & 1 deletion docs/source/examples/simple_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from idom.widgets import Input


victory = idom.web.module_from_template("react", "victory", fallback="loading...")
victory = idom.web.module_from_template(
"react",
"victory-line",
fallback="⌛",
# not usually required (see issue #461 for more info)
unmount_before_update=True,
)
VictoryLine = idom.web.export(victory, "VictoryLine")


Expand Down
14 changes: 10 additions & 4 deletions docs/source/examples/super_simple_chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export { h as createElement, render as renderElement };

export function unmountElement(container) {
render(null, container);
export function bind(node, config) {
return {
render: (component, props, children) => {
if (children) {
console.error("Children not supported");
}
render(h(component, props), node);
},
unmount: () => render(null, node),
}
}

export function SuperSimpleChart(props) {
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/victory_chart.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import idom


victory = idom.web.module_from_template("react", "victory", fallback="⌛")
victory = idom.web.module_from_template("react", "victory-line", fallback="⌛")
VictoryBar = idom.web.export(victory, "VictoryBar")

bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
Expand Down
Loading