Skip to content

Commit 51e71f2

Browse files
committed
doc updates
1 parent 92662e7 commit 51e71f2

File tree

5 files changed

+120
-79
lines changed

5 files changed

+120
-79
lines changed

docs/source/_exts/build_custom_js.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99

1010

1111
def setup(app: Sphinx) -> None:
12-
subprocess.run(["npm", "run", "build"], cwd=CUSTOM_JS_DIR, shell=True)
12+
subprocess.run("npm install", cwd=CUSTOM_JS_DIR, shell=True)
13+
subprocess.run("npm run build", cwd=CUSTOM_JS_DIR, shell=True)

docs/source/custom_js/package-lock.json

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

docs/source/javascript-components.rst

Lines changed: 62 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,53 @@ This can be accomplished in different ways for different reasons:
1717
* - Integration Method
1818
- Use Case
1919

20+
* - :ref:`Dynamically Loaded Components`
21+
- You want to **quickly experiment** with IDOM and the Javascript ecosystem.
22+
2023
* - :ref:`Custom Javascript Components`
2124
- You want to create polished software that can be **easily shared** with others.
2225

23-
* - :ref:`Dynamically Install Javascript` (requires NPM_)
24-
- You want to **quickly experiment** with IDOM and the Javascript ecosystem.
26+
27+
Dynamically Loaded Components
28+
-----------------------------
29+
30+
.. note::
31+
32+
This method is not recommended in production systems - see
33+
:ref:`Distributing Javascript Components` for more info.
34+
35+
IDOM makes it easy to draft your code when you're in the early stages of development by
36+
using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll
37+
be using the ubiquitous React-based UI framework `Material UI`_.
38+
39+
.. example:: material_ui_button_no_action
40+
41+
So now that we can display a Material UI Button we probably want to make it do
42+
something. Thankfully there's nothing new to learn here, you can pass event handlers to
43+
the button just as you did when :ref:`getting started`. Thus, all we need to do is add
44+
an ``onClick`` handler to the component:
45+
46+
.. example:: material_ui_button_on_click
2547

2648

2749
Custom Javascript Components
2850
----------------------------
2951

30-
For projects that will be shared with others we recommend bundling your Javascript with
52+
For projects that will be shared with others, we recommend bundling your Javascript with
3153
`rollup <https://rollupjs.org/guide/en/>`__ or `webpack <https://webpack.js.org/>`__
3254
into a
3355
`web module <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules>`__.
3456
IDOM also provides a
3557
`template repository <https://github.com/idom-team/idom-react-component-cookiecutter>`__
3658
that can be used as a blueprint to build a library of React components.
3759

38-
The core benefit of loading Javascript in this way is that users of your code won't need
39-
to have NPM_ installed. Rather, they can use ``pip`` to install your Python package
40-
without any other build steps because the bundled Javascript you distributed with it
41-
will be symlinked into the IDOM client at runtime.
60+
To work as intended, the Javascript bundle must provide named exports for the following
61+
functions as well as any components that will be rendered.
62+
63+
.. note::
4264

43-
To work as intended, the Javascript bundle must export the following named functions:
65+
The exported components do not have to be React-based since you'll have full control
66+
over the rendering mechanism.
4467

4568
.. code-block:: typescript
4669
@@ -63,25 +86,17 @@ These functions can be thought of as being analogous to those from React.
6386
.. |reactDOM.unmountComponentAtNode| replace:: ``reactDOM.unmountComponentAtNode``
6487
.. _reactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement
6588

66-
And will be called in the following manner:
89+
And will be called in the following manner, where ``component`` is a named export of
90+
your module:
6791

6892
.. code-block::
6993
7094
// on every render
71-
renderElement(createElement(type, props), container);
95+
renderElement(createElement(component, props), container);
7296
// on unmount
7397
unmountElement(container);
7498
75-
Once you've done this, you can distribute the bundled javascript in your Python package
76-
and integrate it into IDOM by defining :class:`~idom.client.module.Module` objects that
77-
load them from source:
78-
79-
.. code-block::
80-
81-
import idom
82-
my_js_package = idom.Module("my-js-package", source_file="/path/to/my/bundle.js")
83-
84-
The simplest way to try this out yourself though, is to hook in simple a hand-crafted
99+
The simplest way to try this out yourself though, is to hook in a simple hand-crafted
85100
Javascript module that has the requisite interface. In the example to follow we'll
86101
create a very basic SVG line chart. The catch though is that we are limited to using
87102
Javascript that can run directly in the browser. This means we can't use fancy syntax
@@ -91,58 +106,40 @@ like `JSX <https://reactjs.org/docs/introducing-jsx.html>`__ and instead will us
91106
.. example:: super_simple_chart
92107

93108

94-
.. Links
95-
.. =====
109+
Distributing Javascript Components
110+
----------------------------------
96111

97-
.. _Material UI: https://material-ui.com/
98-
.. _NPM: https://www.npmjs.com
99-
.. _install NPM: https://www.npmjs.com/get-npm
100-
101-
102-
103-
Dynamically Install Javascript
104-
------------------------------
105-
106-
.. warning::
107-
108-
- Before continuing `install NPM`_.
109-
- Not guaranteed to work in all client implementations
110-
(see :attr:`~idom.config.IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT`)
111-
112-
IDOM makes it easy to draft your code when you're in the early stages of development by
113-
using NPM_ to directly install Javascript packages on the fly. In this example we'll be
114-
using the ubiquitous React-based UI framework `Material UI`_ which can be installed
115-
using the ``idom`` CLI:
116-
117-
.. code-block:: bash
118-
119-
idom install @material-ui/core
112+
There are two ways that you can distribute your :ref:`Custom Javascript Components`:
120113

121-
Or at runtime with :func:`idom.client.module.install` (this is useful if you're working
122-
in a REPL or Jupyter Notebook):
114+
- In a Python package via PyPI_
115+
- Using a CDN_
123116

124-
.. code-block::
125-
126-
import idom
127-
material_ui = idom.install("@material-ui/core")
128-
# or install multiple modules at once
129-
material_ui, *other_modules = idom.install(["@material-ui/core", ...])
130-
131-
.. note::
117+
That can be subsequently loaded using the respective functions:
132118

133-
Any standard javascript dependency specifier is allowed here.
119+
- :func:`~idom.web.module.module_from_file`
120+
- :func:`~idom.web.module.module_from_url`
134121

135-
Once the package has been successfully installed, you can import and display the component:
122+
These options are not mutually exclusive though - if you upload your Javascript
123+
components to NPM_ and also bundle your Javascript inside a Python package, in principle
124+
your users can determine which option work best for them. Regardless though, either you
125+
or, if you give then the choice, your users, will have to consider the tradeoffs of
126+
either approach.
136127

137-
.. example:: material_ui_button_no_action
128+
- Distribution via PyPI_ - This method is ideal for local usage since the user can
129+
server all the Javascript components they depend on from their computer without
130+
requiring a network connection.
138131

132+
- Distribution via a CDN_ - Most useful in production-grade applications where its assumed
133+
the user has a network connection. In this scenario a CDN's
134+
`edge network <https://en.wikipedia.org/wiki/Edge_computing>`__ can be used to bring
135+
the Javascript source closer to the user in order to reduce page load times.
139136

140-
Passing Props To Javascript Components
141-
--------------------------------------
142137

143-
So now that we can install and display a Material UI Button we probably want to make it
144-
do something. Thankfully there's nothing new to learn here, you can pass event handlers
145-
to the button just as you did when :ref:`getting started`. Thus, all we need to do is
146-
add an ``onClick`` handler to the component:
138+
.. Links
139+
.. =====
147140
148-
.. example:: material_ui_button_on_click
141+
.. _Material UI: https://material-ui.com/
142+
.. _NPM: https://www.npmjs.com
143+
.. _install NPM: https://www.npmjs.com/get-npm
144+
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
145+
.. _PyPI: https://pypi.org/

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def docs(session: Session) -> None:
6262
# for some reason this matches absolute paths
6363
"--ignore=**/auto/*",
6464
"--ignore=**/_static/custom.js",
65-
"--ignore=**/node_modules/**/*",
65+
"--ignore=**/node_modules/*",
6666
"--ignore=**/package-lock.json",
6767
"-a",
6868
"-E",

src/idom/web/module.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""A named souce - usually a Javascript package name"""
2828

2929
URL_SOURCE = SourceType("URL")
30-
"""A source loaded from a URL, usually from a CDN"""
30+
"""A source loaded from a URL, usually a CDN"""
3131

3232

3333
def module_from_url(
@@ -36,6 +36,19 @@ def module_from_url(
3636
resolve_exports: bool = IDOM_DEBUG_MODE.current,
3737
resolve_exports_depth: int = 5,
3838
) -> WebModule:
39+
"""Load a :class:`WebModule` from a :data:`URL_SOURCE`
40+
41+
Parameters:
42+
url:
43+
Where the javascript module will be loaded from which conforms to the
44+
interface for :ref:`Custom Javascript Components`
45+
fallback:
46+
What to temporarilly display while the module is being loaded.
47+
resolve_imports:
48+
Whether to try and find all the named exports of this module.
49+
resolve_exports_depth:
50+
How deeply to search for those exports.
51+
"""
3952
return WebModule(
4053
source=url,
4154
source_type=URL_SOURCE,
@@ -51,33 +64,50 @@ def module_from_url(
5164

5265
def module_from_template(
5366
template: str,
54-
name: str,
67+
package: str,
5568
cdn: str = "https://esm.sh",
5669
fallback: Optional[Any] = None,
5770
resolve_exports: bool = IDOM_DEBUG_MODE.current,
5871
resolve_exports_depth: int = 5,
5972
) -> WebModule:
73+
"""Load a :class:`WebModule` from a :data:`URL_SOURCE` using a known framework
74+
75+
Parameters:
76+
template:
77+
The name of the template to use with the given ``package`` (``react`` | ``preact``)
78+
package:
79+
The name of a package to load. May include a file extension (defaults to
80+
``.js`` if not given)
81+
cdn:
82+
Where the package should be loaded from. The CDN must distribute ESM modules
83+
fallback:
84+
What to temporarilly display while the module is being loaded.
85+
resolve_imports:
86+
Whether to try and find all the named exports of this module.
87+
resolve_exports_depth:
88+
How deeply to search for those exports.
89+
"""
6090
cdn = cdn.rstrip("/")
6191

62-
template_file_name = f"{template}{module_name_suffix(name)}"
92+
template_file_name = f"{template}{module_name_suffix(package)}"
6393
template_file = Path(__file__).parent / "templates" / template_file_name
6494
if not template_file.exists():
6595
raise ValueError(f"No template for {template_file_name!r} exists")
6696

67-
target_file = _web_module_path(name)
97+
target_file = _web_module_path(package)
6898
if not target_file.exists():
6999
target_file.parent.mkdir(parents=True, exist_ok=True)
70100
target_file.write_text(
71-
template_file.read_text().replace("$PACKAGE", name).replace("$CDN", cdn)
101+
template_file.read_text().replace("$PACKAGE", package).replace("$CDN", cdn)
72102
)
73103

74104
return WebModule(
75-
source=name + module_name_suffix(name),
105+
source=package + module_name_suffix(package),
76106
source_type=NAME_SOURCE,
77107
default_fallback=fallback,
78108
file=target_file,
79109
export_names=(
80-
resolve_module_exports_from_url(f"{cdn}/{name}", resolve_exports_depth)
110+
resolve_module_exports_from_url(f"{cdn}/{package}", resolve_exports_depth)
81111
if resolve_exports
82112
else None
83113
),
@@ -92,6 +122,23 @@ def module_from_file(
92122
resolve_exports_depth: int = 5,
93123
symlink: bool = False,
94124
) -> WebModule:
125+
"""Load a :class:`WebModule` from a :data:`URL_SOURCE` using a known framework
126+
127+
Parameters:
128+
template:
129+
The name of the template to use with the given ``package``
130+
package:
131+
The name of a package to load. May include a file extension (defaults to
132+
``.js`` if not given)
133+
cdn:
134+
Where the package should be loaded from. The CDN must distribute ESM modules
135+
fallback:
136+
What to temporarilly display while the module is being loaded.
137+
resolve_imports:
138+
Whether to try and find all the named exports of this module.
139+
resolve_exports_depth:
140+
How deeply to search for those exports.
141+
"""
95142
source_file = Path(file)
96143
target_file = _web_module_path(name)
97144
if not source_file.exists():

0 commit comments

Comments
 (0)