Skip to content

Commit 109e03a

Browse files
committed
rework custom component interface with bind() func
also... - updated the react module template to render children - removed preact module template until things are more stable (it was not being tested).
1 parent dfb434f commit 109e03a

File tree

11 files changed

+246
-2390
lines changed

11 files changed

+246
-2390
lines changed

Diff for: docs/source/_static/custom.js

+80-89
Original file line numberDiff line numberDiff line change
@@ -1600,91 +1600,80 @@ function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
16001600
}
16011601
}
16021602

1603-
function Element({ model, key }) {
1603+
function Element({ model }) {
16041604
if (model.importSource) {
16051605
return html`<${ImportedElement} model=${model} />`;
16061606
} else {
16071607
return html`<${StandardElement} model=${model} />`;
16081608
}
16091609
}
16101610

1611-
function elementChildren(modelChildren) {
1612-
if (!modelChildren) {
1613-
return [];
1614-
} else {
1615-
return modelChildren.map((child) => {
1616-
switch (typeof child) {
1617-
case "object":
1618-
return html`<${Element} key=${child.key} model=${child} />`;
1619-
case "string":
1620-
return child;
1621-
}
1622-
});
1623-
}
1624-
}
1625-
16261611
function StandardElement({ model }) {
16271612
const config = react.useContext(LayoutConfigContext);
16281613
const children = elementChildren(model.children);
16291614
const attributes = elementAttributes(model, config.sendEvent);
1630-
if (model.children && model.children.length) {
1631-
return html`<${model.tagName} ...${attributes}>${children}<//>`;
1632-
} else {
1633-
return html`<${model.tagName} ...${attributes} />`;
1634-
}
1615+
// Use createElement here to avoid warning about variable numbers of children not
1616+
// having keys. Warning about this must now be the responsibility of the server
1617+
// providing the models instead of the client rendering them.
1618+
return react.createElement(model.tagName, attributes, ...children);
16351619
}
16361620

16371621
function ImportedElement({ model }) {
16381622
const config = react.useContext(LayoutConfigContext);
1639-
config.sendEvent;
1623+
1624+
const importSourceFallback = model.importSource.fallback;
1625+
const [importSource, setImportSource] = react.useState(null);
1626+
1627+
if (!importSource) {
1628+
// load the import source in the background
1629+
loadImportSource$1(config, model.importSource).then(setImportSource);
1630+
1631+
// display a fallback if one was given
1632+
if (!importSourceFallback) {
1633+
return html`<div />`;
1634+
} else if (typeof importSourceFallback == "string") {
1635+
return html`<div>${importSourceFallback}</div>`;
1636+
} else {
1637+
return html`<${StandardElement} model=${importSourceFallback} />`;
1638+
}
1639+
} else {
1640+
return html`<${RenderImportedElement}
1641+
model=${model}
1642+
importSource=${importSource}
1643+
/>`;
1644+
}
1645+
}
1646+
1647+
function RenderImportedElement() {
1648+
react.useContext(LayoutConfigContext);
16401649
const mountPoint = react.useRef(null);
1641-
const fallback = model.importSource.fallback;
1642-
const importSource = useConst(() =>
1643-
loadFromImportSource(config, model.importSource)
1644-
);
1650+
const sourceBinding = react.useRef(null);
16451651

16461652
react.useEffect(() => {
1647-
if (fallback) {
1648-
importSource.then(() => {
1649-
reactDom.unmountComponentAtNode(mountPoint.current);
1650-
if (mountPoint.current.children) {
1651-
mountPoint.current.removeChild(mountPoint.current.children[0]);
1652-
}
1653-
});
1654-
}
1653+
sourceBinding.current = importSource.bind(mountPoint.current);
1654+
return () => {
1655+
sourceBinding.current.unmount();
1656+
};
16551657
}, []);
16561658

16571659
// this effect must run every time in case the model has changed
1658-
react.useEffect(() => {
1659-
importSource.then(({ createElement, renderElement }) => {
1660-
renderElement(
1661-
createElement(
1662-
model.tagName,
1663-
elementAttributes(model, config.sendEvent),
1664-
model.children
1665-
),
1666-
mountPoint.current
1667-
);
1668-
});
1669-
});
1660+
react.useEffect(() => sourceBinding.current.render(model));
16701661

1671-
react.useEffect(
1672-
() => () =>
1673-
importSource.then(({ unmountElement }) =>
1674-
unmountElement(mountPoint.current)
1675-
),
1676-
[]
1677-
);
16781662

1679-
if (!fallback) {
1680-
return html`<div ref=${mountPoint} />`;
1681-
} else if (typeof fallback == "string") {
1682-
// need the second div there so we can removeChild above
1683-
return html`<div ref=${mountPoint}><div>${fallback}</div></div>`;
1663+
}
1664+
1665+
function elementChildren(modelChildren) {
1666+
if (!modelChildren) {
1667+
return [];
16841668
} else {
1685-
return html`<div ref=${mountPoint}>
1686-
<${StandardElement} model=${fallback} />
1687-
</div>`;
1669+
return modelChildren.map((child) => {
1670+
switch (typeof child) {
1671+
case "object":
1672+
return html`<${Element} key=${child.key} model=${child} />`;
1673+
case "string":
1674+
return child;
1675+
}
1676+
});
16881677
}
16891678
}
16901679

@@ -1715,34 +1704,46 @@ function eventHandler(sendEvent, eventSpec) {
17151704
return value;
17161705
}
17171706
});
1718-
new Promise((resolve, reject) => {
1719-
const msg = {
1720-
data: data,
1721-
target: eventSpec["target"],
1722-
};
1723-
sendEvent(msg);
1724-
resolve(msg);
1707+
sendEvent({
1708+
data: data,
1709+
target: eventSpec["target"],
17251710
});
17261711
};
17271712
}
17281713

1729-
function loadFromImportSource(config, importSource) {
1714+
function loadImportSource$1(config, importSource) {
17301715
return config
17311716
.loadImportSource(importSource.source, importSource.sourceType)
17321717
.then((module) => {
1733-
if (
1734-
typeof module.createElement == "function" &&
1735-
typeof module.renderElement == "function" &&
1736-
typeof module.unmountElement == "function"
1737-
) {
1718+
if (typeof module.bind == "function") {
17381719
return {
1739-
createElement: (type, props, children) =>
1740-
module.createElement(module[type], props, children, config),
1741-
renderElement: module.renderElement,
1742-
unmountElement: module.unmountElement,
1720+
bind: (node) => {
1721+
const binding = module.bind(node, config);
1722+
if (
1723+
typeof binding.render == "function" &&
1724+
typeof binding.unmount == "function"
1725+
) {
1726+
return {
1727+
render: (model) => {
1728+
binding.render(
1729+
module[model.tagName],
1730+
elementAttributes(model, config.sendEvent),
1731+
model.children
1732+
);
1733+
},
1734+
unmount: binding.unmount,
1735+
};
1736+
} else {
1737+
console.error(
1738+
`${importSource.source} returned an impropper binding`
1739+
);
1740+
}
1741+
},
17431742
};
17441743
} else {
1745-
console.error(`${module} does not expose the required interfaces`);
1744+
console.error(
1745+
`${importSource.source} did not export a function 'bind'`
1746+
);
17461747
}
17471748
});
17481749
}
@@ -1767,16 +1768,6 @@ function useForceUpdate() {
17671768
return react.useCallback(() => updateState({}), []);
17681769
}
17691770

1770-
function useConst(func) {
1771-
const ref = react.useRef();
1772-
1773-
if (!ref.current) {
1774-
ref.current = func();
1775-
}
1776-
1777-
return ref.current;
1778-
}
1779-
17801771
function mountLayout(mountElement, layoutProps) {
17811772
reactDom.render(react.createElement(Layout, layoutProps), mountElement);
17821773
}

Diff for: docs/source/custom_js/package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: docs/source/examples/simple_dashboard.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from idom.widgets import Input
77

88

9-
victory = idom.web.module_from_template("react", "victory", fallback="loading...")
9+
victory = idom.web.module_from_template("react", "victory", fallback="")
1010
VictoryLine = idom.web.export(victory, "VictoryLine")
1111

1212

@@ -77,8 +77,8 @@ def update_value(value):
7777
set_value_callback(value)
7878

7979
return idom.html.fieldset(
80-
{"class": "number-input-container"},
81-
[idom.html.legend({"style": {"font-size": "medium"}}, label)],
80+
{"className": "number-input-container"},
81+
[idom.html.legend({"style": {"fontSize": "medium"}}, label)],
8282
Input(update_value, "number", value, attributes=attrs, cast=float),
8383
Input(update_value, "range", value, attributes=attrs, cast=float),
8484
)

Diff for: docs/source/javascript-components.rst

+52-29
Original file line numberDiff line numberDiff line change
@@ -56,44 +56,67 @@ For projects that will be shared with others, we recommend bundling your Javascr
5656
Rollup_ or Webpack_ into a `web module`_. IDOM also provides a `template repository`_
5757
that can be used as a blueprint to build a library of React components.
5858

59-
To work as intended, the Javascript bundle must provide named exports for the following
60-
functions as well as any components that will be rendered.
59+
To work as intended, the Javascript bundle must export a function ``bind()`` that
60+
adheres to the following interface:
61+
62+
.. code-block:: typescript
63+
64+
type EventData = {
65+
target: string;
66+
data: Array<any>;
67+
}
68+
69+
type LayoutContext = {
70+
sendEvent(data: EventData) => void;
71+
loadImportSource(source: string, sourceType: "NAME" | "URL") => Module;
72+
}
73+
74+
type bind = (node: HTMLElement, context: LayoutContext) => ({
75+
render(component: any, props: Object, children: Array<any>): void;
76+
unmount(): void;
77+
});
6178
6279
.. note::
6380

64-
The exported components do not have to be React-based since you'll have full control
65-
over the rendering mechanism.
81+
- ``node`` is the ``HTMLElement`` that ``render()`` should mount to.
6682

67-
.. code-block:: typescript
83+
- ``context`` can send events back to the server and load "import sources"
84+
(like a custom component module).
6885

69-
type createElement = (component: any, props: Object) => any;
70-
type renderElement = (element: any, container: HTMLElement) => void;
71-
type unmountElement = (element: HTMLElement) => void;
86+
- ``component`` is a named export of the current module.
7287

73-
These functions can be thought of as being analogous to those from React.
88+
- ``props`` is an object containing attributes and callbacks for the given
89+
``component``.
7490

75-
- ``createElement`` ➜ |react.createElement|_
76-
- ``renderElement`` ➜ |reactDOM.render|_
77-
- ``unmountElement`` ➜ |reactDOM.unmountComponentAtNode|_
91+
- ``children`` is an array of unrendered VDOM elements.
7892

79-
.. |react.createElement| replace:: ``react.createElement``
80-
.. _react.createElement: https://reactjs.org/docs/react-api.html#createelement
93+
The interface returned by ``bind()`` can be thought of as being similar to that of
94+
React.
8195

82-
.. |reactDOM.render| replace:: ``reactDOM.render``
83-
.. _reactDOM.render: https://reactjs.org/docs/react-dom.html#render
96+
- ``render`` ➜ |React.createElement|_ and |ReactDOM.render|_
97+
- ``unmount`` ➜ |ReactDOM.unmountComponentAtNode|_
8498

85-
.. |reactDOM.unmountComponentAtNode| replace:: ``reactDOM.unmountComponentAtNode``
86-
.. _reactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement
99+
.. |React.createElement| replace:: ``React.createElement``
100+
.. _React.createElement: https://reactjs.org/docs/react-api.html#createelement
87101

88-
And will be called in the following manner, where ``component`` is a named export of
89-
your module:
102+
.. |ReactDOM.render| replace:: ``ReactDOM.render``
103+
.. _ReactDOM.render: https://reactjs.org/docs/react-dom.html#render
90104

91-
.. code-block::
105+
.. |ReactDOM.unmountComponentAtNode| replace:: ``ReactDOM.unmountComponentAtNode``
106+
.. _ReactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement
107+
108+
It will be used in the following manner:
109+
110+
.. code-block:: javascript
111+
112+
// once on mount
113+
const binding = bind(node, context);
92114
93115
// on every render
94-
renderElement(createElement(component, props), container);
95-
// on unmount
96-
unmountElement(container);
116+
binding.render(component, props, children);
117+
118+
// once on unmount
119+
binding.unmount();
97120
98121
The simplest way to try this out yourself though, is to hook in a simple hand-crafted
99122
Javascript module that has the requisite interface. In the example to follow we'll
@@ -190,14 +213,14 @@ To start, let's take a look at the file structure we'll be building:
190213

191214
.. code-block:: javascript
192215
193-
import * as react from "react";
194-
import * as reactDOM from "react-dom";
216+
import * as React from "react";
217+
import * as ReactDOM from "react-dom";
195218
196219
// exports required to interface with IDOM
197220
export const createElement = (component, props) =>
198-
react.createElement(component, props);
199-
export const renderElement = reactDOM.render;
200-
export const unmountElement = reactDOM.unmountComponentAtNode;
221+
React.createElement(component, props);
222+
export const renderElement = ReactDOM.render;
223+
export const unmountElement = ReactDOM.unmountComponentAtNode;
201224
202225
// exports for your components
203226
export YourFirstComponent(props) {...};

Diff for: src/client/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)