diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 27a859fae..428019f87 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -11,31 +11,28 @@ on:
- cron: "0 0 * * *"
jobs:
- test-python:
+ test-python-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: nanasess/setup-chromedriver@master
- uses: actions/setup-node@v2-beta
with:
- node-version: "14.x"
+ node-version: "14"
- name: Use Latest Python
uses: actions/setup-python@v2
with:
- python-version: 3.9
- - name: Install latest NPM
- run: |
- npm install -g npm@7.22.0
- npm --version
+ python-version: "3.9"
- name: Install Python Dependencies
run: pip install -r requirements/test-run.txt
- name: Run Tests
- run: nox -s test -- --headless
- test-python-versions:
+ env: { "CI": "true" }
+ run: nox -s test_python_suite -- --headless
+ test-python-environments:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: [3.7, 3.8, 3.9]
+ python-version: ["3.7", "3.8", "3.9"]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v2
@@ -47,60 +44,36 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Install latest NPM
- run: |
- npm install -g npm@7.22.0
- npm --version
- name: Install Python Dependencies
run: pip install -r requirements/test-run.txt
- name: Run Tests
- run: nox -s test -- --headless --no-cov
- test-javascript:
+ env: { "CI": "true" }
+ run: nox -s test_python -- --headless --no-cov
+ test-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - uses: actions/setup-node@v2
+ - uses: actions/setup-node@v2-beta
with:
node-version: "14"
- - name: Install latest NPM
- run: |
- npm install -g npm@7.22.0
- npm --version
- - name: Test Javascript
- working-directory: ./src/client
- run: |
- npm install
- npm test
- npm run build
- test-documentation-image:
- runs-on: ubuntu-latest
- steps:
- - name: Check out src from Git
- uses: actions/checkout@v2
- - name: Get history and tags for SCM versioning to work
- run: |
- git fetch --prune --unshallow
- git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- - name: Build Docker Image
- run: docker build . --file docs/Dockerfile
- test-build-package:
+ - name: Use Latest Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.9"
+ - name: Install Python Dependencies
+ run: pip install -r requirements/test-run.txt
+ - name: Run Tests
+ env: { "CI": "true" }
+ run: nox -s test_docs
+ test-javascript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: "14"
- - name: Use Latest Python
- uses: actions/setup-python@v2
- with:
- python-version: 3.9
- - name: Install latest NPM
- run: |
- npm install -g npm@7.22.0
- npm --version
- name: Install Python Dependencies
- run: |
- pip install --upgrade pip
- pip install -r requirements/build-pkg.txt
- - name: Test Build Creation
- run: python setup.py bdist_wheel sdist
+ run: pip install -r requirements/test-run.txt
+ - name: Run Tests
+ env: { "CI": "true" }
+ run: nox -s test_javascript
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 34eed395f..319bab4a2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/ambv/black
- rev: 20.8b1
+ rev: 21.12b0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
@@ -12,3 +12,7 @@ repos:
hooks:
- id: isort
name: isort
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: "v2.5.1"
+ hooks:
+ - id: prettier
diff --git a/MANIFEST.in b/MANIFEST.in
index cef982bea..b18f55c7c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,2 @@
recursive-include src/idom/client *
recursive-include src/idom/web/templates *
-include requirements/prod.txt
-include requirements/extras.txt
diff --git a/docs/Dockerfile b/docs/Dockerfile
index 5c5781ab5..78b7bcdc4 100644
--- a/docs/Dockerfile
+++ b/docs/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.8
+FROM python:3.9
WORKDIR /app/
@@ -18,7 +18,6 @@ RUN pip install --upgrade pip
# Install IDOM
# ------------
ADD requirements ./requirements
-ADD .git ./.git
ADD src ./src
ADD scripts ./scripts
ADD setup.py ./
@@ -28,16 +27,23 @@ ADD README.md ./
RUN pip install -e .[all]
+# Add License
+# -----------
+Add LICENSE /app/
+
# Build the Docs
# --------------
-ADD docs/main.py ./docs/
+ADD docs/__init__.py ./docs/
+ADD docs/app.py ./docs/
+ADD docs/examples.py ./docs/
ADD docs/source ./docs/source
RUN pip install -r requirements/build-docs.txt
-RUN sphinx-build -b html docs/source docs/build
+RUN sphinx-build -W -b html docs/source docs/build
# Define Entrypoint
# -----------------
ENV PORT 5000
ENV IDOM_DEBUG_MODE=1
-CMD python docs/main.py
+ENV IDOM_CHECK_VDOM_SPEC=0
+CMD python scripts/run_docs.py
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..067adc0b8
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,18 @@
+# IDOM's Documentation
+
+We provide two main ways to run the docs. Both use `nox` which has a `noxfile.py` at the
+root of the repository. Running the docs with `nox -s docs` will start up an iteractive
+session which will rebuild the docs any time a file is modified. Using `nox -s
+docs_in_docker` on the other hand, will build a docker image and run the docs from
+there. The latter command mimics how the docs will behave in production. As such, if any
+changes to the core of the documentation are made (i.e. to non-`*.rst` files), then you
+should run a manual test of the documentation using the `docs_in_docker` session.
+
+If you with to build and run the docs by hand you need to perform two commands, each
+being run from the root of the repository:
+
+- `sphinx-build -b html docs/source docs/build`
+- `python scripts/run_docs.py`
+
+The first command constructs the static HTML and any Javascript. The latter actually
+runs the web server that serves the content.
diff --git a/docs/__init__.py b/docs/__init__.py
index e69de29bb..a6074c96f 100644
--- a/docs/__init__.py
+++ b/docs/__init__.py
@@ -0,0 +1,4 @@
+from .app import run
+
+
+__all__ = ["run"]
diff --git a/docs/app.py b/docs/app.py
new file mode 100644
index 000000000..b065dd09e
--- /dev/null
+++ b/docs/app.py
@@ -0,0 +1,60 @@
+import os
+from logging import getLogger
+from pathlib import Path
+
+from sanic import Sanic, response
+
+from idom.server.sanic import PerClientStateServer
+from idom.widgets import multiview
+
+from .examples import load_examples
+
+
+HERE = Path(__file__).parent
+IDOM_MODEL_SERVER_URL_PREFIX = "/_idom"
+
+logger = getLogger(__name__)
+
+
+IDOM_MODEL_SERVER_URL_PREFIX = "/_idom"
+
+
+def run():
+ app = make_app()
+
+ PerClientStateServer(
+ make_examples_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)),
+ workers=int(os.environ.get("WEB_CONCURRENCY", 1)),
+ debug=bool(int(os.environ.get("DEBUG", "0"))),
+ )
+
+
+def make_app():
+ app = Sanic(__name__)
+
+ app.static("/docs", str(HERE / "build"))
+
+ @app.route("/")
+ async def forward_to_index(request):
+ return response.redirect("/docs/index.html")
+
+ return app
+
+
+def make_examples_component():
+ mount, component = multiview()
+
+ for example_name, example_component in load_examples():
+ mount.add(example_name, example_component)
+
+ return component
diff --git a/docs/examples.py b/docs/examples.py
new file mode 100644
index 000000000..89b643b99
--- /dev/null
+++ b/docs/examples.py
@@ -0,0 +1,174 @@
+from __future__ import annotations
+
+from io import StringIO
+from pathlib import Path
+from traceback import format_exc
+from typing import Callable, Iterator
+
+import idom
+from idom import ComponentType
+
+
+HERE = Path(__file__)
+SOURCE_DIR = HERE.parent / "source"
+CONF_FILE = SOURCE_DIR / "conf.py"
+RUN_IDOM = idom.run
+
+
+def load_examples() -> Iterator[tuple[str, Callable[[], ComponentType]]]:
+ for name in all_example_names():
+ yield name, load_one_example(name)
+
+
+def all_example_names() -> set[str]:
+ names = set()
+ for file in _iter_example_files():
+ path = file.parent if file.name == "app.py" else file
+ names.add("/".join(path.relative_to(SOURCE_DIR).with_suffix("").parts))
+ return names
+
+
+def load_one_example(file_or_name: Path | str) -> Callable[[], ComponentType]:
+ return lambda: (
+ # we use a lambda to ensure each instance is fresh
+ _load_one_example(file_or_name)
+ )
+
+
+def get_normalized_example_name(
+ name: str, relative_to: str | Path | None = SOURCE_DIR
+) -> str:
+ return "/".join(
+ _get_root_example_path_by_name(name, relative_to).relative_to(SOURCE_DIR).parts
+ )
+
+
+def get_main_example_file_by_name(
+ name: str, relative_to: str | Path | None = SOURCE_DIR
+) -> Path:
+ path = _get_root_example_path_by_name(name, relative_to)
+ if path.is_dir():
+ return path / "app.py"
+ else:
+ return path.with_suffix(".py")
+
+
+def get_example_files_by_name(
+ name: str, relative_to: str | Path | None = SOURCE_DIR
+) -> list[Path]:
+ path = _get_root_example_path_by_name(name, relative_to)
+ if path.is_dir():
+ return [p for p in path.glob("*") if not p.is_dir()]
+ else:
+ path = path.with_suffix(".py")
+ return [path] if path.exists() else []
+
+
+def _iter_example_files() -> Iterator[Path]:
+ for path in SOURCE_DIR.iterdir():
+ if path.is_dir():
+ if not path.name.startswith("_") or path.name == "_examples":
+ yield from path.rglob("*.py")
+ elif path != CONF_FILE and path.suffix == ".py":
+ yield path
+
+
+def _load_one_example(file_or_name: Path | str) -> ComponentType:
+ if isinstance(file_or_name, str):
+ file = get_main_example_file_by_name(file_or_name)
+ else:
+ file = file_or_name
+
+ if not file.exists():
+ raise FileNotFoundError(str(file))
+
+ print_buffer = _PrintBuffer()
+
+ def capture_print(*args, **kwargs):
+ buffer = StringIO()
+ print(*args, file=buffer, **kwargs)
+ print_buffer.write(buffer.getvalue())
+
+ captured_component_constructor = None
+
+ def capture_component(component_constructor):
+ nonlocal captured_component_constructor
+ captured_component_constructor = component_constructor
+
+ idom.run = capture_component
+ try:
+ code = compile(file.read_text(), str(file), "exec")
+ exec(
+ code,
+ {
+ "print": capture_print,
+ "__file__": str(file),
+ "__name__": file.stem,
+ },
+ )
+ except Exception:
+ return _make_error_display(format_exc())
+ finally:
+ idom.run = RUN_IDOM
+
+ if captured_component_constructor is None:
+ return _make_example_did_not_run(str(file))
+
+ @idom.component
+ def Wrapper():
+ return idom.html.div(captured_component_constructor(), PrintView())
+
+ @idom.component
+ def PrintView():
+ text, set_text = idom.hooks.use_state(print_buffer.getvalue())
+ print_buffer.set_callback(set_text)
+ return idom.html.pre({"class": "printout"}, text) if text else idom.html.div()
+
+ return Wrapper()
+
+
+def _get_root_example_path_by_name(name: str, relative_to: str | Path | None) -> Path:
+ if not name.startswith("/") and relative_to is not None:
+ rel_path = Path(relative_to)
+ rel_path = rel_path.parent if rel_path.is_file() else rel_path
+ else:
+ rel_path = SOURCE_DIR
+ return rel_path.joinpath(*name.split("/"))
+
+
+class _PrintBuffer:
+ def __init__(self, max_lines: int = 10):
+ self._callback = None
+ self._lines = ()
+ self._max_lines = max_lines
+
+ def set_callback(self, function: Callable[[str], None]) -> None:
+ self._callback = function
+ return None
+
+ def getvalue(self) -> str:
+ return "".join(self._lines)
+
+ def write(self, text: str) -> None:
+ if len(self._lines) == self._max_lines:
+ self._lines = self._lines[1:] + (text,)
+ else:
+ self._lines += (text,)
+ if self._callback is not None:
+ self._callback(self.getvalue())
+
+
+def _make_example_did_not_run(example_name):
+ @idom.component
+ def ExampleDidNotRun():
+ return idom.html.code(f"Example {example_name} did not run")
+
+ return ExampleDidNotRun()
+
+
+def _make_error_display(message):
+ @idom.component
+ def ShowError():
+ return idom.html.pre(message)
+
+ return ShowError()
diff --git a/docs/main.py b/docs/main.py
deleted file mode 100644
index 7a680b39c..000000000
--- a/docs/main.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import os
-import sys
-from functools import partial
-from pathlib import Path
-
-from sanic import Sanic, response
-
-import idom
-from idom.config import IDOM_WED_MODULES_DIR
-from idom.server.sanic import PerClientStateServer
-from idom.widgets import multiview
-
-
-HERE = Path(__file__).parent
-IDOM_MODEL_SERVER_URL_PREFIX = "/_idom"
-
-
-def make_app():
- app = Sanic(__name__)
- app.static("/docs", str(HERE / "build"))
- app.static("/_modules", str(IDOM_WED_MODULES_DIR.current))
-
- @app.route("/")
- async def forward_to_index(request):
- return response.redirect("/docs/index.html")
-
- return app
-
-
-def make_component():
- mount, component = multiview()
-
- examples_dir = HERE / "source" / "examples"
- sys.path.insert(0, str(examples_dir))
-
- 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)
- try:
- exec(
- file.read_text(),
- {
- "__file__": str(file.absolute()),
- "__name__": f"__main__.examples.{file.stem}",
- },
- )
- except Exception as error:
- raise RuntimeError(f"Failed to execute {file}") from error
- finally:
- idom.run = original_run
-
- 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)),
- workers=int(os.environ.get("WEB_CONCURRENCY", 1)),
- debug=bool(int(os.environ.get("DEBUG", "0"))),
- )
diff --git a/docs/source/auto/developer-apis.rst b/docs/source/_autogen/dev-apis.rst
similarity index 59%
rename from docs/source/auto/developer-apis.rst
rename to docs/source/_autogen/dev-apis.rst
index dfe0e27a9..32b6d2ab4 100644
--- a/docs/source/auto/developer-apis.rst
+++ b/docs/source/_autogen/dev-apis.rst
@@ -1,5 +1,5 @@
-Developer APIs
-==============
+Dev API
+=======
.. automodule:: idom._option
:members:
diff --git a/docs/source/auto/api-reference.rst b/docs/source/_autogen/user-apis.rst
similarity index 94%
rename from docs/source/auto/api-reference.rst
rename to docs/source/_autogen/user-apis.rst
index d47db5797..5618cbbc9 100644
--- a/docs/source/auto/api-reference.rst
+++ b/docs/source/_autogen/user-apis.rst
@@ -1,5 +1,5 @@
-API Reference
-=============
+User API
+========
.. automodule:: idom.config
:members:
@@ -61,6 +61,9 @@ API Reference
Misc Modules
------------
+.. automodule:: idom.sample
+ :members:
+
.. automodule:: idom.server.proto
:members:
diff --git a/docs/source/custom_js/README.md b/docs/source/_custom_js/README.md
similarity index 100%
rename from docs/source/custom_js/README.md
rename to docs/source/_custom_js/README.md
diff --git a/docs/source/custom_js/package-lock.json b/docs/source/_custom_js/package-lock.json
similarity index 57%
rename from docs/source/custom_js/package-lock.json
rename to docs/source/_custom_js/package-lock.json
index 957e188cc..a91823b02 100644
--- a/docs/source/custom_js/package-lock.json
+++ b/docs/source/_custom_js/package-lock.json
@@ -20,6 +20,7 @@
},
"../../../src/client/packages/idom-client-react": {
"version": "0.33.3",
+ "integrity": "sha512-CTqnSzhAVz20IHDexECtCdLuvE2diUIi63kog45sNiJdu5ig+cUKjT4zuA1YHGchf4jJVgsQKPsd1BB3Bx6cew==",
"license": "MIT",
"dependencies": {
"fast-json-patch": "^3.0.0-1",
@@ -27,7 +28,7 @@
},
"devDependencies": {
"jsdom": "16.3.0",
- "prettier": "^2.2.1",
+ "prettier": "^2.5.1",
"uvu": "^0.5.1"
},
"peerDependencies": {
@@ -37,26 +38,26 @@
},
"node_modules/@types/estree": {
"version": "0.0.48",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
+ "dev": true
},
"node_modules/@types/node": {
"version": "15.12.2",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
+ "dev": true
},
"node_modules/@types/resolve": {
"version": "0.0.8",
+ "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/builtin-modules": {
"version": "3.2.0",
+ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
},
@@ -66,18 +67,18 @@
},
"node_modules/estree-walker": {
"version": "0.6.1",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true
},
"node_modules/function-bind": {
"version": "1.1.1",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
},
"node_modules/has": {
"version": "1.0.3",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -91,8 +92,8 @@
},
"node_modules/is-core-module": {
"version": "2.4.0",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has": "^1.0.3"
},
@@ -102,34 +103,34 @@
},
"node_modules/is-module": {
"version": "1.0.0",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
+ "dev": true
},
"node_modules/is-reference": {
"version": "1.2.1",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/magic-string": {
"version": "0.25.7",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"sourcemap-codec": "^1.4.4"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
},
"node_modules/prettier": {
"version": "2.3.1",
+ "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
"dev": true,
- "license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
@@ -139,8 +140,8 @@
},
"node_modules/resolve": {
"version": "1.20.0",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
@@ -151,8 +152,8 @@
},
"node_modules/rollup": {
"version": "2.52.1",
+ "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==",
"dev": true,
- "license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -165,8 +166,9 @@
},
"node_modules/rollup-plugin-commonjs": {
"version": "10.1.0",
+ "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.",
"dev": true,
- "license": "MIT",
"dependencies": {
"estree-walker": "^0.6.1",
"is-reference": "^1.1.2",
@@ -180,8 +182,9 @@
},
"node_modules/rollup-plugin-node-resolve": {
"version": "5.2.0",
+ "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
@@ -195,8 +198,9 @@
},
"node_modules/rollup-plugin-replace": {
"version": "2.2.0",
+ "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
+ "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.",
"dev": true,
- "license": "MIT",
"dependencies": {
"magic-string": "^0.25.2",
"rollup-pluginutils": "^2.6.0"
@@ -204,29 +208,32 @@
},
"node_modules/rollup-pluginutils": {
"version": "2.8.2",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"estree-walker": "^0.6.1"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "dev": true
}
},
"dependencies": {
"@types/estree": {
"version": "0.0.48",
+ "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==",
"dev": true
},
"@types/node": {
"version": "15.12.2",
+ "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
+ "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -234,18 +241,22 @@
},
"builtin-modules": {
"version": "3.2.0",
+ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true
},
"estree-walker": {
"version": "0.6.1",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
},
"function-bind": {
"version": "1.1.1",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"has": {
"version": "1.0.3",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
@@ -257,12 +268,13 @@
"fast-json-patch": "^3.0.0-1",
"htm": "^3.0.3",
"jsdom": "16.3.0",
- "prettier": "^2.2.1",
+ "prettier": "^2.5.1",
"uvu": "^0.5.1"
}
},
"is-core-module": {
"version": "2.4.0",
+ "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
"dev": true,
"requires": {
"has": "^1.0.3"
@@ -270,10 +282,12 @@
},
"is-module": {
"version": "1.0.0",
+ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.2.1",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"requires": {
"@types/estree": "*"
@@ -281,6 +295,7 @@
},
"magic-string": {
"version": "0.25.7",
+ "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
@@ -288,14 +303,17 @@
},
"path-parse": {
"version": "1.0.7",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"prettier": {
"version": "2.3.1",
+ "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==",
"dev": true
},
"resolve": {
"version": "1.20.0",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": {
"is-core-module": "^2.2.0",
@@ -304,6 +322,7 @@
},
"rollup": {
"version": "2.52.1",
+ "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
@@ -311,6 +330,7 @@
},
"rollup-plugin-commonjs": {
"version": "10.1.0",
+ "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1",
@@ -322,6 +342,7 @@
},
"rollup-plugin-node-resolve": {
"version": "5.2.0",
+ "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
"dev": true,
"requires": {
"@types/resolve": "0.0.8",
@@ -333,6 +354,7 @@
},
"rollup-plugin-replace": {
"version": "2.2.0",
+ "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==",
"dev": true,
"requires": {
"magic-string": "^0.25.2",
@@ -341,6 +363,7 @@
},
"rollup-pluginutils": {
"version": "2.8.2",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
@@ -348,6 +371,7 @@
},
"sourcemap-codec": {
"version": "1.4.8",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
}
}
diff --git a/docs/source/custom_js/package.json b/docs/source/_custom_js/package.json
similarity index 100%
rename from docs/source/custom_js/package.json
rename to docs/source/_custom_js/package.json
diff --git a/docs/source/custom_js/rollup.config.js b/docs/source/_custom_js/rollup.config.js
similarity index 100%
rename from docs/source/custom_js/rollup.config.js
rename to docs/source/_custom_js/rollup.config.js
diff --git a/docs/source/custom_js/src/index.js b/docs/source/_custom_js/src/index.js
similarity index 52%
rename from docs/source/custom_js/src/index.js
rename to docs/source/_custom_js/src/index.js
index 048354735..9e9386ffa 100644
--- a/docs/source/custom_js/src/index.js
+++ b/docs/source/_custom_js/src/index.js
@@ -1,9 +1,6 @@
-import { mountLayoutWithWebSocket } from "idom-client-react";
+import { mountWithLayoutServer, LayoutServerInfo } from "idom-client-react";
-const LOC = window.location;
-const HTTP_PROTO = LOC.protocol;
-const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:";
-const IDOM_MODULES_PATH = "/_modules";
+let didMountDebug = false;
export function mountWidgetExample(
mountID,
@@ -11,24 +8,31 @@ export function mountWidgetExample(
idomServerHost,
useActivateButton
) {
- const idomUrl = "//" + (idomServerHost || LOC.host);
- const httpIdomUrl = HTTP_PROTO + idomUrl ;
- const wsIdomUrl = WS_PROTO + idomUrl ;
+ let idomHost, idomPort;
+ if (idomServerHost) {
+ [idomHost, idomPort] = idomServerHost.split(":", 2);
+ } else {
+ idomHost = window.location.hostname;
+ idomPort = window.location.port;
+ }
+
+ const serverInfo = new LayoutServerInfo({
+ host: idomHost,
+ port: idomPort,
+ path: "/_idom/",
+ query: `view_id=${viewID}`,
+ secure: window.location.protocol == "https",
+ });
const mountEl = document.getElementById(mountID);
if (!useActivateButton) {
- mountLayoutWithWebSocket(
- mountEl,
- wsIdomUrl + `/_idom/stream?view_id=${viewID}`,
- (source, sourceType) =>
- loadImportSource(httpIdomUrl, source, sourceType)
- );
+ mountWithLayoutServer(mountEl, serverInfo);
return;
}
const enableWidgetButton = document.createElement("button");
- enableWidgetButton.appendChild(document.createTextNode("Enable Widget"));
+ enableWidgetButton.appendChild(document.createTextNode("Activate"));
enableWidgetButton.setAttribute("class", "enable-widget-button");
enableWidgetButton.addEventListener("click", () =>
@@ -36,12 +40,7 @@ export function mountWidgetExample(
{
mountEl.removeChild(enableWidgetButton);
mountEl.setAttribute("class", "interactive widget-container");
- mountLayoutWithWebSocket(
- mountEl,
- wsIdomUrl + `/_idom/stream?view_id=${viewID}`,
- (source, sourceType) =>
- loadImportSource(httpIdomUrl, source, sourceType)
- );
+ mountWithLayoutServer(mountEl, serverInfo);
}
})
);
@@ -68,11 +67,3 @@ export function mountWidgetExample(
mountEl.appendChild(enableWidgetButton);
}
-
-function loadImportSource(baseUrl, source, sourceType) {
- if (sourceType == "NAME") {
- return import(baseUrl + IDOM_MODULES_PATH + "/" + source);
- } else {
- return import(source);
- }
-}
diff --git a/docs/source/_exts/autogen_api_docs.py b/docs/source/_exts/autogen_api_docs.py
index 92651d3a9..d5f13be4d 100644
--- a/docs/source/_exts/autogen_api_docs.py
+++ b/docs/source/_exts/autogen_api_docs.py
@@ -9,17 +9,32 @@
HERE = Path(__file__).parent
PACKAGE_SRC = HERE.parent.parent.parent / "src"
-AUTO_DIR = HERE.parent / "auto"
-AUTO_DIR.mkdir(exist_ok=True)
+AUTOGEN_DIR = HERE.parent / "_autogen"
+AUTOGEN_DIR.mkdir(exist_ok=True)
-PUBLIC_API_REFERENCE_FILE = AUTO_DIR / "api-reference.rst"
-PRIVATE_API_REFERENCE_FILE = AUTO_DIR / "developer-apis.rst"
+PUBLIC_API_REFERENCE_FILE = AUTOGEN_DIR / "user-apis.rst"
+PRIVATE_API_REFERENCE_FILE = AUTOGEN_DIR / "dev-apis.rst"
-PUBLIC_TITLE = "API Reference\n=============\n"
-PUBLIC_MISC_TITLE = "Misc Modules\n------------\n"
-PRIVATE_TITLE = "Developer APIs\n==============\n"
-PRIVATE_MISC_TITLE = "Misc Dev Modules\n----------------\n"
+PUBLIC_TITLE = """\
+User API
+========
+"""
+
+PUBLIC_MISC_TITLE = """\
+Misc Modules
+------------
+"""
+
+PRIVATE_TITLE = """\
+Dev API
+=======
+"""
+
+PRIVATE_MISC_TITLE = """\
+Misc Dev Modules
+----------------
+"""
AUTODOC_TEMPLATE = ".. automodule:: {module}\n :members:\n"
diff --git a/docs/source/_exts/build_custom_js.py b/docs/source/_exts/build_custom_js.py
index c37e9847f..b84378353 100644
--- a/docs/source/_exts/build_custom_js.py
+++ b/docs/source/_exts/build_custom_js.py
@@ -5,7 +5,7 @@
SOURCE_DIR = Path(__file__).parent.parent
-CUSTOM_JS_DIR = SOURCE_DIR / "custom_js"
+CUSTOM_JS_DIR = SOURCE_DIR / "_custom_js"
def setup(app: Sphinx) -> None:
diff --git a/docs/source/_exts/idom_example.py b/docs/source/_exts/idom_example.py
new file mode 100644
index 000000000..07c0a74a2
--- /dev/null
+++ b/docs/source/_exts/idom_example.py
@@ -0,0 +1,181 @@
+from __future__ import annotations
+
+import re
+from pathlib import Path
+from typing import Any
+
+from docutils.parsers.rst import directives
+from docutils.statemachine import StringList
+from sphinx.application import Sphinx
+from sphinx.util.docutils import SphinxDirective
+from sphinx_design.tabs import TabSetDirective
+
+from docs.examples import (
+ SOURCE_DIR,
+ get_example_files_by_name,
+ get_normalized_example_name,
+)
+
+
+class WidgetExample(SphinxDirective):
+
+ has_content = False
+ required_arguments = 1
+ _next_id = 0
+
+ option_spec = {
+ "result-is-default-tab": directives.flag,
+ "activate-button": directives.flag,
+ }
+
+ def run(self):
+ example_name = get_normalized_example_name(
+ self.arguments[0],
+ # only used if example name starts with "/"
+ self.get_source_info()[0],
+ )
+
+ show_linenos = "linenos" in self.options
+ live_example_is_default_tab = "result-is-default-tab" in self.options
+ activate_result = "activate-button" not in self.options
+
+ ex_files = get_example_files_by_name(example_name)
+ if not ex_files:
+ src_file, line_num = self.get_source_info()
+ raise ValueError(
+ f"Missing example named {example_name!r} "
+ f"referenced by document {src_file}:{line_num}"
+ )
+
+ labeled_tab_items: list[tuple[str, Any]] = []
+ if len(ex_files) == 1:
+ labeled_tab_items.append(
+ (
+ "app.py",
+ _literal_include(
+ path=ex_files[0],
+ linenos=show_linenos,
+ ),
+ )
+ )
+ else:
+ for path in sorted(ex_files, key=lambda p: p.name):
+ labeled_tab_items.append(
+ (
+ path.name,
+ _literal_include(
+ path=path,
+ linenos=show_linenos,
+ ),
+ )
+ )
+
+ result_tab_item = (
+ "▶️ result",
+ _interactive_widget(
+ name=example_name,
+ with_activate_button=not activate_result,
+ ),
+ )
+ if live_example_is_default_tab:
+ labeled_tab_items.insert(0, result_tab_item)
+ else:
+ labeled_tab_items.append(result_tab_item)
+
+ return TabSetDirective(
+ "WidgetExample",
+ [],
+ {},
+ _make_tab_items(labeled_tab_items),
+ self.lineno - 1,
+ self.content_offset,
+ "",
+ self.state,
+ self.state_machine,
+ ).run()
+
+
+def _make_tab_items(labeled_content_tuples):
+ tab_items = ""
+ for label, content in labeled_content_tuples:
+ tab_items += _tab_item_template.format(
+ label=label,
+ content=content.replace("\n", "\n "),
+ )
+ return _string_to_nested_lines(tab_items)
+
+
+def _literal_include(path: Path, linenos: bool):
+ try:
+ language = {
+ ".py": "python",
+ ".js": "javascript",
+ ".json": "json",
+ }[path.suffix]
+ except KeyError:
+ raise ValueError(f"Unknown extension type {path.suffix!r}")
+
+ return _literal_include_template.format(
+ name=str(path.relative_to(SOURCE_DIR)),
+ language=language,
+ options=_join_options(_get_file_options(path)),
+ )
+
+
+def _join_options(option_strings: list[str]) -> str:
+ return "\n ".join(option_strings)
+
+
+OPTION_PATTERN = re.compile(r"#\s:[\w-]+:.*")
+
+
+def _get_file_options(file: Path) -> list[str]:
+ options = []
+
+ for line in file.read_text().split("\n"):
+ if not line.strip():
+ continue
+ if not line.startswith("#"):
+ break
+ if not OPTION_PATTERN.match(line):
+ continue
+ option_string = line[1:].strip()
+ if option_string:
+ options.append(option_string)
+
+ return options
+
+
+def _interactive_widget(name, with_activate_button):
+ return _interactive_widget_template.format(
+ name=name,
+ activate_button_opt=":activate-button:" if with_activate_button else "",
+ )
+
+
+_tab_item_template = """
+.. tab-item:: {label}
+
+ {content}
+"""
+
+
+_interactive_widget_template = """
+.. idom-view:: {name}
+ {activate_button_opt}
+"""
+
+
+_literal_include_template = """
+.. literalinclude:: /{name}
+ :language: {language}
+ {options}
+"""
+
+
+def _string_to_nested_lines(content):
+ return StringList(content.split("\n"))
+
+
+def setup(app: Sphinx) -> None:
+ app.add_directive("idom", WidgetExample)
diff --git a/docs/source/_exts/interactive_widget.py b/docs/source/_exts/idom_view.py
similarity index 56%
rename from docs/source/_exts/interactive_widget.py
rename to docs/source/_exts/idom_view.py
index 381c5eac8..995640301 100644
--- a/docs/source/_exts/interactive_widget.py
+++ b/docs/source/_exts/idom_view.py
@@ -1,41 +1,53 @@
import os
from docutils.nodes import raw
-from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst import directives
from sphinx.application import Sphinx
+from sphinx.util.docutils import SphinxDirective
+
+from docs.examples import get_normalized_example_name
_IDOM_EXAMPLE_HOST = os.environ.get("IDOM_DOC_EXAMPLE_SERVER_HOST", "")
_IDOM_STATIC_HOST = os.environ.get("IDOM_DOC_STATIC_SERVER_HOST", "/docs").rstrip("/")
-class IteractiveWidget(Directive):
+class IteractiveWidget(SphinxDirective):
has_content = False
required_arguments = 1
_next_id = 0
option_spec = {
- "no-activate-button": directives.flag,
+ "activate-button": directives.flag,
+ "margin": float,
}
def run(self):
IteractiveWidget._next_id += 1
container_id = f"idom-widget-{IteractiveWidget._next_id}"
- view_id = self.arguments[0]
+ view_id = get_normalized_example_name(
+ self.arguments[0],
+ # only used if example name starts with "/"
+ self.get_source_info()[0],
+ )
return [
raw(
"",
f"""
@@ -46,4 +58,4 @@ def run(self):
def setup(app: Sphinx) -> None:
- app.add_directive("interactive-widget", IteractiveWidget)
+ app.add_directive("idom-view", IteractiveWidget)
diff --git a/docs/source/_exts/widget_example.py b/docs/source/_exts/widget_example.py
deleted file mode 100644
index 350707b0a..000000000
--- a/docs/source/_exts/widget_example.py
+++ /dev/null
@@ -1,138 +0,0 @@
-from pathlib import Path
-
-from docutils.parsers.rst import directives
-from docutils.statemachine import StringList
-from sphinx.application import Sphinx
-from sphinx.util.docutils import SphinxDirective
-from sphinx_design.tabs import TabSetDirective
-
-
-here = Path(__file__).parent
-examples = here.parent / "examples"
-
-
-class WidgetExample(SphinxDirective):
-
- has_content = False
- required_arguments = 1
- _next_id = 0
-
- option_spec = {
- "linenos": directives.flag,
- "live-example-is-default-tab": directives.flag,
- }
-
- def run(self):
- example_name = self.arguments[0]
- show_linenos = "linenos" in self.options
-
- py_ex_path = examples / f"{example_name}.py"
- if not py_ex_path.exists():
- src_file, line_num = self.get_source_info()
- raise ValueError(
- f"Missing example file named {py_ex_path} referenced by document {src_file}:{line_num}"
- )
-
- labeled_tab_items = {
- "Python Code": _literal_include_py(
- name=example_name,
- linenos=show_linenos,
- ),
- "Live Example": _interactive_widget(
- name=example_name,
- with_activate_button="live-example-is-default-tab" not in self.options,
- ),
- }
-
- if (examples / f"{example_name}.js").exists():
- labeled_tab_items["Javascript Code"] = _literal_include_js(
- name=example_name,
- linenos=show_linenos,
- )
-
- tab_label_order = (
- ["Live Example", "Python Code", "Javascript Code"]
- if "live-example-is-default-tab" in self.options
- else ["Python Code", "Javascript Code", "Live Example"]
- )
-
- return TabSetDirective(
- "WidgetExample",
- [],
- {},
- _make_tab_items(
- [
- (label, labeled_tab_items[label])
- for label in tab_label_order
- if label in labeled_tab_items
- ]
- ),
- self.lineno - 1,
- self.content_offset,
- "",
- self.state,
- self.state_machine,
- ).run()
-
-
-def _make_tab_items(labeled_content_tuples):
- tab_items = ""
- for label, content in labeled_content_tuples:
- tab_items += _tab_item_template.format(
- label=label,
- content=content.replace("\n", "\n "),
- )
- return _string_to_nested_lines(tab_items)
-
-
-def _literal_include_py(name, linenos):
- return _literal_include_template.format(
- name=name,
- ext="py",
- language="python",
- linenos=":linenos:" if linenos else "",
- )
-
-
-def _literal_include_js(name, linenos):
- return _literal_include_template.format(
- name=name,
- ext="js",
- language="javascript",
- linenos=":linenos:" if linenos else "",
- )
-
-
-def _interactive_widget(name, with_activate_button):
- return _interactive_widget_template.format(
- name=name,
- activate_button_opt="" if with_activate_button else ":no-activate-button:",
- )
-
-
-_tab_item_template = """
-.. tab-item:: {label}
-
- {content}
-"""
-
-
-_interactive_widget_template = """
-.. interactive-widget:: {name}
- {activate_button_opt}
-"""
-
-
-_literal_include_template = """
-.. literalinclude:: /examples/{name}.{ext}
- :language: {language}
- {linenos}
-"""
-
-
-def _string_to_nested_lines(content):
- return StringList(content.split("\n"))
-
-
-def setup(app: Sphinx) -> None:
- app.add_directive("example", WidgetExample)
diff --git a/docs/source/_static/idom-logo-square-small.svg b/docs/source/_static/branding/idom-logo-square-small.svg
similarity index 100%
rename from docs/source/_static/idom-logo-square-small.svg
rename to docs/source/_static/branding/idom-logo-square-small.svg
diff --git a/docs/source/_static/idom-logo.svg b/docs/source/_static/branding/idom-logo.svg
similarity index 100%
rename from docs/source/_static/idom-logo.svg
rename to docs/source/_static/branding/idom-logo.svg
diff --git a/docs/source/_static/css/furo-theme-overrides.css b/docs/source/_static/css/furo-theme-overrides.css
new file mode 100644
index 000000000..f23c23168
--- /dev/null
+++ b/docs/source/_static/css/furo-theme-overrides.css
@@ -0,0 +1,4 @@
+body {
+ --admonition-title-font-size: 1rem !important;
+ --admonition-font-size: 1rem !important;
+}
diff --git a/docs/source/_static/css/interactive-widget.css b/docs/source/_static/css/interactive-widget.css
index 41cd58e7c..9abe24198 100644
--- a/docs/source/_static/css/interactive-widget.css
+++ b/docs/source/_static/css/interactive-widget.css
@@ -6,10 +6,17 @@
}
.widget-container {
padding: 15px;
+ overflow: auto;
background-color: var(--color-code-background);
min-height: 75px;
}
+.widget-container .printout {
+ margin-top: 20px;
+ border-top: solid 2px var(--color-foreground-border);
+ padding-top: 20px;
+}
+
.widget-container > div {
width: 100%;
}
diff --git a/docs/source/_static/css/larger-headings.css b/docs/source/_static/css/larger-headings.css
index cb466b3c7..297ab7202 100644
--- a/docs/source/_static/css/larger-headings.css
+++ b/docs/source/_static/css/larger-headings.css
@@ -4,14 +4,6 @@ h3,
h4,
h5,
h6 {
- margin-top: 2em !important;
+ margin-top: 1.5em !important;
font-weight: 900 !important;
}
-
-h2 {
- font-size: 1.8em !important;
-}
-
-h3 {
- font-size: 1.4em !important;
-}
diff --git a/docs/source/_static/css/sphinx-design-overrides.css b/docs/source/_static/css/sphinx-design-overrides.css
new file mode 100644
index 000000000..a6866268b
--- /dev/null
+++ b/docs/source/_static/css/sphinx-design-overrides.css
@@ -0,0 +1,19 @@
+body {
+ --sd-color-info: var(--color-admonition-title-background--note);
+ --sd-color-warning: var(--color-admonition-title-background--warning);
+ --sd-color-danger: var(--color-admonition-title-background--danger);
+ --sd-color-info-text: var(--color-admonition-title--note);
+ --sd-color-warning-text: var(--color-admonition-title--warning);
+ --sd-color-danger-text: var(--color-admonition-title--danger);
+}
+
+.sd-card-body {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.sd-tab-content .highlight pre {
+ max-height: 700px;
+ overflow: auto;
+}
diff --git a/docs/source/_static/css/widget-output-css-overrides.css b/docs/source/_static/css/widget-output-css-overrides.css
new file mode 100644
index 000000000..7ddf1a792
--- /dev/null
+++ b/docs/source/_static/css/widget-output-css-overrides.css
@@ -0,0 +1,8 @@
+.widget-container h1,
+.widget-container h2,
+.widget-container h3,
+.widget-container h4,
+.widget-container h5,
+.widget-container h6 {
+ margin: 0 !important;
+}
diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js
index b2206d073..aaac847fb 100644
--- a/docs/source/_static/custom.js
+++ b/docs/source/_static/custom.js
@@ -1483,7 +1483,17 @@ const targetTransformCategories = {
};
const targetTagCategories = {
- hasValue: ["BUTTON", "INPUT", "OPTION", "LI", "METER", "PROGRESS", "PARAM"],
+ hasValue: [
+ "BUTTON",
+ "INPUT",
+ "OPTION",
+ "LI",
+ "METER",
+ "PROGRESS",
+ "PARAM",
+ "SELECT",
+ "TEXTAREA",
+ ],
hasCurrentTime: ["AUDIO", "VIDEO"],
hasFiles: ["INPUT"],
};
@@ -1911,7 +1921,7 @@ function mountLayoutWithReconnectingWebSocket(
const updateHookPromise = new LazyPromise();
socket.onopen = (event) => {
- console.log(`Connected.`);
+ console.info(`IDOM WebSocket connected.`);
if (mountState.everMounted) {
reactDom.unmountComponentAtNode(element);
@@ -1932,7 +1942,7 @@ function mountLayoutWithReconnectingWebSocket(
socket.onclose = (event) => {
if (!maxReconnectTimeout) {
- console.log(`Connection lost.`);
+ console.info(`IDOM WebSocket connection lost.`);
return;
}
@@ -1941,7 +1951,9 @@ function mountLayoutWithReconnectingWebSocket(
mountState
);
- console.log(`Connection lost, reconnecting in ${reconnectTimeout} seconds`);
+ console.info(
+ `IDOM WebSocket connection lost. Reconnecting in ${reconnectTimeout} seconds...`
+ );
setTimeout(function () {
mountState.reconnectAttempts++;
@@ -1977,10 +1989,37 @@ function LazyPromise() {
});
}
-const LOC = window.location;
-const HTTP_PROTO = LOC.protocol;
-const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:";
-const IDOM_MODULES_PATH = "/_modules";
+function mountWithLayoutServer(
+ element,
+ serverInfo,
+ maxReconnectTimeout
+) {
+ const loadImportSource = (source, sourceType) =>
+ import(sourceType == "NAME" ? serverInfo.path.module(source) : source);
+
+ mountLayoutWithWebSocket(
+ element,
+ serverInfo.path.stream,
+ loadImportSource,
+ maxReconnectTimeout
+ );
+}
+
+function LayoutServerInfo({ host, port, path, query, secure }) {
+ const wsProtocol = "ws" + (secure ? "s" : "");
+ const httpProtocol = "http" + (secure ? "s" : "");
+
+ const uri = host + ":" + port;
+ const url = (uri + path).split("/").slice(0, -1).join("/");
+
+ const wsBaseUrl = wsProtocol + "://" + url;
+ const httpBaseUrl = httpProtocol + "://" + url;
+
+ this.path = {
+ stream: wsBaseUrl + "/stream" + "?" + query,
+ module: (source) => httpBaseUrl + `/modules/${source}`,
+ };
+}
function mountWidgetExample(
mountID,
@@ -1988,24 +2027,31 @@ function mountWidgetExample(
idomServerHost,
useActivateButton
) {
- const idomUrl = "//" + (idomServerHost || LOC.host);
- const httpIdomUrl = HTTP_PROTO + idomUrl ;
- const wsIdomUrl = WS_PROTO + idomUrl ;
+ let idomHost, idomPort;
+ if (idomServerHost) {
+ [idomHost, idomPort] = idomServerHost.split(":", 2);
+ } else {
+ idomHost = window.location.hostname;
+ idomPort = window.location.port;
+ }
+
+ const serverInfo = new LayoutServerInfo({
+ host: idomHost,
+ port: idomPort,
+ path: "/_idom/",
+ query: `view_id=${viewID}`,
+ secure: window.location.protocol == "https",
+ });
const mountEl = document.getElementById(mountID);
if (!useActivateButton) {
- mountLayoutWithWebSocket(
- mountEl,
- wsIdomUrl + `/_idom/stream?view_id=${viewID}`,
- (source, sourceType) =>
- loadImportSource(httpIdomUrl, source, sourceType)
- );
+ mountWithLayoutServer(mountEl, serverInfo);
return;
}
const enableWidgetButton = document.createElement("button");
- enableWidgetButton.appendChild(document.createTextNode("Enable Widget"));
+ enableWidgetButton.appendChild(document.createTextNode("Activate"));
enableWidgetButton.setAttribute("class", "enable-widget-button");
enableWidgetButton.addEventListener("click", () =>
@@ -2013,12 +2059,7 @@ function mountWidgetExample(
{
mountEl.removeChild(enableWidgetButton);
mountEl.setAttribute("class", "interactive widget-container");
- mountLayoutWithWebSocket(
- mountEl,
- wsIdomUrl + `/_idom/stream?view_id=${viewID}`,
- (source, sourceType) =>
- loadImportSource(httpIdomUrl, source, sourceType)
- );
+ mountWithLayoutServer(mountEl, serverInfo);
}
})
);
@@ -2046,12 +2087,4 @@ function mountWidgetExample(
mountEl.appendChild(enableWidgetButton);
}
-function loadImportSource(baseUrl, source, sourceType) {
- if (sourceType == "NAME") {
- return import(baseUrl + IDOM_MODULES_PATH + "/" + source);
- } else {
- return import(source);
- }
-}
-
export { mountWidgetExample };
diff --git a/docs/source/_static/custom_victory_chart.png b/docs/source/_static/custom_victory_chart.png
deleted file mode 100644
index c6025a285..000000000
Binary files a/docs/source/_static/custom_victory_chart.png and /dev/null differ
diff --git a/docs/source/_static/install-and-run-idom.gif b/docs/source/_static/install-and-run-idom.gif
new file mode 100644
index 000000000..67d226a12
Binary files /dev/null and b/docs/source/_static/install-and-run-idom.gif differ
diff --git a/docs/source/_static/primary_secondary_buttons.png b/docs/source/_static/primary_secondary_buttons.png
deleted file mode 100644
index a511c6141..000000000
Binary files a/docs/source/_static/primary_secondary_buttons.png and /dev/null differ
diff --git a/docs/source/_static/victory_bar_default_chart.png b/docs/source/_static/victory_bar_default_chart.png
deleted file mode 100644
index 548918375..000000000
Binary files a/docs/source/_static/victory_bar_default_chart.png and /dev/null differ
diff --git a/docs/source/adding-interactivity/_examples/adding_state_variable/app.py b/docs/source/adding-interactivity/_examples/adding_state_variable/app.py
new file mode 100644
index 000000000..5a96d2f03
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/adding_state_variable/app.py
@@ -0,0 +1,36 @@
+import json
+from pathlib import Path
+
+from idom import component, hooks, html, run
+
+
+HERE = Path(__file__)
+DATA_PATH = HERE.parent / "data.json"
+sculpture_data = json.loads(DATA_PATH.read_text())
+
+
+@component
+def Gallery():
+ index, set_index = hooks.use_state(0)
+
+ def handle_click(event):
+ set_index(index + 1)
+
+ bounded_index = index % len(sculpture_data)
+ sculpture = sculpture_data[bounded_index]
+ alt = sculpture["alt"]
+ artist = sculpture["artist"]
+ description = sculpture["description"]
+ name = sculpture["name"]
+ url = sculpture["url"]
+
+ return html.div(
+ html.button({"onClick": handle_click}, "Next"),
+ html.h2(name, " by ", artist),
+ html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
+ html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
+ html.p(description),
+ )
+
+
+run(Gallery)
diff --git a/docs/source/adding-interactivity/_examples/adding_state_variable/data.json b/docs/source/adding-interactivity/_examples/adding_state_variable/data.json
new file mode 100644
index 000000000..225bc6807
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/adding_state_variable/data.json
@@ -0,0 +1,79 @@
+[
+ {
+ "name": "Homenaje a la Neurocirugía",
+ "artist": "Marta Colvin Andrade",
+ "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
+ "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
+ },
+ {
+ "name": "Floralis Genérica",
+ "artist": "Eduardo Catalano",
+ "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg/300px-La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg",
+ "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens."
+ },
+ {
+ "name": "Eternal Presence",
+ "artist": "John Woodrow Wilson",
+ "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
+ "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
+ },
+ {
+ "name": "Moai",
+ "artist": "Unknown Artist",
+ "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
+ "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
+ },
+ {
+ "name": "Blue Nana",
+ "artist": "Niki de Saint Phalle",
+ "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
+ "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
+ },
+ {
+ "name": "Cavaliere",
+ "artist": "Lamidi Olonade Fakeye",
+ "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
+ "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
+ },
+ {
+ "name": "Big Bellies",
+ "artist": "Alina Szapocznikow",
+ "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
+ "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
+ },
+ {
+ "name": "Terracotta Army",
+ "artist": "Unknown Artist",
+ "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
+ "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
+ },
+ {
+ "name": "Lunar Landscape",
+ "artist": "Louise Nevelson",
+ "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
+ "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
+ },
+ {
+ "name": "Aureole",
+ "artist": "Ranjani Shettar",
+ "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
+ "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
+ },
+ {
+ "name": "Hippos",
+ "artist": "Taipei Zoo",
+ "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
+ "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
+ }
+]
diff --git a/docs/source/examples/audio_player.py b/docs/source/adding-interactivity/_examples/audio_player.py
similarity index 100%
rename from docs/source/examples/audio_player.py
rename to docs/source/adding-interactivity/_examples/audio_player.py
diff --git a/docs/source/adding-interactivity/_examples/button_async_handlers.py b/docs/source/adding-interactivity/_examples/button_async_handlers.py
new file mode 100644
index 000000000..a355f6142
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/button_async_handlers.py
@@ -0,0 +1,23 @@
+import asyncio
+
+from idom import component, html, run
+
+
+@component
+def ButtonWithDelay(message, delay):
+ async def handle_event(event):
+ await asyncio.sleep(delay)
+ print(message)
+
+ return html.button({"onClick": handle_event}, message)
+
+
+@component
+def App():
+ return html.div(
+ ButtonWithDelay("print 3 seconds later", delay=3),
+ ButtonWithDelay("print immediately", delay=0),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/button_does_nothing.py b/docs/source/adding-interactivity/_examples/button_does_nothing.py
new file mode 100644
index 000000000..e26e770d2
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/button_does_nothing.py
@@ -0,0 +1,9 @@
+from idom import component, html, run
+
+
+@component
+def Button():
+ return html.button("I don't do anything yet")
+
+
+run(Button)
diff --git a/docs/source/adding-interactivity/_examples/button_handler_as_arg.py b/docs/source/adding-interactivity/_examples/button_handler_as_arg.py
new file mode 100644
index 000000000..4de22a024
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/button_handler_as_arg.py
@@ -0,0 +1,33 @@
+from idom import component, html, run
+
+
+@component
+def Button(display_text, on_click):
+ return html.button({"onClick": on_click}, display_text)
+
+
+@component
+def PlayButton(movie_name):
+ def handle_click(event):
+ print(f"Playing {movie_name}")
+
+ return Button(f"Play {movie_name}", on_click=handle_click)
+
+
+@component
+def FastForwardButton():
+ def handle_click(event):
+ print("Skipping ahead")
+
+ return Button("Fast forward", on_click=handle_click)
+
+
+@component
+def App():
+ return html.div(
+ PlayButton("Buena Vista Social Club"),
+ FastForwardButton(),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/button_prints_event.py b/docs/source/adding-interactivity/_examples/button_prints_event.py
new file mode 100644
index 000000000..eac05a588
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/button_prints_event.py
@@ -0,0 +1,12 @@
+from idom import component, html, run
+
+
+@component
+def Button():
+ def handle_event(event):
+ print(event)
+
+ return html.button({"onClick": handle_event}, "Click me!")
+
+
+run(Button)
diff --git a/docs/source/adding-interactivity/_examples/button_prints_message.py b/docs/source/adding-interactivity/_examples/button_prints_message.py
new file mode 100644
index 000000000..f5ee69f80
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/button_prints_message.py
@@ -0,0 +1,20 @@
+from idom import component, html, run
+
+
+@component
+def PrintButton(display_text, message_text):
+ def handle_event(event):
+ print(message_text)
+
+ return html.button({"onClick": handle_event}, display_text)
+
+
+@component
+def App():
+ return html.div(
+ PrintButton("Play", "Playing"),
+ PrintButton("Pause", "Paused"),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/delay_before_count_updater.py b/docs/source/adding-interactivity/_examples/delay_before_count_updater.py
new file mode 100644
index 000000000..0c000477e
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/delay_before_count_updater.py
@@ -0,0 +1,20 @@
+import asyncio
+
+from idom import component, html, run, use_state
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ async def handle_click(event):
+ await asyncio.sleep(3)
+ set_number(lambda old_number: old_number + 1)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/delay_before_set_count.py b/docs/source/adding-interactivity/_examples/delay_before_set_count.py
new file mode 100644
index 000000000..024df12e7
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/delay_before_set_count.py
@@ -0,0 +1,20 @@
+import asyncio
+
+from idom import component, html, run, use_state
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ async def handle_click(event):
+ await asyncio.sleep(3)
+ set_number(number + 1)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/delayed_print_after_set.py b/docs/source/adding-interactivity/_examples/delayed_print_after_set.py
new file mode 100644
index 000000000..5471616d4
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/delayed_print_after_set.py
@@ -0,0 +1,22 @@
+import asyncio
+
+from idom import component, html, run, use_state
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ async def handle_click(event):
+ set_number(number + 5)
+ print("about to print...")
+ await asyncio.sleep(3)
+ print(number)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/isolated_state/app.py b/docs/source/adding-interactivity/_examples/isolated_state/app.py
new file mode 100644
index 000000000..08a53d1c6
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/isolated_state/app.py
@@ -0,0 +1,54 @@
+import json
+from pathlib import Path
+
+from idom import component, hooks, html, run
+
+
+HERE = Path(__file__)
+DATA_PATH = HERE.parent / "data.json"
+sculpture_data = json.loads(DATA_PATH.read_text())
+
+
+@component
+def Gallery():
+ index, set_index = hooks.use_state(0)
+ show_more, set_show_more = hooks.use_state(False)
+
+ def handle_next_click(event):
+ set_index(index + 1)
+
+ def handle_more_click(event):
+ set_show_more(not show_more)
+
+ bounded_index = index % len(sculpture_data)
+ sculpture = sculpture_data[bounded_index]
+ alt = sculpture["alt"]
+ artist = sculpture["artist"]
+ description = sculpture["description"]
+ name = sculpture["name"]
+ url = sculpture["url"]
+
+ return html.div(
+ html.button({"onClick": handle_next_click}, "Next"),
+ html.h2(name, " by ", artist),
+ html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
+ html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
+ html.div(
+ html.button(
+ {"onClick": handle_more_click},
+ f"{'Show' if show_more else 'Hide'} details",
+ ),
+ (html.p(description) if show_more else ""),
+ ),
+ )
+
+
+@component
+def App():
+ return html.div(
+ html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
+ html.section({"style": {"width": "50%", "float": "left"}}, Gallery()),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/isolated_state/data.json b/docs/source/adding-interactivity/_examples/isolated_state/data.json
new file mode 100644
index 000000000..225bc6807
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/isolated_state/data.json
@@ -0,0 +1,79 @@
+[
+ {
+ "name": "Homenaje a la Neurocirugía",
+ "artist": "Marta Colvin Andrade",
+ "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
+ "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
+ },
+ {
+ "name": "Floralis Genérica",
+ "artist": "Eduardo Catalano",
+ "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg/300px-La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg",
+ "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens."
+ },
+ {
+ "name": "Eternal Presence",
+ "artist": "John Woodrow Wilson",
+ "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
+ "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
+ },
+ {
+ "name": "Moai",
+ "artist": "Unknown Artist",
+ "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
+ "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
+ },
+ {
+ "name": "Blue Nana",
+ "artist": "Niki de Saint Phalle",
+ "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
+ "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
+ },
+ {
+ "name": "Cavaliere",
+ "artist": "Lamidi Olonade Fakeye",
+ "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
+ "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
+ },
+ {
+ "name": "Big Bellies",
+ "artist": "Alina Szapocznikow",
+ "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
+ "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
+ },
+ {
+ "name": "Terracotta Army",
+ "artist": "Unknown Artist",
+ "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
+ "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
+ },
+ {
+ "name": "Lunar Landscape",
+ "artist": "Louise Nevelson",
+ "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
+ "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
+ },
+ {
+ "name": "Aureole",
+ "artist": "Ranjani Shettar",
+ "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
+ "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
+ },
+ {
+ "name": "Hippos",
+ "artist": "Taipei Zoo",
+ "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
+ "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
+ }
+]
diff --git a/docs/source/adding-interactivity/_examples/multiple_state_variables/app.py b/docs/source/adding-interactivity/_examples/multiple_state_variables/app.py
new file mode 100644
index 000000000..3e7f7bde4
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/multiple_state_variables/app.py
@@ -0,0 +1,46 @@
+import json
+from pathlib import Path
+
+from idom import component, hooks, html, run
+
+
+HERE = Path(__file__)
+DATA_PATH = HERE.parent / "data.json"
+sculpture_data = json.loads(DATA_PATH.read_text())
+
+
+@component
+def Gallery():
+ index, set_index = hooks.use_state(0)
+ show_more, set_show_more = hooks.use_state(False)
+
+ def handle_next_click(event):
+ set_index(index + 1)
+
+ def handle_more_click(event):
+ set_show_more(not show_more)
+
+ bounded_index = index % len(sculpture_data)
+ sculpture = sculpture_data[bounded_index]
+ alt = sculpture["alt"]
+ artist = sculpture["artist"]
+ description = sculpture["description"]
+ name = sculpture["name"]
+ url = sculpture["url"]
+
+ return html.div(
+ html.button({"onClick": handle_next_click}, "Next"),
+ html.h2(name, " by ", artist),
+ html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
+ html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
+ html.div(
+ html.button(
+ {"onClick": handle_more_click},
+ f"{'Show' if show_more else 'Hide'} details",
+ ),
+ (html.p(description) if show_more else ""),
+ ),
+ )
+
+
+run(Gallery)
diff --git a/docs/source/adding-interactivity/_examples/multiple_state_variables/data.json b/docs/source/adding-interactivity/_examples/multiple_state_variables/data.json
new file mode 100644
index 000000000..225bc6807
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/multiple_state_variables/data.json
@@ -0,0 +1,79 @@
+[
+ {
+ "name": "Homenaje a la Neurocirugía",
+ "artist": "Marta Colvin Andrade",
+ "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
+ "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
+ },
+ {
+ "name": "Floralis Genérica",
+ "artist": "Eduardo Catalano",
+ "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg/300px-La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg",
+ "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens."
+ },
+ {
+ "name": "Eternal Presence",
+ "artist": "John Woodrow Wilson",
+ "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
+ "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
+ },
+ {
+ "name": "Moai",
+ "artist": "Unknown Artist",
+ "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
+ "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
+ },
+ {
+ "name": "Blue Nana",
+ "artist": "Niki de Saint Phalle",
+ "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
+ "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
+ },
+ {
+ "name": "Cavaliere",
+ "artist": "Lamidi Olonade Fakeye",
+ "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
+ "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
+ },
+ {
+ "name": "Big Bellies",
+ "artist": "Alina Szapocznikow",
+ "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
+ "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
+ },
+ {
+ "name": "Terracotta Army",
+ "artist": "Unknown Artist",
+ "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
+ "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
+ },
+ {
+ "name": "Lunar Landscape",
+ "artist": "Louise Nevelson",
+ "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
+ "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
+ },
+ {
+ "name": "Aureole",
+ "artist": "Ranjani Shettar",
+ "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
+ "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
+ },
+ {
+ "name": "Hippos",
+ "artist": "Taipei Zoo",
+ "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
+ "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
+ }
+]
diff --git a/docs/source/adding-interactivity/_examples/prevent_default_event_actions.py b/docs/source/adding-interactivity/_examples/prevent_default_event_actions.py
new file mode 100644
index 000000000..7e8ef9938
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/prevent_default_event_actions.py
@@ -0,0 +1,18 @@
+from idom import component, event, html, run
+
+
+@component
+def DoNotChangePages():
+ return html.div(
+ html.p("Normally clicking this link would take you to a new page"),
+ html.a(
+ {
+ "onClick": event(lambda event: None, prevent_default=True),
+ "href": "https://google.com",
+ },
+ "https://google.com",
+ ),
+ )
+
+
+run(DoNotChangePages)
diff --git a/docs/source/adding-interactivity/_examples/print_chat_message.py b/docs/source/adding-interactivity/_examples/print_chat_message.py
new file mode 100644
index 000000000..ae81fbebd
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/print_chat_message.py
@@ -0,0 +1,43 @@
+import asyncio
+
+from idom import component, event, html, run, use_state
+
+
+@component
+def App():
+ recipient, set_recipient = use_state("Alice")
+ message, set_message = use_state("")
+
+ @event(prevent_default=True)
+ async def handle_submit(event):
+ set_message("")
+ print("About to send message...")
+ await asyncio.sleep(5)
+ print(f"Sent '{message}' to {recipient}")
+
+ return html.form(
+ {"onSubmit": handle_submit, "style": {"display": "inline-grid"}},
+ html.label(
+ "To: ",
+ html.select(
+ {
+ "value": recipient,
+ "onChange": lambda event: set_recipient(event["value"]),
+ },
+ html.option({"value": "Alice"}, "Alice"),
+ html.option({"value": "Bob"}, "Bob"),
+ ),
+ ),
+ html.input(
+ {
+ "type": "text",
+ "placeholder": "Your message...",
+ "value": message,
+ "onChange": lambda event: set_message(event["value"]),
+ }
+ ),
+ html.button({"type": "submit"}, "Send"),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/print_count_after_set.py b/docs/source/adding-interactivity/_examples/print_count_after_set.py
new file mode 100644
index 000000000..039a261d9
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/print_count_after_set.py
@@ -0,0 +1,18 @@
+from idom import component, html, run, use_state
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ def handle_click(event):
+ set_number(number + 5)
+ print(number)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/send_message.py b/docs/source/adding-interactivity/_examples/send_message.py
new file mode 100644
index 000000000..61c938797
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/send_message.py
@@ -0,0 +1,35 @@
+from idom import component, event, html, run, use_state
+
+
+@component
+def App():
+ is_sent, set_is_sent = use_state(False)
+ message, set_message = use_state("")
+
+ if is_sent:
+ return html.div(
+ html.h1("Message sent!"),
+ html.button(
+ {"onClick": lambda event: set_is_sent(False)}, "Send new message?"
+ ),
+ )
+
+ @event(prevent_default=True)
+ def handle_submit(event):
+ set_message("")
+ set_is_sent(True)
+
+ return html.form(
+ {"onSubmit": handle_submit, "style": {"display": "inline-grid"}},
+ html.textarea(
+ {
+ "placeholder": "Your message here...",
+ "value": message,
+ "onChange": lambda event: set_message(event["value"]),
+ }
+ ),
+ html.button({"type": "submit"}, "Send"),
+ )
+
+
+run(App)
diff --git a/docs/source/adding-interactivity/_examples/set_color_3_times.py b/docs/source/adding-interactivity/_examples/set_color_3_times.py
new file mode 100644
index 000000000..e755c35b9
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/set_color_3_times.py
@@ -0,0 +1,26 @@
+from idom import component, html, run, use_state
+
+
+@component
+def ColorButton():
+ color, set_color = use_state("gray")
+
+ def handle_click(event):
+ set_color("orange")
+ set_color("pink")
+ set_color("blue")
+
+ def handle_reset(event):
+ set_color("gray")
+
+ return html.div(
+ html.button(
+ {"onClick": handle_click, "style": {"backgroundColor": color}}, "Set Color"
+ ),
+ html.button(
+ {"onClick": handle_reset, "style": {"backgroundColor": color}}, "Reset"
+ ),
+ )
+
+
+run(ColorButton)
diff --git a/docs/source/adding-interactivity/_examples/set_counter_3_times.py b/docs/source/adding-interactivity/_examples/set_counter_3_times.py
new file mode 100644
index 000000000..24801d47b
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/set_counter_3_times.py
@@ -0,0 +1,19 @@
+from idom import component, html, run, use_state
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ def handle_click(event):
+ set_number(number + 1)
+ set_number(number + 1)
+ set_number(number + 1)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/set_state_function.py b/docs/source/adding-interactivity/_examples/set_state_function.py
new file mode 100644
index 000000000..ec3193de9
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/set_state_function.py
@@ -0,0 +1,24 @@
+from idom import component, html, run, use_state
+
+
+def increment(old_number):
+ new_number = old_number + 1
+ return new_number
+
+
+@component
+def Counter():
+ number, set_number = use_state(0)
+
+ def handle_click(event):
+ set_number(increment)
+ set_number(increment)
+ set_number(increment)
+
+ return html.div(
+ html.h1(number),
+ html.button({"onClick": handle_click}, "Increment"),
+ )
+
+
+run(Counter)
diff --git a/docs/source/adding-interactivity/_examples/stop_event_propagation.py b/docs/source/adding-interactivity/_examples/stop_event_propagation.py
new file mode 100644
index 000000000..e87bae026
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/stop_event_propagation.py
@@ -0,0 +1,38 @@
+from idom import component, event, hooks, html, run
+
+
+@component
+def DivInDiv():
+ stop_propagatation, set_stop_propagatation = hooks.use_state(True)
+ inner_count, set_inner_count = hooks.use_state(0)
+ outer_count, set_outer_count = hooks.use_state(0)
+
+ div_in_div = html.div(
+ {
+ "onClick": lambda event: set_outer_count(outer_count + 1),
+ "style": {"height": "100px", "width": "100px", "backgroundColor": "red"},
+ },
+ html.div(
+ {
+ "onClick": event(
+ lambda event: set_inner_count(inner_count + 1),
+ stop_propagation=stop_propagatation,
+ ),
+ "style": {"height": "50px", "width": "50px", "backgroundColor": "blue"},
+ },
+ ),
+ )
+
+ return html.div(
+ html.button(
+ {"onClick": lambda event: set_stop_propagatation(not stop_propagatation)},
+ "Toggle Propogation",
+ ),
+ html.pre(f"Will propagate: {not stop_propagatation}"),
+ html.pre(f"Inner click count: {inner_count}"),
+ html.pre(f"Outer click count: {outer_count}"),
+ div_in_div,
+ )
+
+
+run(DivInDiv)
diff --git a/docs/source/adding-interactivity/_examples/when_variables_arent_enough/app.py b/docs/source/adding-interactivity/_examples/when_variables_arent_enough/app.py
new file mode 100644
index 000000000..f8679cbfc
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/when_variables_arent_enough/app.py
@@ -0,0 +1,42 @@
+# flake8: noqa
+# errors F841,F823 for `index = index + 1` inside the closure
+
+# :lines: 7-
+# :linenos:
+
+import json
+from pathlib import Path
+
+from idom import component, html, run
+
+
+HERE = Path(__file__)
+DATA_PATH = HERE.parent / "data.json"
+sculpture_data = json.loads(DATA_PATH.read_text())
+
+
+@component
+def Gallery():
+ index = 0
+
+ def handle_click(event):
+ index = index + 1
+
+ bounded_index = index % len(sculpture_data)
+ sculpture = sculpture_data[bounded_index]
+ alt = sculpture["alt"]
+ artist = sculpture["artist"]
+ description = sculpture["description"]
+ name = sculpture["name"]
+ url = sculpture["url"]
+
+ return html.div(
+ html.button({"onClick": handle_click}, "Next"),
+ html.h2(name, " by ", artist),
+ html.p(f"({bounded_index + 1} or {len(sculpture_data)})"),
+ html.img({"src": url, "alt": alt, "style": {"height": "200px"}}),
+ html.p(description),
+ )
+
+
+run(Gallery)
diff --git a/docs/source/adding-interactivity/_examples/when_variables_arent_enough/data.json b/docs/source/adding-interactivity/_examples/when_variables_arent_enough/data.json
new file mode 100644
index 000000000..225bc6807
--- /dev/null
+++ b/docs/source/adding-interactivity/_examples/when_variables_arent_enough/data.json
@@ -0,0 +1,79 @@
+[
+ {
+ "name": "Homenaje a la Neurocirugía",
+ "artist": "Marta Colvin Andrade",
+ "description": "Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg/1024px-Homenaje_a_la_Neurocirug%C3%ADa%2C_Instituto_de_Neurocirug%C3%ADa%2C_Providencia%2C_Santiago_20200106_02.jpg",
+ "alt": "A bronze statue of two crossed hands delicately holding a human brain in their fingertips."
+ },
+ {
+ "name": "Floralis Genérica",
+ "artist": "Eduardo Catalano",
+ "description": "This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg/300px-La_Flor_-_Plaza_de_las_Naciones_Unidas.jpg",
+ "alt": "A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens."
+ },
+ {
+ "name": "Eternal Presence",
+ "artist": "John Woodrow Wilson",
+ "description": "Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as \"a symbolic Black presence infused with a sense of universal humanity.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/6/6f/Chicago%2C_Illinois_Eternal_Silence1_crop.jpg",
+ "alt": "The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity."
+ },
+ {
+ "name": "Moai",
+ "artist": "Unknown Artist",
+ "description": "Located on the Easter Island, there are 1,000 moai, or extant monumental statues, created by the early Rapa Nui people, which some believe represented deified ancestors.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/5/50/AhuTongariki.JPG",
+ "alt": "Three monumental stone busts with the heads that are disproportionately large with somber faces."
+ },
+ {
+ "name": "Blue Nana",
+ "artist": "Niki de Saint Phalle",
+ "description": "The Nanas are triumphant creatures, symbols of femininity and maternity. Initially, Saint Phalle used fabric and found objects for the Nanas, and later on introduced polyester to achieve a more vibrant effect.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c8/Blue_Nana_-_panoramio.jpg/1024px-Blue_Nana_-_panoramio.jpg",
+ "alt": "A large mosaic sculpture of a whimsical dancing female figure in a colorful costume emanating joy."
+ },
+ {
+ "name": "Cavaliere",
+ "artist": "Lamidi Olonade Fakeye",
+ "description": "Descended from four generations of woodcarvers, Fakeye's work blended traditional and contemporary Yoruba themes.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/3/34/Nigeria%2C_lamidi_olonade_fakeye%2C_cavaliere%2C_1992.jpg",
+ "alt": "An intricate wood sculpture of a warrior with a focused face on a horse adorned with patterns."
+ },
+ {
+ "name": "Big Bellies",
+ "artist": "Alina Szapocznikow",
+ "description": "Szapocznikow is known for her sculptures of the fragmented body as a metaphor for the fragility and impermanence of youth and beauty. This sculpture depicts two very realistic large bellies stacked on top of each other, each around five feet (1,5m) tall.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/KMM_Szapocznikow.JPG/200px-KMM_Szapocznikow.JPG",
+ "alt": "The sculpture reminds a cascade of folds, quite different from bellies in classical sculptures."
+ },
+ {
+ "name": "Terracotta Army",
+ "artist": "Unknown Artist",
+ "description": "The Terracotta Army is a collection of terracotta sculptures depicting the armies of Qin Shi Huang, the first Emperor of China. The army consited of more than 8,000 soldiers, 130 chariots with 520 horses, and 150 cavalry horses.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/9a/2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg/1920px-2015-09-22-081415_-_Terrakotta-Armee%2C_Grosse_Halle.jpg",
+ "alt": "12 terracotta sculptures of solemn warriors, each with a unique facial expression and armor."
+ },
+ {
+ "name": "Lunar Landscape",
+ "artist": "Louise Nevelson",
+ "description": "Nevelson was known for scavenging objects from New York City debris, which she would later assemble into monumental constructions. In this one, she used disparate parts like a bedpost, juggling pin, and seat fragment, nailing and gluing them into boxes that reflect the influence of Cubism’s geometric abstraction of space and form.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/1999-3-A--J_s.jpg/220px-1999-3-A--J_s.jpg",
+ "alt": "A black matte sculpture where the individual elements are initially indistinguishable."
+ },
+ {
+ "name": "Aureole",
+ "artist": "Ranjani Shettar",
+ "description": "Shettar merges the traditional and the modern, the natural and the industrial. Her art focuses on the relationship between man and nature. Her work was described as compelling both abstractly and figuratively, gravity defying, and a \"fine synthesis of unlikely materials.\"",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Shettar-_5854-sm_%285132866765%29.jpg/399px-Shettar-_5854-sm_%285132866765%29.jpg",
+ "alt": "A pale wire-like sculpture mounted on concrete wall and descending on the floor. It appears light."
+ },
+ {
+ "name": "Hippos",
+ "artist": "Taipei Zoo",
+ "description": "The Taipei Zoo commissioned a Hippo Square featuring submerged hippos at play.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e3/Hippo_sculpture_Taipei_Zoo_20543.jpg/250px-Hippo_sculpture_Taipei_Zoo_20543.jpg",
+ "alt": "A group of bronze hippo sculptures emerging from the sett sidewalk as if they were swimming."
+ }
+]
diff --git a/docs/source/adding-interactivity/_static/direct-state-change.png b/docs/source/adding-interactivity/_static/direct-state-change.png
new file mode 100644
index 000000000..fdcfebd1e
Binary files /dev/null and b/docs/source/adding-interactivity/_static/direct-state-change.png differ
diff --git a/docs/source/adding-interactivity/_static/idom-state-change.png b/docs/source/adding-interactivity/_static/idom-state-change.png
new file mode 100644
index 000000000..ec13fdc7a
Binary files /dev/null and b/docs/source/adding-interactivity/_static/idom-state-change.png differ
diff --git a/docs/source/adding-interactivity/components-with-state.rst b/docs/source/adding-interactivity/components-with-state.rst
new file mode 100644
index 000000000..bbd95be73
--- /dev/null
+++ b/docs/source/adding-interactivity/components-with-state.rst
@@ -0,0 +1,351 @@
+Components With State
+=====================
+
+Components often need to change what’s on the screen as a result of an interaction. For
+example, typing into the form should update the input field, clicking “next” on an image
+carousel should change which image is displayed, clicking “buy” should put a product in
+the shopping cart. Components need to “remember” things like the current input value,
+the current image, the shopping cart. In IDOM, this kind of component-specific memory is
+called state.
+
+
+When Variables Aren't Enough
+----------------------------
+
+Below is a gallery of images about sculpture. Clicking the "Next" button should
+increment the ``index`` and, as a result, change what image is displayed. However, this
+does not work:
+
+.. idom:: _examples/when_variables_arent_enough
+
+.. note::
+
+ Try clicking the button to see that it does not cause a change.
+
+After clicking "Next", if you check the server logs, you'll discover an
+``UnboundLocalError`` error. It turns out that in this case, the ``index = index + 1``
+statement is similar to `trying to set global variables
+`__.
+Tehcnically there's a way to `fix this error
+`__, but even if we did,
+that still wouldn't fix the underlying problems:
+
+1. **Local variables do not persist across component renders** - when a component is
+ updated, its associated function gets called again. That is, it renders. As a result,
+ all the local state that was created the last time the function was called gets
+ destroyed when it updates.
+
+2. **Changes to local variables do not cause components to re-render** - there's no way
+ for IDOM to observe when these variables change. Thus IDOM is not aware that
+ something has changed and that a re-render should take place.
+
+To address these problems, IDOM provides the :func:`~idom.core.hooks.use_state` "hook"
+which provides:
+
+1. A **state variable** whose data is retained aross renders.
+
+2. A **state setter** function that can be used to update that variable and trigger a
+ render.
+
+
+Adding State to Components
+--------------------------
+
+To create a state variable and state setter with :func:`~idom.core.hooks.use_state` hook
+as described above, we'll begin by importing it:
+
+.. testcode::
+
+ from idom import use_state
+
+Then we'll make the following changes to our code :ref:`from before `:
+
+.. code-block:: diff
+
+ - index = 0
+ + index, set_index = use_state
+
+ def handle_click(event):
+ - index = index + 1
+ + set_index(index + 1)
+
+After making those changes we should get:
+
+.. code-block::
+ :linenos:
+ :lineno-start: 14
+
+ index, set_index = use_state(0)
+
+ def handle_click(event):
+ set_index(index + 1)
+
+We'll talk more about what this is doing :ref:`shortly `, but for
+now let's just verify that this does in fact fix the problems from before:
+
+.. idom:: _examples/adding_state_variable
+
+
+Your First Hook
+---------------
+
+In IDOM, ``use_state``, as well as any other function whose name starts with ``use``, is
+called a "hook". These are special functions that should only be called while IDOM is
+:ref:`rendering `. They let you "hook into" the different
+capabilities of IDOM's components of which ``use_state`` is just one (well get into the
+other :ref:`later `).
+
+While hooks are just normal functions, but it's helpful to think of them as
+:ref:`unconditioned ` declarations about a component's needs. In other
+words, you'll "use" hooks at the top of your component in the same way you might
+"import" modules at the top of your Python files.
+
+
+.. _Introduction to use_state:
+
+Introduction to ``use_state``
+-----------------------------
+
+When you call :func:`~idom.core.hooks.use_state` inside the body of a component's render
+function, you're declaring that this component needs to remember something. That
+"something" which needs to be remembered, is known as **state**. So when we look at an
+assignment expression like the one below
+
+.. code-block::
+
+ index, set_index = use_state(0)
+
+we should read it as saying that ``index`` is a piece of state which must be
+remembered by the component that declared it. The argument to ``use_state`` (in this
+case ``0``) is then conveying what the initial value for ``index`` is.
+
+We should then understand that each time the component which owns this state renders
+``use_state`` will return a tuple containing two values - the current value of the state
+(``index``) and a function to change that value the next time the component is rendered.
+Thus, in this example:
+
+- ``index`` - is a **state variable** containing the currently stored value.
+- ``set_index`` - is a **state setter** for changing that value and triggering a re-render
+ of the component.
+
+The convention is that, if you name your state variable ``thing``, your state setter
+should be named ``set_thing``. While you could name them anything you want, adhereing to
+the convention makes things easier to understand across projects.
+
+----
+
+To understand how this works in context, let's break down our example by examining key
+moments in the execution of the ``Gallery`` component. Each numbered tab in the section
+below highlights a line of code where something of interest occurs:
+
+.. hint::
+
+ Try clicking through the numbered tabs to each highlighted step of execution
+
+.. tab-set::
+
+ .. tab-item:: 1
+
+ .. raw:: html
+
+ Initial render
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 2
+
+ At this point, we've just begun to render the ``Gallery`` component. As yet,
+ IDOM is not aware that this component has any state or what view it will
+ display. This will change in a moment though when we move to the next line...
+
+ .. tab-item:: 2
+
+ .. raw:: html
+
+ Initial state declaration
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 3
+
+ The ``Gallery`` component has just declared some state. IDOM now knows that it
+ must remember the ``index`` and trigger an update of this component when
+ ``set_index`` is called. Currently the value of ``index`` is ``0`` as per the
+ default value given to ``use_state``. Thus, the resulting view will display
+ information about the first item in our ``sculpture_data`` list.
+
+ .. tab-item:: 3
+
+ .. raw:: html
+
+ Define event handler
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 5
+
+ We've now defined an event handler that we intend to assign to a button in the
+ view. This will respond once the user clicks that button. The action this
+ handler performs is to update the value of ``index`` and schedule our ``Gallery``
+ component to update.
+
+ .. tab-item:: 4
+
+ .. raw:: html
+
+ Return the view
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 16
+
+ The ``handle_click`` function we defined above has now been assigned to a button
+ in the view and we are about to display information about the first item in out
+ ``sculpture_data`` list. When the view is ultimately displayed, if a user clicks
+ the "Next" button, the handler we just assigned will be triggered. Until that
+ point though, the application will remain static.
+
+ .. tab-item:: 5
+
+ .. raw:: html
+
+ User interaction
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 5
+
+ A user has just clicked the button 🖱️! IDOM has sent information about the event
+ to the ``handle_click`` function and it is about to execute. In a moment we will
+ update the state of this component and schedule a re-render.
+
+ .. tab-item:: 6
+
+ .. raw:: html
+
+ New state is set
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 6
+
+ We've just now told IDOM that we want to update the state of our ``Gallery`` and
+ that it needs to be re-rendered. More specifically, we are incrementing its
+ ``index``, and once ``Gallery`` re-renders the index *will* be ``1``.
+ Importantly, at this point, **the value of ``index`` is still ``0``**! This will
+ only change once the component begins to re-render.
+
+ .. tab-item:: 7
+
+ .. raw:: html
+
+ Next render begins
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 2
+
+ The scheduled re-render of ``Gallery`` has just begun. IDOM has now updated its
+ internal state store such that, the next time we call ``use_state`` we will get
+ back the updated value of ``index``.
+
+ .. tab-item:: 8
+
+ .. raw:: html
+
+ Next state is acquired
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+ :emphasize-lines: 3
+
+ With IDOM's state store updated, as we call ``use_state``, instead of returning
+ ``0`` for the value of ``index`` as it did before, IDOM now returns the value
+ ``1``. With this change the view we display will be altered - instead of
+ displaying data for the first item in our ``sculpture_data`` list we will now
+ display information about the second.
+
+ .. tab-item:: 9
+
+ .. raw:: html
+
+ Repeat...
+
+ .. literalinclude:: _examples/adding_state_variable/app.py
+ :lines: 12-33
+
+ From this point on, the steps remain the same. The only difference being the
+ progressively incrementing ``index`` each time the user clicks the "Next" button
+ and the view which is altered to to reflect the currently indexed item in the
+ ``sculpture_data`` list.
+
+ .. note::
+
+ Once we reach the end of the ``sculpture_data`` list the view will return
+ back to the first item since we create a ``bounded_index`` by doing a modulo
+ of the index with the length of the list (``index % len(sculpture_data)``).
+ Ideally we would do this bounding at the time we call ``set_index`` to
+ prevent ``index`` from incrementing to infinity, but to keep things simple
+ in this examples, we've kept this logic separate.
+
+
+Multiple State Declarations
+---------------------------
+
+The powerful thing about hooks like :func:`~idom.core.hooks.use_state` is that you're
+not limited to just one state declaration. You can call ``use_state()`` as many times as
+you need to in one component. For example, in the example below we've added a
+``show_more`` state variable along with a few other modifications (e.g. renaming
+``handle_click``) to make the description for each sculpture optionally displayed. Only
+when the user clicks the "Show details" button is this description shown:
+
+.. idom:: _examples/multiple_state_variables
+
+It's generally a good idea to define separate state variables if the data they represent
+is unrelated. In this case, ``index`` corresponds to what sculpture information is being
+displayed and ``show_more`` is solely concerned with whether the description for a given
+sculpture is shown. Put other way ``index`` is concerned with *what* information is
+displayed while ``show_more`` is concerned with *how* it is displayed. Conversely
+though, if you have a form with many fields, it probably makes sense to have a single
+objec that holds the data for all the fields rather than an object per-field.
+
+.. note::
+
+ This topic is discussed more in the :ref:`structuring your state` section.
+
+
+State is Isolated and Private
+-----------------------------
+
+State is local to a component instance on the screen. In other words, if you render the
+same component twice, each copy will have completely isolated state! Changing one of
+them will not affect the other.
+
+In this example, the ``Gallery`` component from earlier is rendered twice with no
+changes to its logic. Try clicking the buttons inside each of the galleries. Notice that
+their state is independent:
+
+.. idom:: _examples/isolated_state
+ :result-is-default-tab:
+
+This is what makes state different from regular variables that you might declare at the
+top of your module. State is not tied to a particular function call or a place in the
+code, but it’s “local” to the specific place on the screen. You rendered two ``Gallery``
+components, so their state is stored separately.
+
+Also notice how the Page component doesn’t “know” anything about the Gallery state or
+even whether it has any. Unlike props, state is fully private to the component declaring
+it. The parent component can’t change it. This lets you add state to any component or
+remove it without impacting the rest of the components.
+
+.. card::
+ :link: /managing-state/shared-component-state
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ What if you wanted both galleries to keep their states in sync? The right way to do
+ it in IDOM is to remove state from child components and add it to their closest
+ shared parent.
diff --git a/docs/source/adding-interactivity/dangers-of-mutability.rst b/docs/source/adding-interactivity/dangers-of-mutability.rst
new file mode 100644
index 000000000..264dd6cd5
--- /dev/null
+++ b/docs/source/adding-interactivity/dangers-of-mutability.rst
@@ -0,0 +1,8 @@
+.. _Dangers of Mutability:
+
+Dangers of Mutability 🚧
+========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/adding-interactivity/index.rst b/docs/source/adding-interactivity/index.rst
new file mode 100644
index 000000000..dac476347
--- /dev/null
+++ b/docs/source/adding-interactivity/index.rst
@@ -0,0 +1,197 @@
+Adding Interactivity
+====================
+
+.. toctree::
+ :hidden:
+
+ responding-to-events
+ components-with-state
+ state-as-a-snapshot
+ multiple-state-updates
+ dangers-of-mutability
+
+
+.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
+ :color: info
+ :animate: fade-in
+ :open:
+
+ .. grid:: 2
+
+ .. grid-item-card:: :octicon:`bell` Responding to Events
+ :link: responding-to-events
+ :link-type: doc
+
+ Define event handlers and learn about the available event types they can be
+ bound to.
+
+ .. grid-item-card:: :octicon:`package-dependencies` Components With State
+ :link: components-with-state
+ :link-type: doc
+
+ Allow components to change what they display by saving and updating their
+ state.
+
+ .. grid-item-card:: :octicon:`device-camera-video` State as a Snapshot
+ :link: state-as-a-snapshot
+ :link-type: doc
+
+ Learn why state updates schedules a re-render, instead of being applied
+ immediately.
+
+ .. grid-item-card:: :octicon:`versions` Multiple State Updates
+ :link: multiple-state-updates
+ :link-type: doc
+
+ Learn how updates to a components state can be batched, or applied
+ incrementally.
+
+ .. grid-item-card:: :octicon:`issue-opened` Dangers of Mutability
+ :link: dangers-of-mutability
+ :link-type: doc
+
+ Under construction 🚧
+
+
+Section 1: Responding to Events
+-------------------------------
+
+IDOM lets you add event handlers to your parts of the interface. This means that you can
+define synchronous or asynchronous functions that are triggered when a particular user
+interaction occurs like clicking, hovering, of focusing on form inputs, and more.
+
+.. idom:: _examples/button_prints_message
+
+It may feel weird to define a function within a function like this, but doing so allows
+the ``handle_event`` function to access information from within the scope of the
+component. That's important if you want to use any arguments that may have beend passed
+your component in the handler.
+
+.. card::
+ :link: responding-to-events
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Define event handlers and learn about the available event types they can be bound
+ to.
+
+
+Section 2: Components with State
+--------------------------------
+
+Components often need to change what’s on the screen as a result of an interaction. For
+example, typing into the form should update the input field, clicking a “Comment” button
+should bring up a text input field, clicking “Buy” should put a product in the shopping
+cart. Components need to “remember” things like the current input value, the current
+image, the shopping cart. In IDOM, this kind of component-specific memory is created and
+updated with a "hook" called ``use_state()`` that creates a **state variable** and
+**state setter** respectively:
+
+.. idom:: _examples/adding_state_variable
+
+In IDOM, ``use_state``, as well as any other function whose name starts with ``use``, is
+called a "hook". These are special functions that should only be called while IDOM is
+:ref:`rendering `. They let you "hook into" the different
+capabilities of IDOM's components of which ``use_state`` is just one (well get into the
+other :ref:`later `).
+
+.. card::
+ :link: components-with-state
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Allow components to change what they display by saving and updating their state.
+
+
+Section 3: State as a Snapshot
+------------------------------
+
+As we :ref:`learned earlier `, state setters behave a little
+differently than you might exepct at first glance. Instead of updating your current
+handle on the setter's corresponding variable, it schedules a re-render of the component
+which owns the state.
+
+.. code-block::
+
+ count, set_count = use_state(0)
+ print(count) # prints: 0
+ set_count(count + 1) # schedule a re-render where count is 1
+ print(count) # still prints: 0
+
+This behavior of IDOM means that each render of a component is like taking a snapshot of
+the UI based on the component's state at that time. Treating state in this way can help
+reduce subtle bugs. For example, in the code below there's a simple chat app with a
+message input and recipient selector. The catch is that the message actually gets sent 5
+seconds after the "Send" button is clicked. So what would happen if we changed the
+recipient between the time the "Send" button was clicked and the moment the message is
+actually sent?
+
+.. idom:: _examples/print_chat_message
+
+As it turns out, changing the message recipient after pressing send does not change
+where the message ulitmately goes. However, one could imagine a bug where the recipient
+of a message is determined at the time the message is sent rather than at the time the
+"Send" button it clicked. Thus changing the recipient after pressing send would change
+where the message got sent.
+
+In many cases, IDOM avoids this class of bug entirely because it treats state as a
+snapshot.
+
+.. card::
+ :link: state-as-a-snapshot
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn why state updates schedules a re-render, instead of being applied immediately.
+
+
+Section 4: Multiple State Updates
+---------------------------------
+
+As we saw in an earlier example, :ref:`setting state triggers renders`. In other words,
+changes to state only take effect in the next render, not in the current one. Further,
+changes to state are batched, calling a particular state setter 3 times won't trigger 3
+renders, it will only trigger 1. This means that multiple state assignments are batched
+- so long as the event handler is synchronous (i.e. the event handler is not an
+``async`` function), IDOM waits until all the code in an event handler has run before
+processing state and starting the next render:
+
+.. idom:: _examples/set_color_3_times
+
+Sometimes though, you need to update a state variable more than once before the next
+render. In these cases, instead of having updates batched, you instead want them to be
+applied incrementally. That is, the next update can be made to depend on the prior one.
+To accomplish this, instead of passing the next state value directly (e.g.
+``set_state(new_state)``), we may pass an **"updater function"** of the form
+``compute_new_state(old_state)`` to the state setter (e.g.
+``set_state(compute_new_state)``):
+
+.. idom:: _examples/set_state_function
+
+.. card::
+ :link: multiple-state-updates
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn how updates to a components state can be batched, or applied incrementally.
+
+
+Section 5: Dangers of Mutability
+--------------------------------
+
+.. card::
+ :link: dangers-of-mutability
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ ...
diff --git a/docs/source/adding-interactivity/multiple-state-updates.rst b/docs/source/adding-interactivity/multiple-state-updates.rst
new file mode 100644
index 000000000..d230a60d6
--- /dev/null
+++ b/docs/source/adding-interactivity/multiple-state-updates.rst
@@ -0,0 +1,109 @@
+Multiple State Updates
+======================
+
+Setting a state variable will queue another render. But sometimes you might want to
+perform multiple operations on the value before queueing the next render. To do this, it
+helps to understand how React batches state updates.
+
+
+Batched Updates
+---------------
+
+As we learned :ref:`previously `, state variables remain fixed
+inside each render as if state were a snapshot taken at the begining of each render.
+This is why, in the example below, even though it might seem like clicking the
+"Increment" button would cause the ``number`` to increase by ``3``, it only does by
+``1``:
+
+.. idom:: _examples/set_counter_3_times
+
+The reason this happens is because, so long as the event handler is synchronous (i.e.
+the event handler is not an ``async`` function), IDOM waits until all the code in an
+event handler has run before processing state and starting the next render. Thus, it's
+the last call to a given state setter that matters. In the example below, even though we
+set the color of the button to ``"orange"`` and then ``"pink"`` before ``"blue"``,
+the color does not quickly flash orange and pink before blue - it alway remains blue:
+
+.. idom:: _examples/set_color_3_times
+
+This behavior let's you make multiple state changes without triggering unnecessary
+renders or renders with inconsistent state where only some of the variables have been
+updated. With that said, it also means that the UI won't change until after synchronous
+handlers have finished running.
+
+.. note::
+
+ For asynchronous event handlers, IDOM will not render until you ``await`` something.
+ As we saw in :ref:`prior examples `, if you introduce
+ an asynchronous delay to an event handler after changing state, renders may take
+ place before the remainder of the event handler completes. However, state variables
+ within handlers, even async ones, always remains static.
+
+This behavior of IDOM to "batch" state changes that take place inside a single event
+handler, do not extend across event handlers. In other words, distinct events will
+always produce distinct renders. For example, if clicking a button increments a counter
+by one, no matter how fast the user clicks, the view will never jump from 1 to 3 - it
+will always display 1, then 2, and then 3.
+
+
+Incremental Updates
+-------------------
+
+While it's uncommon, you need to update a state variable more than once before the next
+render. In these cases, instead of having updates batched, you instead want them to be
+applied incrementally. That is, the next update can be made to depend on the prior one.
+For example, what it we wanted to make it so that, in our ``Counter`` example :ref:`from
+before `, each call to ``set_number`` did in fact increment
+``number`` by one causing the view to display ``0``, then ``3``, then ``6``, and so on?
+
+To accomplish this, instead of passing the next state value as in ``set_number(number +
+1)``, we may pass an **"updater function"** to ``set_number`` that computes the next
+state based on the previous state. This would look like ``set_number(lambda number:
+number + 1)``. In other words we need a function of the form:
+
+.. code-block::
+
+ def compute_new_state(old_state):
+ ...
+ return new_state
+
+In our case, ``new_state = old_state + 1``. So we might define:
+
+.. code-block::
+
+ def increment(old_number):
+ new_number = old_number + 1
+ return new_number
+
+Which we can use to replace ``set_number(number + 1)`` with ``set_number(increment)``:
+
+.. idom:: _examples/set_state_function
+
+The way to think about how IDOM runs though this series of ``set_state(increment)``
+calls is to imagine that each one updates the internally managed state with its return
+value, then that return value is being passed to the next updater function. Ultimately,
+this is functionally equivalent to the following:
+
+.. code-block::
+
+ set_number(increment(increment(increment(number))))
+
+So why might you want to do this? Why not just compute ``set_number(number + 3)`` from
+the start? The easiest way to explain the use case is with an example. Imagine that we
+introduced a delay before ``set_number(number + 1)``. What would happen if we clicked
+the "Increment" button more than once before the delay in the first triggered event
+completed?
+
+.. idom:: _examples/delay_before_set_count
+
+From an :ref:`earlier lesson `, we learned that introducing
+delays do not change the fact that state variables do not change until the next render.
+As a result, despite clicking many times before the delay completes, the ``number`` only
+increments by one. To solve this we can use updater functions:
+
+.. idom:: _examples/delay_before_count_updater
+
+Now when you click the "Increment" button, each click, though delayed, corresponds to
+``number`` being increased. This is because the ``old_number`` in the updater function
+uses the value which was assigned by the last call to ``set_number`` rather than relying
+in the static ``number`` state variable.
diff --git a/docs/source/adding-interactivity/responding-to-events.rst b/docs/source/adding-interactivity/responding-to-events.rst
new file mode 100644
index 000000000..3b3033bf6
--- /dev/null
+++ b/docs/source/adding-interactivity/responding-to-events.rst
@@ -0,0 +1,144 @@
+Responding to Events
+====================
+
+IDOM lets you add event handlers to your parts of the interface. These events handlers
+are functions which can be assigned to a part of a UI such that, when a user iteracts
+with the interface, those functions get triggered. Examples of interaction include
+clicking, hovering, of focusing on form inputs, and more.
+
+
+Adding Event Handlers
+---------------------
+
+To start out we'll just display a button that, for the moment, doesn't do anything:
+
+.. idom:: _examples/button_does_nothing
+
+To add an event handler to this button we'll do three things:
+
+1. Declare a function called ``handle_event(event)`` inside the body of our ``Button`` component
+2. Add logic to ``handle_event`` that will print the ``event`` it receives to the console.
+3. Add an ``"onClick": handle_event`` attribute to the ```` element.
+
+.. idom:: _examples/button_prints_event
+
+.. note::
+
+ Normally print statements will only be displayed in the terminal where you launched
+ IDOM.
+
+It may feel weird to define a function within a function like this, but doing so allows
+the ``handle_event`` function to access information from within the scope of the
+component. That's important if you want to use any arguments that may have beend passed
+your component in the handler:
+
+.. idom:: _examples/button_prints_message
+
+With all that said, since our ``handle_event`` function isn't doing that much work, if
+we wanted to streamline our component definition, we could pass in our event handler as a
+lambda:
+
+.. code-block::
+
+ html.button({"onClick": lambda event: print(message_text)}, "Click me!")
+
+
+Supported Event Types
+---------------------
+
+Since IDOM's event information comes from React, most the the information (:ref:`with
+some exceptions `) about how React handles events translates
+directly to IDOM. Follow the links below to learn about each category of event:
+
+- :ref:`Clipboard Events`
+- :ref:`Composition Events`
+- :ref:`Keyboard Events`
+- :ref:`Focus Events`
+- :ref:`Form Events`
+- :ref:`Generic Events`
+- :ref:`Mouse Events`
+- :ref:`Pointer Events`
+- :ref:`Selection Events`
+- :ref:`Touch Events`
+- :ref:`UI Events`
+- :ref:`Wheel Events`
+- :ref:`Media Events`
+- :ref:`Image Events`
+- :ref:`Animation Events`
+- :ref:`Transition Events`
+- :ref:`Other Events`
+
+
+Passing Handlers to Components
+------------------------------
+
+A common pattern when factoring out common logic is to pass event handlers into a more
+generic component definition. This allows the component to focus on the things which are
+common while still giving its usages customizablity. Consider the case below where we
+want to create a generic ``Button`` component that can be used for a variety of purpose:
+
+.. idom:: _examples/button_handler_as_arg
+
+
+.. _Async Event Handler:
+
+Async Event Handlers
+--------------------
+
+Sometimes event handlers need to execute asynchronous tasks when they are triggered.
+Behind the scenes, IDOM is running an :mod:`asyncio` event loop for just this purpose.
+By defining your event handler as an asynchronous function instead of a normal
+synchronous one. In the layout below we sleep for several seconds before printing out a
+message in the first button. However, because the event handler is asynchronous, the
+handler for the second button is still able to respond:
+
+.. idom:: _examples/button_async_handlers
+
+
+Event Data Serialization
+------------------------
+
+Not all event data is serialized. The most notable example of this is the lack of a
+``target`` key in the dictionary sent back to the handler. Instead, data which is not
+inherhently JSON serializable must be treated on a case-by-case basis. A simple case
+to demonstrate this is the ``currentTime`` attribute of ``audio`` and ``video``
+elements. Normally this would be accessible via ``event.target.currentTime``, but here
+it's simply passed in under the key ``currentTime``:
+
+.. idom:: _examples/audio_player
+
+
+Client-side Event Behavior
+--------------------------
+
+Because IDOM operates server-side, there are inevitable limitations that prevent it from
+achieving perfect parity with all the behaviors of React. With that said, any feature
+that cannot be achieved in Python with IDOM, can be done by creating
+:ref:`Custom Javascript Components`.
+
+
+Preventing Default Event Actions
+................................
+
+Instead of calling an ``event.preventDefault()`` method as you would do in React, you
+must declare whether to prevent default behavior ahead of time. This can be accomplished
+using the :func:`~idom.core.events.event` decorator and setting ``prevent_default``. For
+example, we can stop a link from going to the specified URL:
+
+.. idom:: _examples/prevent_default_event_actions
+
+Unfortunately this means you cannot conditionally prevent default behavior in response
+to event data without writing :ref:`Custom Javascript Components`.
+
+
+Stop Event Propogation
+......................
+
+Similarly to :ref:`preventing default behavior `, you
+can use the :func:`~idom.core.events.event` decorator to prevent events originating in a
+child element from propagating to parent elements by setting ``stop_propagation``. In
+the example below we place a red ``div`` inside a parent blue ``div``. When propogation
+is turned on, clicking the red element will cause the handler for the outer blue one to
+trigger. Conversely, when it's off, only the handler for the red element will trigger.
+
+.. idom:: _examples/stop_event_propagation
diff --git a/docs/source/adding-interactivity/state-as-a-snapshot.rst b/docs/source/adding-interactivity/state-as-a-snapshot.rst
new file mode 100644
index 000000000..4d3c6c0e9
--- /dev/null
+++ b/docs/source/adding-interactivity/state-as-a-snapshot.rst
@@ -0,0 +1,158 @@
+State as a Snapshot
+===================
+
+When you watch the user interfaces you build change as you interact with them, it's easy
+to imagining that they do so because there's some bit of code that modifies the relevant
+parts of the view directly. For example, you may think that when a user clicks a "Send"
+button, there's code which reaches into the view and adds some text saying "Message
+sent!":
+
+.. image:: _static/direct-state-change.png
+
+IDOM works a bit differently though - user interactions cause event handlers to
+:ref:`"set state" ` triggering IDOM to re-render a new
+version of the view rather then mutating the existing one.
+
+.. image:: _static/idom-state-change.png
+
+Given this, when IDOM "renders" something, it's as if IDOM has taken a snapshot of the
+UI where all the event handlers, local variables and the view itself were calculated
+using what state was present at the time of that render. Then, when user iteractions
+trigger state setters, IDOM is made away of the newly set state and schedules a
+re-render. When this subsequent renders occurs it performs all the same calculations as
+before, but with this new state.
+
+As we've :ref:`already seen `, state variables are not
+like normal variables. Instead, they live outside your components and are managed by
+IDOM. When a component is rendered, IDOM provides the component a snapshot of the state
+in that exact moment. As a result, the view returned by that component is itself a
+snapshot of the UI at that time.
+
+
+Setting State Triggers Renders
+------------------------------
+
+Setting state does not impact the current render, instead it schedules a re-render. It's
+only in this subsequent render that changes to state take effect. As a result, setting
+state more than once in the context of the same render will not cause those changes to
+compound. This makes it easier to reason about how your UI will react to user
+interactions because state does not change until the next render.
+
+Let's experiment with this behaviors of state to see why we should think about it with
+respect to these "snapshots" in time. Take a look at the example below and try to guess
+how it will behave. **What will the count be after you click the "Increment" button?**
+
+.. idom:: _examples/set_counter_3_times
+
+Despite the fact that we called ``set_count(count + 1)`` three times, the count only
+increments by ``1``! This is perhaps a surprising result, but let's break what's
+happening inside the event handler to see why this is happening:
+
+.. code-block::
+
+ set_count(count + 1)
+ set_count(count + 1)
+ set_count(count + 1)
+
+On the initial render of your ``Counter`` the ``number`` variable is ``0``. Because we
+know that state variables do not change until the next render we ought to be able to
+substitute ``number`` with ``0`` everywhere it's referenced within the component until
+then. That includes the event handler too we should be able to rewrite the three lines
+above as:
+
+.. code-block::
+
+ set_count(0 + 1)
+ set_count(0 + 1)
+ set_count(0 + 1)
+
+Even though, we called ``set_count`` three times with what might have seemed like
+different values, every time we were actually just doing ``set_count(1)`` on each call.
+Only after the event handler returns will IDOM actually perform the next render where
+count is ``1``. When it does, ``number`` will be ``1`` and we'll be able to perform the
+same subtitution as before to see what the next number will be after we click
+"Increment":
+
+.. code-block::
+
+ set_count(1 + 1)
+ set_count(1 + 1)
+ set_count(1 + 1)
+
+
+State And Delayed Reactions
+---------------------------
+
+Given what we :ref:`learned above `, we ought to be able
+to reason about what should happen in the example below. What will be printed when the
+"Increment" button is clicked?
+
+.. idom:: _examples/print_count_after_set
+
+If we use the same subtitution trick we saw before, we can rewrite these lines:
+
+.. code-block::
+
+ set_number(number + 5)
+ print(number)
+
+Using the value of ``number`` in the initial render which is ``0``:
+
+.. code-block::
+
+ set_number(0 + 5)
+ print(0)
+
+Thus when we click the button we should expect that the next render will show ``5``, but
+we will ``print`` the number ``0`` instead. The next time we click the view will show
+``10`` and the printout will be ``5``. In this sense the print statement, because it
+lives within the prior snapshot, trails what is displayed in the next render.
+
+What if we slightly modify this example, by introducing a delay between when we call
+``set_number`` and when we print? Will this behavior remain the same? To add this delay
+we'll use an :ref:`async event handler` and :func:`~asyncio.sleep` for some time:
+
+.. idom:: _examples/delayed_print_after_set
+
+Even though the render completed before the print statement took place, the behavior
+remained the same! Despite the fact that the next render took place before the print
+statement did, the print statement still relies on the state snapshot from the initial
+render. Thus we can continue to use our substitution trick to analyze what's happening:
+
+.. code-block::
+
+ set_number(0 + 5)
+ print("about to print...")
+ await asyncio.sleep(3)
+ print(0)
+
+This property of state, that it remains static within the context of particular render,
+while unintuitive at first, is actually an important tool for preventing subtle bugs.
+Let's consider the example below where there's a form that sends a message with a 5
+second delay. Imagine a scenario where the user:
+
+1. Presses the "Send" button with the message "Hello" where "Alice" is the recipient.
+2. Then, before the five-second delay ends, the user changes the "To" field to "Bob".
+
+The first question to ask is "What should happen?" In this case, the user's expectation
+is that after they press "Send", changing the recipient, even if the message has not
+been sent yet, should not impact where the message is ultimately sent. We then need to
+ask what actually happens. Will it print “You said Hello to Alice” or “You said Hello to
+Bob”?
+
+.. idom:: _examples/print_chat_message
+
+As it turns out, the code above matches the user's expectation. This is because IDOM
+keeps the state values fixed within the event handlers defined during a particular
+render. As a result, you don't need to worry about whether state has changed while
+code in an event handler is running.
+
+.. card::
+ :link: multiple-state-updates
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ What if you wanted to read the latest state values before the next render? You’ll
+ want to use a state updater function, covered on the next page!
diff --git a/docs/source/architectural-patterns.rst b/docs/source/architectural-patterns.rst
deleted file mode 100644
index d025a15cf..000000000
--- a/docs/source/architectural-patterns.rst
+++ /dev/null
@@ -1,208 +0,0 @@
-Architectural Patterns
-======================
-
-Over the `past 5 years `__ front-end developers seem to have concluded that
-programs written with a declarative_ style or framework tend to be easier to understand
-and maintain than those done imperatively. Put more simply, mutable state in programs
-can quickly lead to unsustainable complexity. This trend is largely evidenced by the
-`rise `_ of Javascript frameworks like Vue_ and React_
-which describe the logic of computations without explicitly stating their control flow.
-
-.. _React: https://reactjs.org
-.. _NPM-trends: https://www.npmtrends.com/react-vs-angular-vs-vue
-.. _Vue: https://vuejs.org
-.. _Declarative: https://www.youtube.com/watch?v=yGh0bjzj4IQ
-.. _Frontend-Frameworks-Popularity: https://gist.github.com/tkrotoff/b1caa4c3a185629299ec234d2314e190
-
-.. image:: _static/npm-download-trends.png
-
-So what does this have to do with Python and IDOM? Well, because browsers are the de
-facto "operating system of the internet", even back-end languages like Python have had
-to figure out clever ways to integrate with them. While standard REST_ APIs are well
-suited to applications built using HTML templates, modern browser users expect a higher
-degree of interactivity than this alone can achieve.
-
-.. _REST: https://en.wikipedia.org/wiki/Representational_state_transfer
-
-A variety of Python packages have since been created to help solve this problem:
-
-- IPyWidgets_ - Adds interactive widgets to `Jupyter Notebooks`_
-- Dash_ - Allows data scientists to produces enterprise-ready analytic apps
-- Streamlit_ - Turns simple Python scripts into interactive dashboards
-- Bokeh_ - An interactive visualization library for modern web browsers
-
-.. _IPyWidgets: https://github.com/jupyter-widgets/ipywidgets
-.. _Jupyter Notebooks: https://jupyter.org/
-.. _Dash: https://plotly.com/dash/
-.. _Streamlit: https://www.streamlit.io/
-.. _Bokeh: https://docs.bokeh.org/
-
-However they each have drawbacks that can make them difficult to use.
-
-3. **Restrictive ecosystems** - UI components developed for one framework cannot be
- easily ported to any of the others because their APIs are either too complex,
- undocumented, or are structurally inaccesible.
-
-1. **Imperative paradigm** - IPyWidgets and Bokeh have not embraced the same declarative
- design principles pioneered by front-end developers. Streamlit and Dash on the
- otherhand, are declarative, but fall short of the features provided by React or Vue.
-
-1. **Limited layouts** - At their initial inception, the developers of these libraries
- were driven by the visualization needs of data scientists so the ability to create
- complex UI layouts may not have been a primary engineering goal.
-
-As a result, IDOM was developed to help solve these problems.
-
-
-Ecosystem Independence
-----------------------
-
-IDOM has a flexible set of :ref:`Core Abstractions` that allow it to interface with its
-peers. At the time of writing Jupyter, Dash, and Bokeh (via Panel) are supported, while
-Streamlit is in the works:
-
-- idom-jupyter_ (try it now with Binder_)
-- idom-dash_
-- `IDOM in Panel`_
-
-.. _Panel: https://panel.holoviz.org/Comparisons.html#comparing-panel-and-bokeh
-.. _idom-jupyter: https://github.com/idom-team/idom-jupyter
-.. _Binder: https://mybinder.org/v2/gh/idom-team/idom-jupyter/main?filepath=notebooks%2Fintroduction.ipynb
-.. _idom-dash: https://github.com/idom-team/idom-dash
-.. _IDOM in Panel: https://panel.holoviz.org/reference/panes/IDOM.html#panes-gallery-idom
-
-By providing well defined interfaces and straighforward protocols, IDOM makes it easy to
-swap out any part of the stack with an alternate implementation if you want to. For
-example, if you need a different web server for your application, IDOM already has
-several options to choose from or, use as blueprints to create your own:
-
-- :ref:`Sanic `
-- :ref:`FastAPI `
-- :ref:`Tornado `
-- :ref:`Flask `
-
-You can even target your usage of IDOM in your production-grade applications with IDOM's
-Javascript `React client library `_. Just install it in your
-front-end app and connect to a back-end websocket that's serving up IDOM models. This
-documentation acts as a prime example for this targeted usage - most of the page is
-static HTML, but embedded in it are :ref:`interactive examples ` that feature
-live views being served from a web socket:
-
-.. _idom-client-react: https://github.com/idom-team/idom/tree/main/src/idom/client/packages/idom-client-react
-
-.. image:: _static/live-examples-in-docs.gif
-
-
-Declarative Components
-----------------------
-
-IDOM, by adopting the :ref:`Hook ` design pattern from React_,
-inherits many of its aesthetic and functional characteristics. For those unfamiliar with
-hooks, user interfaces are composed of basic HTML elements that are constructed and
-returned by special functions called "components". Then, through the magic of hooks,
-those component functions can be made to have state. Consider the component below which
-displays a basic representation of an AND-gate:
-
-.. example:: simple_and_gate
-
-Note that the code never explicitely describes how to evolve the frontend view when
-events occur. Instead, it declares that, given a particular state, this is how the view
-should look. It's then IDOM's responsibility to figure out how to bring that declaration
-into being. This behavior of defining outcomes without stating the means by which to
-achieve them is what makes components in IDOM and React "declarative". For comparison, a
-hypothetical, and a more imperative approach to defining the same interface might look
-similar to the following:
-
-.. code-block::
-
- layout = Layout()
-
-
- def make_and_gate():
- state = {"input_1": False, "input_2": False}
- output_text = html.pre()
- update_output_text(output_text, state)
-
- def toggle_input(index):
- state[f"input_{index}"] = not state[f"input_{index}"]
- update_output_text(output_text, state)
-
- return html.div(
- html.input({"type": "checkbox", "onClick": lambda event: toggle_input(1)}),
- html.input({"type": "checkbox", "onClick": lambda event: toggle_input(2)}),
- output_text,
- )
-
-
- def update_output_text(text, state):
- text.update(
- children="{input_1} AND {input_2} = {output}".format(
- input_1=state["input_1"],
- input_2=state["input_2"],
- output=state["input_1"] and state["input_2"],
- )
- )
-
-
- layout.add_element(make_and_gate())
- layout.run()
-
-In this imperative incarnation there are several disadvantages:
-
-1. **Refactoring is difficult** - Functions are much more specialized to their
- particular usages in ``make_and_gate`` and thus cannot be easily generalized. By
- comparison, ``use_toggle`` from the declarative implementation could be applicable to
- any scenario where boolean indicators are toggled on and off.
-
-2. **No clear static relations** - There is no one section of code through which to
- discern the basic structure and behaviors of the view. This issue is exemplified by
- the fact that we must call ``update_output_text`` from two different locations. Once
- in the body of ``make_and_gate`` and again in the body of the callback
- ``toggle_input``. This means that, to understand what the ``output_text`` might
- contain, we must also understand all the business logic that surrounds it.
-
-3. **Referential links cause complexity** - To evolve the view, various callbacks must
- hold references to all the elements that they will update. At the outset this makes
- writing programs difficult since elements must be passed up and down the call stack
- wherever they are needed. Considered further though, it also means that a function
- layers down in the call stack can accidentally or intentionally impact the behavior
- of ostensibly unrelated parts of the program.
-
-
-Communication Scheme
---------------------
-
-To communicate between its back-end Python server and Javascript client, IDOM uses
-something called a Virtual Document Object Model (:ref:`VDOM`) to
-construct a representation of the view. The VDOM is constructed on the Python side by
-components. Then, as it evolves, IDOM's layout computes VDOM-diffs and wires them to its
-Javascript client where it is ultimately displayed:
-
-.. image:: _static/idom-flow-diagram.svg
-
-By contrast, IDOM's peers take an approach that aligns fairly closely with the
-Model-View-Controller_ design pattern - the controller lives server-side (though not
-always), the model is what's synchronized between the server and client, and the view is
-run client-side in Javascript. To draw it out might look something like this:
-
-.. image:: _static/mvc-flow-diagram.svg
-
-.. _Model-View-Controller: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
-
-
-Javascript Integration
-----------------------
-
-If you're thinking critically about IDOM's use of a virtual DOM, you may have thought...
-
- Isn't wiring a virtual representation of the view to the client, even if its diffed,
- expensive?
-
-And yes, while the performance of IDOM is sufficient for most use cases, there are
-inevitably scenarios where this could be an issue. Thankfully though, just like its
-peers, IDOM makes it possible to seemlesly integrate :ref:`Javascript Components`. They
-can be :ref:`custom built ` for your use case, or you can
-just leverage the existing Javascript ecosystem
-:ref:`without any extra work `:
-
-.. example:: material_ui_switch
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d17bfc688..716da3aea 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -7,6 +7,7 @@
# http://www.sphinx-doc.org/en/master/config
import sys
+from doctest import DONT_ACCEPT_TRUE_FOR_1, ELLIPSIS, NORMALIZE_WHITESPACE
from pathlib import Path
@@ -30,10 +31,26 @@
# -- Common External Links ---------------------------------------------------
extlinks = {
- "issue": ("https://github.com/idom-team/idom/issues/%s", "#"),
- "pull": ("https://github.com/idom-team/idom/pull/%s", "#"),
- "discussion": ("https://github.com/idom-team/idom/discussions/%s", "#"),
- "commit": ("https://github.com/idom-team/idom/commit/%s", ""),
+ "issue": (
+ "https://github.com/idom-team/idom/issues/%s",
+ "#",
+ ),
+ "pull": (
+ "https://github.com/idom-team/idom/pull/%s",
+ "#",
+ ),
+ "discussion": (
+ "https://github.com/idom-team/idom/discussions/%s",
+ "#",
+ ),
+ "discussion-type": (
+ "https://github.com/idom-team/idom/discussions/categories/%s",
+ "",
+ ),
+ "commit": (
+ "https://github.com/idom-team/idom/commit/%s",
+ "",
+ ),
}
# -- General configuration ---------------------------------------------------
@@ -61,9 +78,9 @@
"async_doctest",
"autogen_api_docs",
"copy_vdom_json_schema",
- "interactive_widget",
+ "idom_view",
"patched_html_translator",
- "widget_example",
+ "idom_example",
"build_custom_js",
]
@@ -89,7 +106,9 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
-# exclude_patterns = []
+exclude_patterns = [
+ "_custom_js",
+]
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
@@ -100,8 +119,28 @@
# Controls how sphinx.ext.autodoc represents typehints in the function signature
autodoc_typehints = "description"
+# -- Doc Test Configuration -------------------------------------------------------
+
+doctest_default_flags = NORMALIZE_WHITESPACE | ELLIPSIS | DONT_ACCEPT_TRUE_FOR_1
+
# -- Extension Configuration ------------------------------------------------------
+# -- MyST Parser --
+
+myst_enable_extensions = [
+ # "amsmath",
+ "colon_fence",
+ # "deflist",
+ # "dollarmath",
+ # "html_admonition",
+ # "html_image",
+ # "linkify",
+ # "replacements",
+ # "smartquotes",
+ # "substitution",
+ # "tasklist",
+]
+
# -- sphinx_panel --
# Used to stop the extension from loading bootstrap twice since the `pydata_sphinx_theme`
@@ -121,18 +160,22 @@
# -- sphinx_reredirects --
redirects = {
- "package-api": "auto/api-reference.html",
- "configuration-options": "auto/api-reference.html#configuration-options",
+ "package-api": "_autogen/user-apis.html",
+ "configuration-options": "_autogen/dev-apis.html#configuration-options",
+ "examples": "creating-interfaces/index.html",
}
# -- Options for HTML output -------------------------------------------------
+# Set the page title
+html_title = "IDOM Docs"
+
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "furo"
-html_logo = "_static/idom-logo.svg"
-html_favicon = "_static/idom-logo-square-small.svg"
+html_logo = "_static/branding/idom-logo.svg"
+html_favicon = "_static/branding/idom-logo-square-small.svg"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -196,16 +239,14 @@
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (master_doc, "IDOM.tex", "IDOM Documentation", "Ryan Morshead", "manual")
-]
+latex_documents = [(master_doc, "IDOM.tex", html_title, "Ryan Morshead", "manual")]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
-man_pages = [(master_doc, "idom", "IDOM Documentation", [author], 1)]
+man_pages = [(master_doc, "idom", html_title, [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
@@ -217,7 +258,7 @@
(
master_doc,
"IDOM",
- "IDOM Documentation",
+ html_title,
author,
"IDOM",
"One line description of project.",
diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst
deleted file mode 100644
index 8c60e47b6..000000000
--- a/docs/source/contributing.rst
+++ /dev/null
@@ -1,49 +0,0 @@
-Contributing
-============
-
-.. note::
-
- The
- `Code of Conduct `__
- applies in all community spaces. If you are not familiar with our Code of Conduct
- policy, take a minute to read it before making your first contribution.
-
-The IDOM team welcomes contributions and contributors of all kinds - whether they come
-as code changes, participation in the discussions, opening issues and pointing out bugs,
-or simply sharing your work with your colleagues and friends. We're excited to see how
-you can help move this project and community forward!
-
-.. list-table::
- :header-rows: 1
-
- * - I'm Looking For...
- - Description
-
- * - `A discussion board `__
- - Ask questions, share ideas, or show projects
-
- * - `Where to report issues `__
- - Tell us about any problems you're having
-
- * - :ref:`How to contribute Code `
- - Make and review code changes
-
- * - `Other related projects `__
- - Find other projects created by the IDOM team
-
-
-Everyone Can Contribute!
-------------------------
-
-Trust us, there's so many ways to support the project. We're always looking for people
-who can:
-
-- Improve our documentation
-- Teach and tell others about IDOM
-- Share ideas for new features
-- Report bugs
-- Participate in general discussions
-
-Still aren't sure what you have to offer? Just
-`ask us `__ and we'll help you make your
-first contribution.
diff --git a/docs/source/core-abstractions.rst b/docs/source/core-abstractions.rst
deleted file mode 100644
index 999c3c792..000000000
--- a/docs/source/core-abstractions.rst
+++ /dev/null
@@ -1,253 +0,0 @@
-Core Abstractions
-=================
-
-
-Pure Components
----------------
-
-As in most programming paradigms, so many of the problems come down to how we manage
-state. The first tool in encouraging its proper curation is the usage of
-`pure functions`_. The benefit of a pure function is that there's no state. Similar to
-the adage "the best code is no code at all," we make the related claim that "the best
-way to manage state is to have no state at all."
-
-With IDOM the core of your application will be built on the back of basic functions and
-coroutines that return :ref:`VDOM` models and which do so without state
-and without `side effects`_. We call these kinds of model rendering functions
-:ref:`Pure Components`. For example, one might want a function which
-accepted a list of strings and turned it into a series of paragraph elements:
-
-.. code-block::
-
- def paragraphs(list_of_text):
- return idom.html.div([idom.html.p(text) for text in list_of_text])
-
-
-Stateful Components
--------------------
-
-A Stateful Component is one which uses a :ref:`Life Cycle Hooks`. These life cycle hooks
-allow you to add state to otherwise stateless functions. To create a stateful component
-you'll need to apply the :func:`~idom.core.component.component` decorator to a coroutine_
-whose body contains a hook usage. We'll demonstrate that with a simple
-:ref:`click counter`:
-
-.. testcode::
-
- import idom
-
-
- def use_counter():
- count, set_count = idom.hooks.use_state(0)
- return count, lambda: set_count(lambda old_count: old_count + 1)
-
-
- @idom.component
- def ClickCount():
- count, increment_count = use_counter()
- return idom.html.button(
- {"onClick": lambda event: increment_count()},
- [f"Click count: {count}"],
- )
-
-
-Component Layout
-----------------
-
-Displaying components requires you to turn them into :ref:`VDOM`. This
-transformation, known as "rendering a component", is done by a
-:class:`~idom.core.proto.LayoutType`. Layouts are responsible for rendering components
-and scheduling their re-renders when they change. IDOM's concrete
-:class:`~idom.core.layout.Layout` implementation renders
-:class:`~idom.core.component.Component` instances into
-:class:`~idom.core.layout.LayoutUpdate` and responds to
-:class:`~idom.core.layout.LayoutEvent` objects respectively.
-
-To create a layout, you'll need a :class:`~idom.core.component.Component` instance, that
-will become its root. This component won't ever be removed from the model. Then, you'll
-just need to call and await a :meth:`~idom.core.layout.Layout.render` which will return
-a :ref:`JSON Patch`:
-
-
-.. testcode::
-
- with idom.Layout(ClickCount()) as layout:
- update = await layout.render()
-
-The layout also handles the deliver of events to their handlers. Normally these are sent
-through a :ref:`Dispatcher ` first, but for now we'll do it manually.
-To accomplish this we need to pass a fake event with its "target" (event handler
-identifier), to the layout's :meth:`~idom.core.layout.Layout.deliver` method, after
-which we can re-render and see what changed:
-
-.. testcode::
-
- from idom.core.layout import LayoutEvent
- from idom.testing import StaticEventHandler
-
- static_handler = StaticEventHandler()
-
-
- @idom.component
- def ClickCount():
- count, increment_count = use_counter()
-
- # we do this in order to capture the event handler's target ID
- handler = static_handler.use(lambda event: increment_count())
-
- return idom.html.button({"onClick": handler}, [f"Click count: {count}"])
-
-
- with idom.Layout(ClickCount()) as layout:
- update_1 = await layout.render()
-
- fake_event = LayoutEvent(target=static_handler.target, data=[{}])
- await layout.deliver(fake_event)
-
- update_2 = await layout.render()
- assert update_2.new["children"][0] == "Click count: 1"
-
-.. note::
-
- Don't worry about the format of the layout event's ``target``. Its an internal
- detail of the layout's implementation that is neither necessary to understanding
- how things work, nor is it part of the interface clients should rely on.
-
-
-Layout Dispatcher
------------------
-
-A "dispatcher" implementation is a relatively thin layer of logic around a
-:class:`~idom.core.layout.Layout` which drives the triggering of events and updates by
-scheduling an asynchronous loop that will run forever - effectively animating the model.
-The simplest dispatcher is :func:`~idom.core.dispatcher.dispatch_single_view` which
-accepts three arguments. The first is a :class:`~idom.core.layout.Layout`, the second is
-a "send" callback to which the dispatcher passes updates, and the third is a "receive"
-callback that's called by the dispatcher to collect events it should execute.
-
-.. testcode::
-
- import asyncio
-
- from idom.core.layout import LayoutEvent
- from idom.core.dispatcher import dispatch_single_view
-
-
- sent_patches = []
-
- # We need this to simulate a scenario in which events arriving *after* each update
- # has been sent to the client. Otherwise the events would all arrive at once and we
- # would observe one large update rather than many discrete updates.
- semaphore = asyncio.Semaphore(0)
-
-
- async def send(patch):
- sent_patches.append(patch)
- semaphore.release()
- if len(sent_patches) == 5:
- # if we didn't cancel the dispatcher would continue forever
- raise asyncio.CancelledError()
-
-
- async def recv():
- await semaphore.acquire()
- event = LayoutEvent(target=static_handler.target, data=[{}])
- return event
-
-
- await dispatch_single_view(idom.Layout(ClickCount()), send, recv)
- assert len(sent_patches) == 5
-
-
-.. note::
-
- The :func:`~idom.core.dispatcher.create_shared_view_dispatcher`, while more complex
- in its usage, allows multiple clients to share one synchronized view.
-
-
-Layout Server
--------------
-
-The :ref:`Dispatcher ` allows you to animate the layout, but we still
-need to get the models on the screen. One of the last steps in that journey is to send
-them over the wire. To do that you need a :class:`~idom.server.proto.ServerFactory`
-implementation. Presently, IDOM comes with support for the following web servers:
-
-- :class:`sanic.app.Sanic` (``pip install idom[sanic]``)
-
- - :class:`idom.server.sanic.PerClientStateServer`
-
- - :class:`idom.server.sanic.SharedClientStateServer`
-
-- `fastapi.FastAPI `__ (``pip install idom[fastapi]``)
-
- - :class:`idom.server.fastapi.PerClientStateServer`
-
- - :class:`idom.server.fastapi.SharedClientStateServer`
-
-- :class:`flask.Flask` (``pip install idom[flask]``)
-
- - :class:`idom.server.flask.PerClientStateServer`
-
-- :class:`tornado.web.Application` (``pip install idom[tornado]``)
-
- - :class:`idom.server.tornado.PerClientStateServer`
-
-However, in principle, the base server class is capable of working with any other async
-enabled server framework. Potential candidates range from newer frameworks like
-`vibora `__ and `starlette `__ to
-`aiohttp `__.
-
-.. note::
-
- If using or implementing a bridge between IDOM and an async server not listed here
- interests you, post an `issue `__.
-
-The main thing to understand about server implementations is that they can function in
-two ways - as a standalone application or as an extension to an existing application.
-
-
-Standalone Server Usage
-.......................
-
-The implementation constructs a default application that's used to serve the dispatched
-models:
-
-.. code-block:: python
-
- import idom
- from idom.server.sanic import PerClientStateServer
-
- @idom.component
- def View(self):
- return idom.html.h1(["Hello World"])
-
- app = PerClientStateServer(View)
- app.run("localhost", 5000)
-
-
-Server Extension Usage
-......................
-
-The implementation registers hooks into the application to serve the model once run:
-
-.. code-block:: python
-
- import idom
- from idom.server.sanic import PerClientState
- from sanic import Sanic
-
- app = Sanic()
-
- @idom.component
- def View(self):
- return idom.html.h1(["Hello World"])
-
- per_client_state = PerClientStateServer(View, app=app)
-
- app.run("localhost", 5000)
-
-
-.. _pure functions: https://en.wikipedia.org/wiki/Pure_function
-.. _side effects: https://en.wikipedia.org/wiki/Side_effect_(computer_science)
-.. _coroutine: https://docs.python.org/3/glossary.html#term-coroutine
diff --git a/docs/source/creating-interfaces/_examples/bad_conditional_todo_list.py b/docs/source/creating-interfaces/_examples/bad_conditional_todo_list.py
new file mode 100644
index 000000000..a5de11f77
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/bad_conditional_todo_list.py
@@ -0,0 +1,24 @@
+from idom import component, html, run
+
+
+@component
+def Item(name, done):
+ if done:
+ return html.li(name, " ✔")
+ else:
+ return html.li(name)
+
+
+@component
+def TodoList():
+ return html.section(
+ html.h1("My Todo List"),
+ html.ul(
+ Item("Find a cool problem to solve", done=True),
+ Item("Build an app to solve it", done=True),
+ Item("Share that app with the world!", done=False),
+ ),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/_examples/good_conditional_todo_list.py b/docs/source/creating-interfaces/_examples/good_conditional_todo_list.py
new file mode 100644
index 000000000..64d6f6813
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/good_conditional_todo_list.py
@@ -0,0 +1,21 @@
+from idom import component, html, run
+
+
+@component
+def Item(name, done):
+ return html.li(name, "" if done else " ✔")
+
+
+@component
+def TodoList():
+ return html.section(
+ html.h1("My Todo List"),
+ html.ul(
+ Item("Find a cool problem to solve", done=True),
+ Item("Build an app to solve it", done=True),
+ Item("Share that app with the world!", done=False),
+ ),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/_examples/nested_photos.py b/docs/source/creating-interfaces/_examples/nested_photos.py
new file mode 100644
index 000000000..4c512b7e6
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/nested_photos.py
@@ -0,0 +1,25 @@
+from idom import component, html, run
+
+
+@component
+def Photo():
+ return html.img(
+ {
+ "src": "https://picsum.photos/id/274/500/300",
+ "style": {"width": "30%"},
+ "alt": "Ray Charles",
+ }
+ )
+
+
+@component
+def Gallery():
+ return html.section(
+ html.h1("Famous Musicians"),
+ Photo(),
+ Photo(),
+ Photo(),
+ )
+
+
+run(Gallery)
diff --git a/docs/source/creating-interfaces/_examples/parametrized_photos.py b/docs/source/creating-interfaces/_examples/parametrized_photos.py
new file mode 100644
index 000000000..aeea41589
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/parametrized_photos.py
@@ -0,0 +1,25 @@
+from idom import component, html, run
+
+
+@component
+def Photo(alt_text, image_id):
+ return html.img(
+ {
+ "src": f"https://picsum.photos/id/{image_id}/500/300",
+ "style": {"width": "50%"},
+ "alt": alt_text,
+ }
+ )
+
+
+@component
+def Gallery():
+ return html.section(
+ html.h1("Photo Gallery"),
+ Photo("Landscape", image_id=830),
+ Photo("City", image_id=274),
+ Photo("Puppy", image_id=237),
+ )
+
+
+run(Gallery)
diff --git a/docs/source/creating-interfaces/_examples/simple_photo.py b/docs/source/creating-interfaces/_examples/simple_photo.py
new file mode 100644
index 000000000..c6b92c652
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/simple_photo.py
@@ -0,0 +1,15 @@
+from idom import component, html, run
+
+
+@component
+def Photo():
+ return html.img(
+ {
+ "src": "https://picsum.photos/id/237/500/300",
+ "style": {"width": "50%"},
+ "alt": "Puppy",
+ }
+ )
+
+
+run(Photo)
diff --git a/docs/source/creating-interfaces/_examples/sorted_and_filtered_todo_list.py b/docs/source/creating-interfaces/_examples/sorted_and_filtered_todo_list.py
new file mode 100644
index 000000000..28708416e
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/sorted_and_filtered_todo_list.py
@@ -0,0 +1,32 @@
+from idom import component, html, run
+
+
+@component
+def DataList(items, filter_by_priority=None, sort_by_priority=False):
+ if filter_by_priority is not None:
+ items = [i for i in items if i["priority"] <= filter_by_priority]
+ if sort_by_priority:
+ items = list(sorted(items, key=lambda i: i["priority"]))
+ list_item_elements = [html.li(i["text"]) for i in items]
+ return html.ul(list_item_elements)
+
+
+@component
+def TodoList():
+ tasks = [
+ {"text": "Make breakfast", "priority": 0},
+ {"text": "Feed the dog", "priority": 0},
+ {"text": "Do laundry", "priority": 2},
+ {"text": "Go on a run", "priority": 1},
+ {"text": "Clean the house", "priority": 2},
+ {"text": "Go to the grocery store", "priority": 2},
+ {"text": "Do some coding", "priority": 1},
+ {"text": "Read a book", "priority": 1},
+ ]
+ return html.section(
+ html.h1("My Todo List"),
+ DataList(tasks, filter_by_priority=1, sort_by_priority=True),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/_examples/todo_from_list.py b/docs/source/creating-interfaces/_examples/todo_from_list.py
new file mode 100644
index 000000000..474c78770
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/todo_from_list.py
@@ -0,0 +1,28 @@
+from idom import component, html, run
+
+
+@component
+def DataList(items):
+ list_item_elements = [html.li(text) for text in items]
+ return html.ul(list_item_elements)
+
+
+@component
+def TodoList():
+ tasks = [
+ "Make breakfast (important)",
+ "Feed the dog (important)",
+ "Do laundry",
+ "Go on a run (important)",
+ "Clean the house",
+ "Go to the grocery store",
+ "Do some coding",
+ "Read a book (important)",
+ ]
+ return html.section(
+ html.h1("My Todo List"),
+ DataList(tasks),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/_examples/todo_list.py b/docs/source/creating-interfaces/_examples/todo_list.py
new file mode 100644
index 000000000..4f0053c4b
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/todo_list.py
@@ -0,0 +1,21 @@
+from idom import component, html, run
+
+
+@component
+def Item(name, done):
+ return html.li(name)
+
+
+@component
+def TodoList():
+ return html.section(
+ html.h1("My Todo List"),
+ html.ul(
+ Item("Find a cool problem to solve", done=True),
+ Item("Build an app to solve it", done=True),
+ Item("Share that app with the world!", done=False),
+ ),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/_examples/todo_list_with_keys.py b/docs/source/creating-interfaces/_examples/todo_list_with_keys.py
new file mode 100644
index 000000000..5a173e1e0
--- /dev/null
+++ b/docs/source/creating-interfaces/_examples/todo_list_with_keys.py
@@ -0,0 +1,32 @@
+from idom import component, html, run
+
+
+@component
+def DataList(items, filter_by_priority=None, sort_by_priority=False):
+ if filter_by_priority is not None:
+ items = [i for i in items if i["priority"] <= filter_by_priority]
+ if sort_by_priority:
+ items = list(sorted(items, key=lambda i: i["priority"]))
+ list_item_elements = [html.li(i["text"], key=i["id"]) for i in items]
+ return html.ul(list_item_elements)
+
+
+@component
+def TodoList():
+ tasks = [
+ {"id": 0, "text": "Make breakfast", "priority": 0},
+ {"id": 1, "text": "Feed the dog", "priority": 0},
+ {"id": 2, "text": "Do laundry", "priority": 2},
+ {"id": 3, "text": "Go on a run", "priority": 1},
+ {"id": 4, "text": "Clean the house", "priority": 2},
+ {"id": 5, "text": "Go to the grocery store", "priority": 2},
+ {"id": 6, "text": "Do some coding", "priority": 1},
+ {"id": 7, "text": "Read a book", "priority": 1},
+ ]
+ return html.section(
+ html.h1("My Todo List"),
+ DataList(tasks, filter_by_priority=1, sort_by_priority=True),
+ )
+
+
+run(TodoList)
diff --git a/docs/source/creating-interfaces/html-with-idom.rst b/docs/source/creating-interfaces/html-with-idom.rst
new file mode 100644
index 000000000..96c5c9e75
--- /dev/null
+++ b/docs/source/creating-interfaces/html-with-idom.rst
@@ -0,0 +1,121 @@
+HTML With IDOM
+==============
+
+In a typical Python-base web application the resonsibility of defining the view along
+with its backing data and logic are distributed between a client and server
+respectively. With IDOM, both these tasks are centralized in a single place. This is
+done by allowing HTML interfaces to be constructed in Python. Take a look at the two
+code examples below. The one on the left shows how to make a basic title and todo list
+using standard HTML, the one of the right uses IDOM in Python, and below is a view of
+what the HTML would look like if displayed:
+
+.. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item::
+ :columns: 6
+
+ .. code-block:: html
+
+ My Todo List
+
+ Build a cool new app
+ Share it with the world!
+
+
+ .. grid-item::
+ :columns: 6
+
+ .. testcode::
+
+ from idom import html
+
+ html.h1("My Todo List")
+ html.ul(
+ html.li("Build a cool new app"),
+ html.li("Share it with the world!"),
+ )
+
+ .. grid-item-card::
+ :columns: 12
+
+ .. raw:: html
+
+
+
My Todo List
+
+ Build a cool new app
+ Share it with the world!
+
+
+
+What this shows is that you can recreate the same HTML layouts with IDOM using functions
+from the :mod:`idom.html` module. These function share the same names as their
+corresponding HTML tags. For example, the `` `` element above has a similarly named
+:func:`~idom.html.h1` function. With that said, while the code above looks similar, it's
+not very useful because we haven't captured the results from these function calls in a
+variable. To do this we need to wrap up the layout above into a single
+:func:`~idom.html.div` and assign it to a variable:
+
+.. testcode::
+
+ layout = html.div(
+ html.h1("My Todo List"),
+ html.ul(
+ html.li("Build a cool new app"),
+ html.li("Share it with the world!"),
+ ),
+ )
+
+
+Adding HTML Attributes
+----------------------
+
+That's all well and good, but there's more to HTML than just text. What if we wanted to
+display an image? In HTMl we'd use the ` ` element and add attributes to it order
+to specify a URL to its ``src`` and use some ``style`` to modify and position it:
+
+.. code-block:: html
+
+
+
+In IDOM we add these attributes to elements using dictionaries. There are some notable
+differences though. The biggest being the fact that all names in IDOM use ``camelCase``
+instead of dash-sepearted words. For example, ``margin-left`` becomes ``marginLeft``.
+Additionally, instead of specifying ``style`` using a string, we use a dictionary:
+
+.. testcode::
+
+ html.img(
+ {
+ "src": "https://picsum.photos/id/237/500/300",
+ "style": {"width": "50%", "marginLeft": "25%"},
+ "alt": "Billie Holiday",
+ }
+ )
+
+.. raw:: html
+
+
+
+
+----------
+
+
+.. card::
+ :link: /understanding-idom/representing-html
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Dive into the data structures IDOM uses to represent HTML
diff --git a/docs/source/creating-interfaces/index.rst b/docs/source/creating-interfaces/index.rst
new file mode 100644
index 000000000..a47d5e49d
--- /dev/null
+++ b/docs/source/creating-interfaces/index.rst
@@ -0,0 +1,128 @@
+Creating Interfaces
+===================
+
+.. toctree::
+ :hidden:
+
+ html-with-idom
+ your-first-components
+ rendering-data
+
+.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
+ :color: info
+ :animate: fade-in
+ :open:
+
+ .. grid:: 2
+
+ .. grid-item-card:: :octicon:`code-square` HTML with IDOM
+ :link: html-with-idom
+ :link-type: doc
+
+ Construct HTML layouts from the basic units of user interface functionality.
+
+ .. grid-item-card:: :octicon:`package` Your First Components
+ :link: your-first-components
+ :link-type: doc
+
+ Define reusable building blocks that it easier to construct complex
+ interfaces.
+
+ .. grid-item-card:: :octicon:`database` Rendering Data
+ :link: rendering-data
+ :link-type: doc
+
+ Use data to organize and render HTML elements and components.
+
+IDOM is a Python package for making user interfaces (UI). These interfaces are built
+from small elements of functionality like buttons text and images. IDOM allows you to
+combine these elements into reusable, nestable :ref:`"components" `. In the sections that follow you'll learn how these UI elements are created
+and organized into components. Then, you'll use components to customize and
+conditionally display more complex UIs.
+
+
+Section 1: HTML with IDOM
+-------------------------
+
+In a typical Python-base web application the resonsibility of defining the view along
+with its backing data and logic are distributed between a client and server
+respectively. With IDOM, both these tasks are centralized in a single place. The most
+foundational pilar of this capability is formed by allowing HTML interfaces to be
+constructed in Python. Let's consider the HTML sample below:
+
+.. code-block:: html
+
+ My Todo List
+
+ Build a cool new app
+ Share it with the world!
+
+
+To recreate the same thing in IDOM you would write:
+
+.. code-block::
+
+ from idom import html
+
+ html.div(
+ html.h1("My Todo List"),
+ html.ul(
+ html.li("Design a cool new app"),
+ html.li("Build it"),
+ html.li("Share it with the world!"),
+ )
+ )
+
+.. card::
+ :link: html-with-idom
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Construct HTML layouts from the basic units of user interface functionality.
+
+
+Section 2: Your First Components
+--------------------------------
+
+The next building block in our journey with IDOM are components. At their core,
+components are just a normal Python functions that return :ref:`HTML `.
+The one special thing about them that we'll concern ourselves with now, is that to
+create them we need to add an ``@component`` `decorator
+ `__. To see what this looks like in
+practice we'll quickly make a ``Photo`` component:
+
+.. idom:: _examples/simple_photo
+
+.. card::
+ :link: your-first-components
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Define reusable building blocks that it easier to construct complex interfaces.
+
+
+Section 3: Rendering Data
+-------------------------
+
+The last pillar of knowledge you need before you can start making :ref:`interactive
+interfaces ` is the ability to render sections of the UI given a
+collection of data. This will require you to understand how elements which are derived
+from data in this way must be orgnized with :ref:`"keys" `.
+One case where we might want to do this is if items in a todo list come from a list of
+data that we want to sort and filter:
+
+.. idom:: _examples/todo_list_with_keys
+
+.. card::
+ :link: rendering-data
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Use data to organize and render HTML elements and components.
diff --git a/docs/source/creating-interfaces/rendering-data.rst b/docs/source/creating-interfaces/rendering-data.rst
new file mode 100644
index 000000000..9d6824f4e
--- /dev/null
+++ b/docs/source/creating-interfaces/rendering-data.rst
@@ -0,0 +1,297 @@
+Rendering Data
+==============
+
+Frequently you need to construct a number of similar components from a collection of
+data. Let's imagine that we want to create a todo list that can be ordered and filtered
+on the priority of each item in the list. To start, we'll take a look at the kind of
+view we'd like to display:
+
+.. code-block:: html
+
+
+ Make breakfast (important)
+ Feed the dog (important)
+ Do laundry
+ Go on a run (important)
+ Clean the house
+ Go to the grocery store
+ Do some coding
+ Read a book (important)
+
+
+Based on this, our next step in achieving our goal is to break this view down into the
+underlying data that we'd want to use to represent it. The most straightforward way to
+do this would be to just put the text of each ```` into a list:
+
+.. testcode::
+
+ tasks = [
+ "Make breakfast (important)",
+ "Feed the dog (important)",
+ "Do laundry",
+ "Go on a run (important)",
+ "Clean the house",
+ "Go to the grocery store",
+ "Do some coding",
+ "Read a book (important)",
+ ]
+
+We could then take this list and "render" it into a series of `` `` elements:
+
+.. testcode::
+
+ from idom import html
+
+ list_item_elements = [html.li(text) for text in tasks]
+
+This list of elements can then be passed into a parent ```` element:
+
+.. testcode::
+
+ list_element = html.ul(list_item_elements)
+
+The last thing we have to do is return this from a component:
+
+.. idom:: _examples/todo_from_list
+
+
+Filtering and Sorting Elements
+------------------------------
+
+Our representation of ``tasks`` worked fine to just get them on the screen, but it
+doesn't extend well to the case where we want to filter and order them based on
+priority. Thus, we need to change the data structure we're using to represent our tasks:
+
+.. testcode::
+
+ tasks = [
+ {"text": "Make breakfast", "priority": 0},
+ {"text": "Feed the dog", "priority": 0},
+ {"text": "Do laundry", "priority": 2},
+ {"text": "Go on a run", "priority": 1},
+ {"text": "Clean the house", "priority": 2},
+ {"text": "Go to the grocery store", "priority": 2},
+ {"text": "Do some coding", "priority": 1},
+ {"text": "Read a book", "priority": 1},
+ ]
+
+With this we can now imaging writing some filtering and sorting logic using Python's
+:func:`filter` and :func:`sorted` functions respecitvely. We'll do this by only
+displaying items whose ``priority`` is less than or equal to some ``filter_by_priority``
+and then ordering the elements based on the ``priority``:
+
+.. testcode::
+
+ x = 1
+
+.. testcode::
+
+ filter_by_priority = 1
+ sort_by_priority = True
+
+ filtered_tasks = tasks
+ if filter_by_priority is not None:
+ filtered_tasks = [t for t in filtered_tasks if t["priority"] <= filter_by_priority]
+ if sort_by_priority:
+ filtered_tasks = list(sorted(filtered_tasks, key=lambda t: t["priority"]))
+
+ assert filtered_tasks == [
+ {'text': 'Make breakfast', 'priority': 0},
+ {'text': 'Feed the dog', 'priority': 0},
+ {'text': 'Go on a run', 'priority': 1},
+ {'text': 'Do some coding', 'priority': 1},
+ {'text': 'Read a book', 'priority': 1},
+ ]
+
+We could then add this code to our ``DataList`` component:
+
+.. idom:: _examples/sorted_and_filtered_todo_list
+
+
+Organizing Items With Keys
+--------------------------
+
+If you run the examples above :ref:`in debug mode ` you'll
+see the server log a bunch of errors that look something like:
+
+.. code-block:: text
+
+ Key not specified for dynamic child {'tagName': 'li', 'children': ['Do some coding']}
+
+What this is telling you is that we haven't specified a unique ``key`` for each of the
+items in our todo list. In order to silence this warning we need to expand our data
+structure even further to include a unique ID for each item in our todo list:
+
+.. testcode::
+
+ tasks = [
+ {"id": 0, "text": "Make breakfast", "priority": 0},
+ {"id": 1, "text": "Feed the dog", "priority": 0},
+ {"id": 2, "text": "Do laundry", "priority": 2},
+ {"id": 3, "text": "Go on a run", "priority": 1},
+ {"id": 4, "text": "Clean the house", "priority": 2},
+ {"id": 5, "text": "Go to the grocery store", "priority": 2},
+ {"id": 6, "text": "Do some coding", "priority": 1},
+ {"id": 7, "text": "Read a book", "priority": 1},
+ ]
+
+Then, as we're constructing our ```` elements we'll pass in a ``key`` argument to
+the element constructor:
+
+.. code-block::
+
+ list_item_elements = [html.li(t["text"], key=t["id"]) for t in tasks]
+
+This ``key`` tells IDOM which `` `` element corresponds to which item of data in our
+``tasks`` list. This becomes important if the order or number of items in your list can
+change. In our case, if we decided to change whether we want to ``filter_by_priority``
+or ``sort_by_priority`` the items in our ```` element would change. Given this,
+here's how we'd change our component:
+
+.. idom:: _examples/todo_list_with_keys
+
+
+Keys for Components
+...................
+
+Thus far we've been talking about passing keys to standard HTML elements. However, this
+principle also applies to components too. Every function decorated with the
+``@component`` decorator automatically gets a ``key`` parameter that operates in the
+exact same way that it does for standard HTML elements:
+
+.. testcode::
+
+ from idom import component
+
+
+ @component
+ def ListItem(text):
+ return html.li(text)
+
+ tasks = [
+ {"id": 0, "text": "Make breakfast"},
+ {"id": 1, "text": "Feed the dog"},
+ {"id": 2, "text": "Do laundry"},
+ {"id": 3, "text": "Go on a run"},
+ {"id": 4, "text": "Clean the house"},
+ {"id": 5, "text": "Go to the grocery store"},
+ {"id": 6, "text": "Do some coding"},
+ {"id": 7, "text": "Read a book"},
+ ]
+
+ list_element = [ListItem(t["text"], key=t["id"]) for t in tasks]
+
+
+.. warning::
+
+ The ``key`` argument is reserved for this purpose. Defining a component with a
+ function that has a ``key`` parameter will cause an error:
+
+ .. testcode::
+
+ from idom import component
+
+ @component
+ def FunctionWithKeyParam(key):
+ ...
+
+ .. testoutput::
+
+ Traceback (most recent call last):
+ ...
+ TypeError: Component render function ... uses reserved parameter 'key'
+
+
+Rules of Keys
+.............
+
+In order to avoid unexpected behaviors when rendering data with keys, there are a few
+rules that need to be followed. These will ensure that each item of data is associated
+with the correct UI element.
+
+.. dropdown:: Keys may be the same if their elements are not siblings
+ :color: info
+
+ If two elements have different parents in the UI, they can use the same keys.
+
+ .. testcode::
+
+ data_1 = [
+ {"id": 1, "text": "Something"},
+ {"id": 2, "text": "Something else"},
+ ]
+
+ data_2 = [
+ {"id": 1, "text": "Another thing"},
+ {"id": 2, "text": "Yet another thing"},
+ ]
+
+ html.section(
+ html.ul([html.li(data["text"], key=data["id"]) for data in data_1]),
+ html.ul([html.li(data["text"], key=data["id"]) for data in data_2]),
+ )
+
+.. dropdown:: Keys must be unique amonst siblings
+ :color: danger
+
+ Keys must be unique among siblings.
+
+ .. testcode::
+
+ data = [
+ {"id": 1, "text": "Something"},
+ {"id": 2, "text": "Something else"},
+ {"id": 1, "text": "Another thing"}, # BAD: has a duplicated id
+ {"id": 2, "text": "Yet another thing"}, # BAD: has a duplicated id
+ ]
+
+ html.section(
+ html.ul([html.li(data["text"], key=data["id"]) for data in data]),
+ )
+
+.. dropdown:: Keys must be fixed to their data.
+ :color: danger
+
+ Don't generate random values for keys to avoid the warning.
+
+ .. testcode::
+
+ from random import random
+
+ data = [
+ {"id": random(), "text": "Something"},
+ {"id": random(), "text": "Something else"},
+ {"id": random(), "text": "Another thing"},
+ {"id": random(), "text": "Yet another thing"},
+ ]
+
+ html.section(
+ html.ul([html.li(data["text"], key=data["id"]) for data in data]),
+ )
+
+ Doing so will result in unexpected behavior.
+
+Since we've just been working with a small amount of sample data thus far, it was easy
+enough for us to manually add an ``id`` key to each item of data. Often though, we have
+to work with data that already exists. In those cases, how should we pick what value to
+use for each ``key``?
+
+- If your data comes from your database you should use the keys and IDs generated by
+ that database since these are inherently unique. For example, you might user the
+ primary key of records in a relational database.
+
+- If your data is generated and persisted locally (e.g. notes in a note-taking app), use
+ an incrementing counter or :mod:`uuid` from the standard library when creating items.
+
+
+----------
+
+
+.. card::
+ :link: /understanding-idom/why-idom-needs-keys
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn about why IDOM needs keys in the first place.
diff --git a/docs/source/creating-interfaces/your-first-components.rst b/docs/source/creating-interfaces/your-first-components.rst
new file mode 100644
index 000000000..eeee7fa31
--- /dev/null
+++ b/docs/source/creating-interfaces/your-first-components.rst
@@ -0,0 +1,77 @@
+Your First Components
+=====================
+
+As we learned :ref:`earlier ` we can use IDOM to make rich structured
+documents out of standard HTML elements. As these documents become larger and more
+complex though, working with these tiny UI elements can become difficult. When this
+happens, IDOM allows you to group these elements together info "components". These
+components can then be reused throughout your application.
+
+
+Defining a Component
+--------------------
+
+At their core, components are just normal Python functions that return HTML. To define a
+component you just need to add a ``@component`` `decorator
+ `__ to a function. Functions
+decorator in this way are known as **render function** and, by convention, we name them
+like classes - with ``CamelCase``. So consider what we would do if we wanted to write,
+and then :ref:`display ` a ``Photo`` component:
+
+.. idom:: _examples/simple_photo
+
+.. warning::
+
+ If we had not decorated our ``Photo``'s render function with the ``@component``
+ decorator, the server would start, but as soon as we tried to view the page it would
+ be blank. The servers logs would then indicate:
+
+ .. code-block:: text
+
+ TypeError: Expected a ComponentType, not dict.
+
+
+Using a Component
+-----------------
+
+Having defined our ``Photo`` component we can now nest it inside of other components. We
+can define a "parent" ``Gallery`` component that returns one or more ``Profile``
+components. This is part of what makes components so powerful - you can define a
+component once and use it wherever and however you need to:
+
+.. idom:: _examples/nested_photos
+
+
+Parametrizing Components
+------------------------
+
+Since components are just regular functions, you can add parameters to them. This allows
+parent components to pass information to child components. Where standard HTML elements
+are parametrized by dictionaries, since components behave like typical functions you can
+give them positional and keyword arguments as you would normally:
+
+.. idom:: _examples/parametrized_photos
+
+
+Conditional Rendering
+---------------------
+
+Your components will often need to display different things depending on different
+conditions. Let's imagine that we had a basic todo list where only some of the items
+have been completed. Below we have a basic implementation for such a list except that
+the ``Item`` component doesn't change based on whether it's ``done``:
+
+.. idom:: _examples/todo_list
+
+Let's imagine that we want to add a ✔ to the items which have been marked ``done=True``.
+One way to do this might be to write an ``if`` statement where we return one ``li``
+element if the item is ``done`` and a different one if it's not:
+
+.. idom:: _examples/bad_conditional_todo_list
+
+As you can see this accomplishes our goal! However, notice how similar ``html.li(name, "
+✔")`` and ``html.li(name)`` are. While in this case it isn't especially harmful, we
+could make our code a little easier to read and maintain by using an "inline" ``if``
+statement.
+
+.. idom:: _examples/good_conditional_todo_list
diff --git a/docs/source/credits-and-licenses.rst b/docs/source/credits-and-licenses.rst
new file mode 100644
index 000000000..5aef11a59
--- /dev/null
+++ b/docs/source/credits-and-licenses.rst
@@ -0,0 +1,16 @@
+Credits and Licenses
+====================
+
+Much of this documentation, including its layout and content, was created with heavy
+influence from https://reactjs.org which uses the `Creative Commons Attribution 4.0
+International
+`__
+license. While many things have been transformed, we paraphrase and, in some places,
+copy language or examples where IDOM's behavior mirrors that of React's.
+
+
+Source Code License
+-------------------
+
+.. literalinclude:: ../../LICENSE
+ :language: text
diff --git a/docs/source/changelog.rst b/docs/source/developing-idom/changelog.rst
similarity index 97%
rename from docs/source/changelog.rst
rename to docs/source/developing-idom/changelog.rst
index fa8ff374f..8349d5154 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/developing-idom/changelog.rst
@@ -347,11 +347,10 @@ update deletes the original button.
0.25.0
------
-Completely refactors :ref:`Layout Dispatchers ` by switching from a
-class-based approach to one that leverages pure functions. While the logic itself isn't
-any simpler, it was easier to implement, and now hopefully understand, correctly. This
-conversion was motivated by several bugs that had cropped up related to improper usage
-of ``anyio``.
+Completely refactors layout dispatcher by switching from a class-based approach to one
+that leverages pure functions. While the logic itself isn't any simpler, it was easier
+to implement, and now hopefully understand, correctly. This conversion was motivated by
+several bugs that had cropped up related to improper usage of ``anyio``.
**Issues Fixed:**
diff --git a/docs/source/developer-guide.rst b/docs/source/developing-idom/contributor-guide.rst
similarity index 84%
rename from docs/source/developer-guide.rst
rename to docs/source/developing-idom/contributor-guide.rst
index 153a3b05a..a7f73bdb9 100644
--- a/docs/source/developer-guide.rst
+++ b/docs/source/developing-idom/contributor-guide.rst
@@ -1,46 +1,16 @@
-Developer Guide
-===============
+Contributor Guide
+=================
.. note::
If you have any questions during set up or development post on our
- `discussion board `__ and we'll
- answer them.
+ :discussion-type:`discussion board ` and we'll answer them.
This project uses the `GitHub Flow`_ (more detail :ref:`below `)
-for collaboration and consists primarily of Python code and Javascript code. A variety
-of tools are used to aid in its development. Below is a brief list of the most commonly
-used tools:
-
-.. list-table::
- :header-rows: 1
-
- * - Tool
- - Used For
-
- * - Git_
- - version control
-
- * - Nox_
- - automating development tasks.
-
- * - PyTest_
- - executing the Python-based test suite
-
- * - pre-commit_
- - helping impose basic style guidelines
-
- * - NPM_
- - managing and installing Javascript packages
-
- * - Selenium_ and ChromeDriver_
- - to control the browser while testing
-
- * - `GitHub Actions`_
- - hosting and running our CI/CD suite
-
- * - Docker_ and Heroku_
- - containerizing and hosting this documentation
+for collaboration and consists primarily of Python code and Javascript code. A
+:ref:`variety of tools ` are used to aid in its development.
+Any code contributed to IDOM is validated by a :ref:` series of tests ` to ensure its quality and correctness.
Making a Pull Request
@@ -95,6 +65,9 @@ In order to develop IDOM locally you'll first need to install the following:
* - NPM >= 7.13
- https://docs.npmjs.com/try-the-latest-stable-version-of-npm
+ * - Docker
+ - https://docs.docker.com/get-docker/
+
.. note::
NodeJS distributes a version of NPM, but you'll want to get the latest
@@ -124,7 +97,7 @@ If you modify any Javascript, you'll need to re-install IDOM:
pip install -e .
-However you may also ``cd`` to the ``src/idom/client/app`` directory which contains a
+However you may also ``cd`` to the ``src/client`` directory which contains a
``package.json`` that you can use to run standard ``npm`` commands from.
@@ -191,10 +164,10 @@ fails, the installation of the Python package with ``pip`` will too.
Code Quality Checks
-------------------
-Several tools are run on the Python codebase to help validate its quality. For the most
-part, if you set up your :ref:`Development Environment` with ``pre-commit`` to check
-your work before you commit it, then you'll be notified when changes need to be made or,
-in the best case, changes will be made automatically for you.
+Several tools are run on the codebase to help validate its quality. For the most part,
+if you set up your :ref:`Development Environment` with pre-commit_ to check your work
+before you commit it, then you'll be notified when changes need to be made or, in the
+best case, changes will be made automatically for you.
The following are currently being used:
@@ -202,15 +175,15 @@ The following are currently being used:
- Black_ - an opinionated code formatter
- Flake8_ - a style guide enforcement tool
- ISort_ - a utility for alphabetically sorting imports
+- Prettier_ - a tool for autimatically formatting Javascript code
The most strict measure of quality enforced on the codebase is 100% coverage. This means
that every line of coded added to IDOM requires a test case that exercises it. This
doesn't prevent all bugs, but it should ensure that we catch the most common ones.
If you need help understanding why code you've submitted does not pass these checks,
-then be sure to ask, either in the
-`Community Forum `__ or in your
-:ref:`Pull Request `.
+then be sure to ask, either in the :discussion-type:`Community Forum ` or in
+your :ref:`Pull Request `.
.. note::
@@ -363,12 +336,17 @@ Other Core Repositories
IDOM depends on, or is used by several other core projects. For documentation on them
you should refer to their respective documentation in the links below:
-- https://github.com/idom-team/idom-react-component-cookiecutter - A template repo for
- making :ref:`Custom Javascript Components`.
-- https://github.com/idom-team/flake8-idom-hooks - Enforces the :ref:`Rules of Hooks`
-- https://github.com/idom-team/idom-jupyter - IDOM integration for Jupyter
-- https://github.com/idom-team/idom-dash - IDOM integration for Plotly Dash
-- https://github.com/idom-team/django-idom - IDOM integration for Django
+- `idom-react-component-cookiecutter
+ `__ - Template repo
+ for making :ref:`Custom Javascript Components`.
+- `flake8-idom-hooks `__ - Enforces the
+ :ref:`Rules of Hooks`
+- `idom-jupyter `__ - IDOM integration for
+ Jupyter
+- `idom-dash `__ - IDOM integration for Plotly
+ Dash
+- `django-idom `__ - IDOM integration for
+ Django
.. Links
.. =====
@@ -394,3 +372,4 @@ you should refer to their respective documentation in the links below:
.. _Flake8: https://flake8.pycqa.org/en/latest/
.. _ISort: https://pycqa.github.io/isort/
.. _UVU: https://github.com/lukeed/uvu
+.. _Prettier: https://prettier.io/
diff --git a/docs/source/developing-idom/index.rst b/docs/source/developing-idom/index.rst
new file mode 100644
index 000000000..88a83eea7
--- /dev/null
+++ b/docs/source/developing-idom/index.rst
@@ -0,0 +1,67 @@
+Developing IDOM
+===============
+
+.. toctree::
+ :hidden:
+
+ contributor-guide
+ changelog
+ roadmap
+ /_autogen/dev-apis
+
+.. note::
+
+ The
+ `Code of Conduct `__
+ applies in all community spaces. If you are not familiar with our Code of Conduct
+ policy, take a minute to read it before making your first contribution.
+
+The IDOM team welcomes contributions and contributors of all kinds - whether they come
+as code changes, participation in the discussions, opening issues and pointing out bugs,
+or simply sharing your work with your colleagues and friends. We're excited to see how
+you can help move this project and community forward!
+
+
+.. grid:: 2
+ :gutter: 1
+
+ .. grid-item-card:: :octicon:`people` Discussion Forum
+ :link: https://github.com/idom-team/idom/discussions
+
+ Ask questions, share ideas, or show projects
+
+ .. grid-item-card:: :octicon:`info` Contributor Guide
+ :link: contributor-guide
+ :link-type: doc
+
+ Learn how to make changes to IDOM and submit them back to the project
+
+ .. grid-item-card:: :octicon:`git-pull-request` Changelog
+ :link: changelog
+ :link-type: doc
+
+ Discover the features and fixes included in each release
+
+ .. grid-item-card:: :octicon:`milestone` Roadmap
+ :link: roadmap
+ :link-type: doc
+
+ See the long term goals of the IDOM team
+
+
+.. _everyone can contribute:
+
+Everyone Can Contribute!
+------------------------
+
+Trust us, there's so many ways to support the project. We're always looking for people
+who can:
+
+- Improve our documentation
+- Teach and tell others about IDOM
+- Share ideas for new features
+- Report bugs
+- Participate in general discussions
+
+Still aren't sure what you have to offer? Just :discussion-type:`ask us ` and
+we'll help you make your first contribution.
diff --git a/docs/source/roadmap.rst b/docs/source/developing-idom/roadmap.rst
similarity index 100%
rename from docs/source/roadmap.rst
rename to docs/source/developing-idom/roadmap.rst
diff --git a/docs/source/examples/material_ui_button_no_action.py b/docs/source/escape-hatches/_examples/material_ui_button_no_action.py
similarity index 100%
rename from docs/source/examples/material_ui_button_no_action.py
rename to docs/source/escape-hatches/_examples/material_ui_button_no_action.py
diff --git a/docs/source/examples/material_ui_button_on_click.py b/docs/source/escape-hatches/_examples/material_ui_button_on_click.py
similarity index 100%
rename from docs/source/examples/material_ui_button_on_click.py
rename to docs/source/escape-hatches/_examples/material_ui_button_on_click.py
diff --git a/docs/source/escape-hatches/_examples/super_simple_chart/app.py b/docs/source/escape-hatches/_examples/super_simple_chart/app.py
new file mode 100644
index 000000000..2f2e17556
--- /dev/null
+++ b/docs/source/escape-hatches/_examples/super_simple_chart/app.py
@@ -0,0 +1,33 @@
+from pathlib import Path
+
+from idom import component, run, web
+
+
+file = Path(__file__).parent / "super-simple-chart.js"
+ssc = web.module_from_file("super-simple-chart", file, fallback="⌛")
+SuperSimpleChart = web.export(ssc, "SuperSimpleChart")
+
+
+@component
+def App():
+ return SuperSimpleChart(
+ {
+ "data": [
+ {"x": 1, "y": 2},
+ {"x": 2, "y": 4},
+ {"x": 3, "y": 7},
+ {"x": 4, "y": 3},
+ {"x": 5, "y": 5},
+ {"x": 6, "y": 9},
+ {"x": 7, "y": 6},
+ ],
+ "height": 300,
+ "width": 500,
+ "color": "royalblue",
+ "lineWidth": 4,
+ "axisColor": "silver",
+ }
+ )
+
+
+run(App)
diff --git a/docs/source/examples/super_simple_chart.js b/docs/source/escape-hatches/_examples/super_simple_chart/super-simple-chart.js
similarity index 100%
rename from docs/source/examples/super_simple_chart.js
rename to docs/source/escape-hatches/_examples/super_simple_chart/super-simple-chart.js
diff --git a/docs/source/escape-hatches/class-components.rst b/docs/source/escape-hatches/class-components.rst
new file mode 100644
index 000000000..5b7ec19d8
--- /dev/null
+++ b/docs/source/escape-hatches/class-components.rst
@@ -0,0 +1,13 @@
+.. _Class Components:
+
+Class Components 🚧
+===================
+
+.. warning::
+
+ This feature has not been implemented `yet
+ `__.
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/javascript-components.rst b/docs/source/escape-hatches/distributing-javascript.rst
similarity index 67%
rename from docs/source/javascript-components.rst
rename to docs/source/escape-hatches/distributing-javascript.rst
index 2355b79b8..9ed94f3ea 100644
--- a/docs/source/javascript-components.rst
+++ b/docs/source/escape-hatches/distributing-javascript.rst
@@ -1,142 +1,5 @@
-.. _Javascript Component:
-
-Javascript Components
-=====================
-
-While IDOM is a great tool for displaying HTML and responding to browser events with
-pure Python, there are other projects which already allow you to do this inside
-`Jupyter Notebooks `__
-or in standard
-`web apps `__.
-The real power of IDOM comes from its ability to seamlessly leverage the existing
-Javascript ecosystem. This can be accomplished in different ways for different reasons:
-
-.. list-table::
- :header-rows: 1
-
- * - Integration Method
- - Use Case
-
- * - :ref:`Dynamically Loaded Components`
- - You want to **quickly experiment** with IDOM and the Javascript ecosystem.
-
- * - :ref:`Custom Javascript Components`
- - You want to create polished software that can be **easily shared** with others.
-
-
-.. _Dynamically Loaded Component:
-
-Dynamically Loaded Components
------------------------------
-
-.. note::
-
- This method is not recommended in production systems - see
- :ref:`Distributing Javascript Components` for more info. Instead, it's best used
- during exploratory phases of development.
-
-IDOM makes it easy to draft your code when you're in the early stages of development by
-using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll
-be using the ubiquitous React-based UI framework `Material UI`_.
-
-.. example:: material_ui_button_no_action
-
-So now that we can display a Material UI Button we probably want to make it do
-something. Thankfully there's nothing new to learn here, you can pass event handlers to
-the button just as you did when :ref:`getting started`. Thus, all we need to do is add
-an ``onClick`` handler to the component:
-
-.. example:: material_ui_button_on_click
-
-
-.. _Custom Javascript Component:
-
-Custom Javascript Components
-----------------------------
-
-For projects that will be shared with others, we recommend bundling your Javascript with
-Rollup_ or Webpack_ into a `web module`_. IDOM also provides a `template repository`_
-that can be used as a blueprint to build a library of React components.
-
-To work as intended, the Javascript bundle must export a function ``bind()`` that
-adheres to the following interface:
-
-.. code-block:: typescript
-
- type EventData = {
- target: string;
- data: Array;
- }
-
- type LayoutContext = {
- sendEvent(data: EventData) => void;
- loadImportSource(source: string, sourceType: "NAME" | "URL") => Module;
- }
-
- type bind = (node: HTMLElement, context: LayoutContext) => ({
- create(type: any, props: Object, children: Array): any;
- render(element): void;
- unmount(): void;
- });
-
-.. note::
-
- - ``node`` is the ``HTMLElement`` that ``render()`` should mount to.
-
- - ``context`` can send events back to the server and load "import sources"
- (like a custom component module).
-
- - ``type``is a named export of the current module, or a string (e.g. ``"div"``,
- ``"button"``, etc.)
-
- - ``props`` is an object containing attributes and callbacks for the given
- ``component``.
-
- - ``children`` is an array of elements which were constructed by recursively calling
- ``create``.
-
-The interface returned by ``bind()`` can be thought of as being similar to that of
-React.
-
-- ``create`` ➜ |React.createElement|_
-- ``render`` ➜ |ReactDOM.render|_
-- ``unmount`` ➜ |ReactDOM.unmountComponentAtNode|_
-
-.. |React.createElement| replace:: ``React.createElement``
-.. _React.createElement: https://reactjs.org/docs/react-api.html#createelement
-
-.. |ReactDOM.render| replace:: ``ReactDOM.render``
-.. _ReactDOM.render: https://reactjs.org/docs/react-dom.html#render
-
-.. |ReactDOM.unmountComponentAtNode| replace:: ``ReactDOM.unmountComponentAtNode``
-.. _ReactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement
-
-It will be used in the following manner:
-
-.. code-block:: javascript
-
- // once on mount
- const binding = bind(node, context);
-
- // on every render
- let element = binding.create(type, props, children)
- binding.render(element);
-
- // once on unmount
- binding.unmount();
-
-The simplest way to try this out yourself though, is to hook in a simple hand-crafted
-Javascript module that has the requisite interface. In the example to follow we'll
-create a very basic SVG line chart. The catch though is that we are limited to using
-Javascript that can run directly in the browser. This means we can't use fancy syntax
-like `JSX `__ and instead will use
-`htm `__ to simulate JSX in plain Javascript.
-
-.. example:: super_simple_chart
-
-
-Distributing Javascript Components
-----------------------------------
+Distributing Javascript
+=======================
There are two ways that you can distribute your :ref:`Custom Javascript Components`:
@@ -160,7 +23,7 @@ your users, will have to consider the tradeoffs of either approach.
Distributing Javascript via CDN_
-................................
+--------------------------------
Under this approach, to simplify these instructions, we're going to ignore the problem
of distributing the Javascript since that must be handled by your CDN. For open source
@@ -181,7 +44,7 @@ where you can then load any of its exports:
Distributing Javascript via PyPI_
-.................................
+---------------------------------
This can be most easily accomplished by using the `template repository`_ that's been
purpose-built for this. However, to get a better sense for its inner workings, we'll
@@ -430,7 +293,6 @@ up and running as quickly as possible.
.. Links
.. =====
-.. _Material UI: https://material-ui.com/
.. _NPM: https://www.npmjs.com
.. _install NPM: https://www.npmjs.com/get-npm
.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
diff --git a/docs/source/escape-hatches/index.rst b/docs/source/escape-hatches/index.rst
new file mode 100644
index 000000000..ddf0be60e
--- /dev/null
+++ b/docs/source/escape-hatches/index.rst
@@ -0,0 +1,15 @@
+Escape Hatches
+==============
+
+.. toctree::
+ :hidden:
+
+ class-components
+ javascript-components
+ distributing-javascript
+ writing-your-own-server
+ writing-your-own-client
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/escape-hatches/javascript-components.rst b/docs/source/escape-hatches/javascript-components.rst
new file mode 100644
index 000000000..be31f6de2
--- /dev/null
+++ b/docs/source/escape-hatches/javascript-components.rst
@@ -0,0 +1,146 @@
+.. _Javascript Component:
+
+Javascript Components
+=====================
+
+While IDOM is a great tool for displaying HTML and responding to browser events with
+pure Python, there are other projects which already allow you to do this inside
+`Jupyter Notebooks `__
+or in standard
+`web apps `__.
+The real power of IDOM comes from its ability to seamlessly leverage the existing
+Javascript ecosystem. This can be accomplished in different ways for different reasons:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Integration Method
+ - Use Case
+
+ * - :ref:`Dynamically Loaded Components`
+ - You want to **quickly experiment** with IDOM and the Javascript ecosystem.
+
+ * - :ref:`Custom Javascript Components`
+ - You want to create polished software that can be **easily shared** with others.
+
+
+.. _Dynamically Loaded Component:
+
+Dynamically Loaded Components
+-----------------------------
+
+.. note::
+
+ This method is not recommended in production systems - see :ref:`Distributing
+ Javascript` for more info. Instead, it's best used during exploratory phases of
+ development.
+
+IDOM makes it easy to draft your code when you're in the early stages of development by
+using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll
+be using the ubiquitous React-based UI framework `Material UI`_.
+
+.. idom:: _examples/material_ui_button_no_action
+
+So now that we can display a Material UI Button we probably want to make it do
+something. Thankfully there's nothing new to learn here, you can pass event handlers to
+the button just as you did when :ref:`getting started `. Thus, all
+we need to do is add an ``onClick`` handler to the component:
+
+.. idom:: _examples/material_ui_button_on_click
+
+
+.. _Custom Javascript Component:
+
+Custom Javascript Components
+----------------------------
+
+For projects that will be shared with others, we recommend bundling your Javascript with
+Rollup_ or Webpack_ into a `web module`_. IDOM also provides a `template repository`_
+that can be used as a blueprint to build a library of React components.
+
+To work as intended, the Javascript bundle must export a function ``bind()`` that
+adheres to the following interface:
+
+.. code-block:: typescript
+
+ type EventData = {
+ target: string;
+ data: Array;
+ }
+
+ type LayoutContext = {
+ sendEvent(data: EventData) => void;
+ loadImportSource(source: string, sourceType: "NAME" | "URL") => Module;
+ }
+
+ type bind = (node: HTMLElement, context: LayoutContext) => ({
+ create(type: any, props: Object, children: Array): any;
+ render(element): void;
+ unmount(): void;
+ });
+
+.. note::
+
+ - ``node`` is the ``HTMLElement`` that ``render()`` should mount to.
+
+ - ``context`` can send events back to the server and load "import sources"
+ (like a custom component module).
+
+ - ``type``is a named export of the current module, or a string (e.g. ``"div"``,
+ ``"button"``, etc.)
+
+ - ``props`` is an object containing attributes and callbacks for the given
+ ``component``.
+
+ - ``children`` is an array of elements which were constructed by recursively calling
+ ``create``.
+
+The interface returned by ``bind()`` can be thought of as being similar to that of
+React.
+
+- ``create`` ➜ |React.createElement|_
+- ``render`` ➜ |ReactDOM.render|_
+- ``unmount`` ➜ |ReactDOM.unmountComponentAtNode|_
+
+.. |React.createElement| replace:: ``React.createElement``
+.. _React.createElement: https://reactjs.org/docs/react-api.html#createelement
+
+.. |ReactDOM.render| replace:: ``ReactDOM.render``
+.. _ReactDOM.render: https://reactjs.org/docs/react-dom.html#render
+
+.. |ReactDOM.unmountComponentAtNode| replace:: ``ReactDOM.unmountComponentAtNode``
+.. _ReactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement
+
+It will be used in the following manner:
+
+.. code-block:: javascript
+
+ // once on mount
+ const binding = bind(node, context);
+
+ // on every render
+ let element = binding.create(type, props, children)
+ binding.render(element);
+
+ // once on unmount
+ binding.unmount();
+
+The simplest way to try this out yourself though, is to hook in a simple hand-crafted
+Javascript module that has the requisite interface. In the example to follow we'll
+create a very basic SVG line chart. The catch though is that we are limited to using
+Javascript that can run directly in the browser. This means we can't use fancy syntax
+like `JSX `__ and instead will use
+`htm `__ to simulate JSX in plain Javascript.
+
+.. idom:: _examples/super_simple_chart
+
+
+.. Links
+.. =====
+
+.. _Material UI: https://material-ui.com/
+.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network
+.. _template repository: https://github.com/idom-team/idom-react-component-cookiecutter
+.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
+.. _Rollup: https://rollupjs.org/guide/en/
+.. _Webpack: https://webpack.js.org/
diff --git a/docs/source/escape-hatches/writing-your-own-client.rst b/docs/source/escape-hatches/writing-your-own-client.rst
new file mode 100644
index 000000000..f52235813
--- /dev/null
+++ b/docs/source/escape-hatches/writing-your-own-client.rst
@@ -0,0 +1,8 @@
+.. _Writing Your Own Client:
+
+Writing Your Own Client 🚧
+==========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/escape-hatches/writing-your-own-server.rst b/docs/source/escape-hatches/writing-your-own-server.rst
new file mode 100644
index 000000000..51f0a70e9
--- /dev/null
+++ b/docs/source/escape-hatches/writing-your-own-server.rst
@@ -0,0 +1,8 @@
+.. _Writing Your Own Server:
+
+Writing Your Own Server 🚧
+==========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/examples/prevent_default_event_actions.py b/docs/source/examples/prevent_default_event_actions.py
deleted file mode 100644
index eb3bac208..000000000
--- a/docs/source/examples/prevent_default_event_actions.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import idom
-
-
-@idom.component
-def DoNotChangePages():
- return idom.html.div(
- idom.html.p("Normally clicking this link would take you to a new page"),
- idom.html.a(
- {
- "onClick": idom.event(lambda e: None, prevent_default=True),
- "href": "https://google.com",
- },
- "https://google.com",
- ),
- )
-
-
-idom.run(DoNotChangePages)
diff --git a/docs/source/examples/show_click_event.py b/docs/source/examples/show_click_event.py
deleted file mode 100644
index 6e9691e50..000000000
--- a/docs/source/examples/show_click_event.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import json
-
-import idom
-
-
-@idom.component
-def BasicButton():
- event, set_event = idom.hooks.use_state(None)
- return idom.html.div(
- idom.html.button({"onClick": lambda e: set_event(e)}, "click to see event"),
- idom.html.pre(json.dumps(event, indent=2)),
- )
-
-
-idom.run(BasicButton)
diff --git a/docs/source/examples/simple_and_gate.py b/docs/source/examples/simple_and_gate.py
deleted file mode 100644
index 36ad60fd8..000000000
--- a/docs/source/examples/simple_and_gate.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import idom
-
-
-@idom.component
-def AndGate():
- input_1, toggle_1 = use_toggle()
- input_2, toggle_2 = use_toggle()
- return idom.html.div(
- idom.html.input({"type": "checkbox", "onClick": lambda event: toggle_1()}),
- idom.html.input({"type": "checkbox", "onClick": lambda event: toggle_2()}),
- idom.html.pre(f"{input_1} AND {input_2} = {input_1 and input_2}"),
- )
-
-
-def use_toggle():
- state, set_state = idom.hooks.use_state(False)
-
- def toggle_state():
- set_state(lambda old_state: not old_state)
-
- return state, toggle_state
-
-
-idom.run(AndGate)
diff --git a/docs/source/examples/stop_event_propagation.py b/docs/source/examples/stop_event_propagation.py
deleted file mode 100644
index 7478d94e5..000000000
--- a/docs/source/examples/stop_event_propagation.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import idom
-
-
-@idom.component
-def DivInDiv():
- stop_propagatation, set_stop_propagatation = idom.hooks.use_state(True)
- inner_count, set_inner_count = idom.hooks.use_state(0)
- outer_count, set_outer_count = idom.hooks.use_state(0)
-
- div_in_div = idom.html.div(
- {
- "onClick": idom.event(lambda e: set_outer_count(outer_count + 1)),
- "style": {"height": "100px", "width": "100px", "backgroundColor": "red"},
- },
- idom.html.div(
- {
- "onClick": idom.event(
- lambda e: set_inner_count(inner_count + 1),
- stop_propagation=stop_propagatation,
- ),
- "style": {"height": "50px", "width": "50px", "backgroundColor": "blue"},
- },
- ),
- )
-
- return idom.html.div(
- idom.html.button(
- {"onClick": lambda e: set_stop_propagatation(not stop_propagatation)},
- "Toggle Propogation",
- ),
- idom.html.pre(f"Will propagate: {not stop_propagatation}"),
- idom.html.pre(f"Inner click count: {inner_count}"),
- idom.html.pre(f"Outer click count: {outer_count}"),
- div_in_div,
- )
-
-
-idom.run(DivInDiv)
diff --git a/docs/source/examples/super_simple_chart.py b/docs/source/examples/super_simple_chart.py
deleted file mode 100644
index 8f5d77ae3..000000000
--- a/docs/source/examples/super_simple_chart.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from pathlib import Path
-
-import idom
-
-
-file = Path(__file__).parent / "super_simple_chart.js"
-ssc = idom.web.module_from_file("super-simple-chart", file, fallback="⌛")
-SuperSimpleChart = idom.web.export(ssc, "SuperSimpleChart")
-
-idom.run(
- idom.component(
- lambda: SuperSimpleChart(
- {
- "data": [
- {"x": 1, "y": 2},
- {"x": 2, "y": 4},
- {"x": 3, "y": 7},
- {"x": 4, "y": 3},
- {"x": 5, "y": 5},
- {"x": 6, "y": 9},
- {"x": 7, "y": 6},
- ],
- "height": 300,
- "width": 500,
- "color": "royalblue",
- "lineWidth": 4,
- "axisColor": "silver",
- }
- )
- )
-)
diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst
deleted file mode 100644
index 985f5ea5a..000000000
--- a/docs/source/getting-started.rst
+++ /dev/null
@@ -1,95 +0,0 @@
-Getting Started
-===============
-
-Let's break down the following example:
-
-.. example:: slideshow
- :linenos:
-
-Since it's likely a lot to take in at once, we'll break it down piece by piece:
-
-.. literalinclude:: /examples/slideshow.py
- :lineno-start: 4
- :lines: 4-5
- :linenos:
-
-The ``idom.component`` decorator creates a :ref:`Component `
-constructor whose "renderer" is the function below it. To create a Component instance
-we call ``Slideshow()`` with the same arguments as its render function. The render
-function of a Component returns a data structure that depicts a user interface, or in
-more technical terms a Document Object Model (DOM). We call this structural
-representation of the DOM a `Virtual DOM`__ (VDOM) - a term familiar to those who work
-with `ReactJS`_. In the case of ``Slideshow`` it will return a VDOM representing an
-image which, when clicked, will change.
-
-__ https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom
-
-.. literalinclude:: /examples/slideshow.py
- :lineno-start: 6
- :lines: 6
- :linenos:
-
-The :func:`~idom.core.hooks.use_state` function is a :ref:`Hook `.
-Calling a Hook inside a Component's render function (one decorated by ``idom.component``)
-adds some local state to it. IDOM will preserve the state added by Hooks between calls
-to the Component's render function.
-
-The ``use_state`` hook returns two values - the **current** state value and a function
-that let's you update that value. In the case of ``Slideshow`` the value of the
-``use_state`` hook determines which image is shown to the user, while its update
-function allow us to change it. The one required argument of ``use_state`` is the
-**initial** state value.
-
-.. literalinclude:: /examples/slideshow.py
- :lineno-start: 8
- :lines: 8,9
- :linenos:
-
-The function above will get added as an event handler to the resulting view. When it
-responds to an event it will use ``set_state`` (the update function returned by the
-``use_state`` Hook) to change which image is shown to the user. Calling the update
-function will schedule the Component to be re-rendered. That is, the Component's render
-function will be called again, and its new result will be displayed.
-
-.. note::
-
- Even handlers like ``next_image`` which respond to user interactions receive an
- ``event`` dictionary that contains different information depending on the type of
- event that occurred. All supported events and the data they contain are listed
- `here`__.
-
-__ https://reactjs.org/docs/events.html
-
-.. literalinclude:: /examples/slideshow.py
- :lineno-start: 11
- :lines: 11-16
- :linenos:
-
-Finally we come to the end of the ``Slideshow`` body where we return a model for an
-`` `` element that draws its image from https://picsum.photos. Our ``next_image``
-event handler has been added to the image so that when an ``onClick`` event occurs we
-can respond to it. We've also added a little bit of CSS styling to the image so that
-when the cursor hovers over the image it will become a pointer so it appears clickable.
-The returned model conforms to the `VDOM mimetype specification`_.
-
-.. literalinclude:: /examples/slideshow.py
- :lineno-start: 20
- :lines: 20
- :linenos:
-
-This last step runs a simple web server that will send the layout of elements defined in
-our ``Slideshow`` to the browser and receive any incoming events from the browser via a
-WebSocket. To display the layout we can navigate to http://localhost:8765/client/index.html.
-
-.. note::
-
- See the :ref:`Examples` section for more info on the ways to display your layouts.
-
-
-.. Links
-.. =====
-
-.. _VDOM event specification: https://github.com/nteract/vdom/blob/master/docs/event-spec.md
-.. _VDOM mimetype specification: https://github.com/nteract/vdom/blob/master/docs/mimetype-spec.md
-.. _ReactJS: https://reactjs.org/docs/faq-internals.html
-.. _React Hooks: https://reactjs.org/docs/hooks-overview.html
diff --git a/docs/source/getting-started/_examples/debug_error_example.py b/docs/source/getting-started/_examples/debug_error_example.py
new file mode 100644
index 000000000..cb0b6b405
--- /dev/null
+++ b/docs/source/getting-started/_examples/debug_error_example.py
@@ -0,0 +1,19 @@
+from idom import component, html, run
+
+
+@component
+def App():
+ return html.div(GoodComponent(), BadComponent())
+
+
+@component
+def GoodComponent():
+ return html.p("This component rendered successfuly")
+
+
+@component
+def BadComponent():
+ raise RuntimeError("This component raised an error")
+
+
+run(App)
diff --git a/docs/source/getting-started/_examples/hello_world.py b/docs/source/getting-started/_examples/hello_world.py
new file mode 100644
index 000000000..a5621718e
--- /dev/null
+++ b/docs/source/getting-started/_examples/hello_world.py
@@ -0,0 +1,9 @@
+from idom import component, html, run
+
+
+@component
+def App():
+ return html.h1("Hello, World!")
+
+
+run(App)
diff --git a/docs/source/getting-started/_examples/sample_app.py b/docs/source/getting-started/_examples/sample_app.py
new file mode 100644
index 000000000..610f8988a
--- /dev/null
+++ b/docs/source/getting-started/_examples/sample_app.py
@@ -0,0 +1,4 @@
+import idom
+
+
+idom.run(idom.sample.App)
diff --git a/docs/source/getting-started/_static/embed-doc-ex.html b/docs/source/getting-started/_static/embed-doc-ex.html
new file mode 100644
index 000000000..027a32694
--- /dev/null
+++ b/docs/source/getting-started/_static/embed-doc-ex.html
@@ -0,0 +1,8 @@
+
+
diff --git a/docs/source/getting-started/_static/embed-idom-view/index.html b/docs/source/getting-started/_static/embed-idom-view/index.html
new file mode 100644
index 000000000..3080e1d5d
--- /dev/null
+++ b/docs/source/getting-started/_static/embed-idom-view/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Example App
+
+
+ This is an Example App
+ Just below is an embedded IDOM view...
+
+
+
+
diff --git a/docs/source/getting-started/_static/embed-idom-view/main.py b/docs/source/getting-started/_static/embed-idom-view/main.py
new file mode 100644
index 000000000..0c0cb5ac0
--- /dev/null
+++ b/docs/source/getting-started/_static/embed-idom-view/main.py
@@ -0,0 +1,23 @@
+from sanic import Sanic
+from sanic.response import file
+
+from idom import component, html
+from idom.server.sanic import Config, PerClientStateServer
+
+
+app = Sanic(__name__)
+
+
+@app.route("/")
+async def index(request):
+ return await file("index.html")
+
+
+@component
+def IdomView():
+ return html.code("This text came from an IDOM App")
+
+
+PerClientStateServer(IdomView, app=app, config=Config(url_prefix="/_idom"))
+
+app.run(host="127.0.0.1", port=5000)
diff --git a/docs/source/getting-started/_static/embed-idom-view/screenshot.png b/docs/source/getting-started/_static/embed-idom-view/screenshot.png
new file mode 100644
index 000000000..7439c83cf
Binary files /dev/null and b/docs/source/getting-started/_static/embed-idom-view/screenshot.png differ
diff --git a/docs/source/getting-started/_static/idom-in-jupyterlab.gif b/docs/source/getting-started/_static/idom-in-jupyterlab.gif
new file mode 100644
index 000000000..b420ecd8c
Binary files /dev/null and b/docs/source/getting-started/_static/idom-in-jupyterlab.gif differ
diff --git a/docs/source/getting-started/_static/logo-django.svg b/docs/source/getting-started/_static/logo-django.svg
new file mode 100644
index 000000000..1538f0817
--- /dev/null
+++ b/docs/source/getting-started/_static/logo-django.svg
@@ -0,0 +1,38 @@
+
+
+
+
+]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/source/getting-started/_static/logo-jupyter.svg b/docs/source/getting-started/_static/logo-jupyter.svg
new file mode 100644
index 000000000..fb2921a41
--- /dev/null
+++ b/docs/source/getting-started/_static/logo-jupyter.svg
@@ -0,0 +1,88 @@
+
+logo.svg
+Created using Figma 0.90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/source/getting-started/_static/logo-plotly.svg b/docs/source/getting-started/_static/logo-plotly.svg
new file mode 100644
index 000000000..3dd95459a
--- /dev/null
+++ b/docs/source/getting-started/_static/logo-plotly.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/source/getting-started/_static/shared-client-state-server-slider.gif b/docs/source/getting-started/_static/shared-client-state-server-slider.gif
new file mode 100644
index 000000000..61bb8295f
Binary files /dev/null and b/docs/source/getting-started/_static/shared-client-state-server-slider.gif differ
diff --git a/docs/source/getting-started/index.rst b/docs/source/getting-started/index.rst
new file mode 100644
index 000000000..0277305f3
--- /dev/null
+++ b/docs/source/getting-started/index.rst
@@ -0,0 +1,108 @@
+Getting Started
+===============
+
+.. toctree::
+ :hidden:
+
+ installing-idom
+ running-idom
+
+.. dropdown:: :octicon:`bookmark-fill;2em` What You'll Learn
+ :color: info
+ :animate: fade-in
+ :open:
+
+ .. grid:: 2
+
+ .. grid-item-card:: :octicon:`tools` Installing IDOM
+ :link: installing-idom
+ :link-type: doc
+
+ Learn how IDOM can be installed in a variety of different ways - with different web
+ servers and even in different frameworks.
+
+ .. grid-item-card:: :octicon:`play` Running IDOM
+ :link: running-idom
+ :link-type: doc
+
+ See the ways that IDOM can be run with servers or be embedded in existing
+ applications.
+
+The fastest way to get started with IDOM is to try it out in a `Juptyer Notebook
+`__.
+If you want to use a Notebook to work through the examples shown in this documentation,
+you'll need to replace calls to ``idom.run(App)`` with a line at the end of each cell
+that constructs the ``App()`` in question. If that doesn't make sense, the introductory
+notebook linked below will demonstrate how to do this:
+
+.. card::
+ :link: https://mybinder.org/v2/gh/idom-team/idom-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb
+
+ .. image:: _static/idom-in-jupyterlab.gif
+ :scale: 72%
+ :align: center
+
+
+Section 1: Installing IDOM
+--------------------------
+
+The next fastest option is to install IDOM with ``pip``:
+
+.. code-block:: bash
+
+ pip install "idom[stable]"
+
+To check that everything is working you can run the sample application:
+
+.. code-block:: bash
+
+ python -c "import idom; idom.run_sample_app(open_browser=True)"
+
+This should automatically open up a browser window to a page that looks like this:
+
+.. card::
+
+ .. idom-view:: _examples/sample_app
+
+If you get a ``RuntimeError`` similar to the following:
+
+.. code-block:: text
+
+ Found none of the following builtin server implementations...
+
+Then be sure you installed ``"idom[stable]"`` and not just ``idom``.
+
+For anything else, report your issue in IDOM's :discussion-type:`discussion forum
+`.
+
+.. card::
+ :link: installing-idom
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn how IDOM can be installed in a variety of different ways - with different web
+ servers and even in different frameworks.
+
+
+Section 2: Running IDOM
+-----------------------
+
+Once you've :ref:`installed IDOM `. The simplest way to run IDOM is
+with the :func:`~idom.server.prefab.run` function. By default this will execute your
+application using one of the builtin server implementations whose dependencies have all
+been installed. Running a tiny "hello world" application just requires the following
+code:
+
+.. idom:: _examples/hello_world
+
+.. card::
+ :link: running-idom
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ See the ways that IDOM can be run with servers or be embedded in existing
+ applications.
diff --git a/docs/source/getting-started/installing-idom.rst b/docs/source/getting-started/installing-idom.rst
new file mode 100644
index 000000000..a9082972d
--- /dev/null
+++ b/docs/source/getting-started/installing-idom.rst
@@ -0,0 +1,114 @@
+Installing IDOM
+===============
+
+The easiest way to ``pip`` install idom is to do so using the ``stable`` option:
+
+.. code-block:: bash
+
+ pip install "idom[stable]"
+
+This includes a set of default dependencies for one of the builtin web server
+implementation. If you want to install IDOM without these dependencies you may simply
+``pip install idom``.
+
+
+Installing Other Servers
+------------------------
+
+IDOM includes built-in support for a variety web server implementations. To install the
+required dependencies for each you should substitute ``stable`` from the ``pip install``
+command above with one of the options below:
+
+- ``fastapi`` - https://fastapi.tiangolo.com
+- ``flask`` - https://palletsprojects.com/p/flask/
+- ``sanic`` - https://sanicframework.org
+- ``tornado`` - https://www.tornadoweb.org/en/stable/
+
+If you need to, you can install more than one option by separating them with commas:
+
+.. code-block:: bash
+
+ pip install idom[fastapi,flask,sanic,tornado]
+
+Once this is complete you should be able to :ref:`run IDOM ` with your
+:ref:`chosen server implementation `.
+
+
+Installing In Other Frameworks
+------------------------------
+
+While IDOM can run in a variety of contexts, sometimes web frameworks require extra work
+in order to integrate with them. In these cases, the IDOM team distributes bindings for
+various frameworks as separate Python packages. For documentation on how to install and
+run IDOM in the supported frameworks, follow the links below:
+
+.. raw:: html
+
+
+
+.. role:: transparent-text-color
+
+.. We add transparent-text-color to the text so it's not visible, but it's still
+.. searchable.
+
+.. grid:: 3
+
+ .. grid-item-card::
+ :link: https://github.com/idom-team/django-idom
+ :img-background: _static/logo-django.svg
+ :class-card: card-logo-image
+
+ :transparent-text-color:`Django`
+
+ .. grid-item-card::
+ :link: https://github.com/idom-team/idom-jupyter
+ :img-background: _static/logo-jupyter.svg
+ :class-card: card-logo-image
+
+ :transparent-text-color:`Jupyter`
+
+ .. grid-item-card::
+ :link: https://github.com/idom-team/idom-dash
+ :img-background: _static/logo-plotly.svg
+ :class-card: card-logo-image
+
+ :transparent-text-color:`Plotly Dash`
+
+
+Installing for Development
+--------------------------
+
+If you want to contribute to the development of IDOM or modify it, you'll want to
+install a development version of IDOM. This involves cloning the repository where IDOM's
+source is maintained, and setting up a :ref:`development environment`. From there you'll
+be able to modifying IDOM's source code and :ref:`run its tests ` to
+ensure the modifications you've made are backwards compatible. If you want to add a new
+feature to IDOM you should write your own test that validates its behavior.
+
+If you have questions about how to modify IDOM or help with its development, be sure to
+`start a discussion
+`__. The IDOM team
+are always excited to :ref:`welcome ` new contributions and
+contributors of all kinds
+
+.. card::
+ :link: /developing-idom/index
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn more about how to contribute to the development of IDOM.
diff --git a/docs/source/getting-started/running-idom.rst b/docs/source/getting-started/running-idom.rst
new file mode 100644
index 000000000..a821117ce
--- /dev/null
+++ b/docs/source/getting-started/running-idom.rst
@@ -0,0 +1,290 @@
+Running IDOM
+============
+
+The simplest way to run IDOM is with the :func:`~idom.server.prefab.run` function. By
+default this will execute your application using one of the builtin server
+implementations whose dependencies have all been installed. Running a tiny "hello world"
+application just requires the following code:
+
+.. idom:: _examples/hello_world
+
+.. note::
+
+ Try clicking the **▶️ Result** tab to see what this displays!
+
+
+Running IDOM in Debug Mode
+--------------------------
+
+IDOM provides a debug mode that is turned off by default. This can be enabled when you
+run your application by setting the ``IDOM_DEBUG_MODE`` environment variable.
+
+.. tab-set::
+
+ .. tab-item:: Unix Shell
+
+ .. code-block::
+
+ export IDOM_DEBUG_MODE=1
+ python my_idom_app.py
+
+ .. tab-item:: Command Prompt
+
+ .. code-block:: text
+
+ set IDOM_DEBUG_MODE=1
+ python my_idom_app.py
+
+ .. tab-item:: PowerShell
+
+ .. code-block:: powershell
+
+ $env:IDOM_DEBUG_MODE = "1"
+ python my_idom_app.py
+
+.. danger::
+
+ Leave debug mode off in production!
+
+Among other things, running in this mode:
+
+- Turns on debug log messages
+- Adds checks to ensure the :ref:`VDOM` spec is adhered to
+- Displays error messages that occur within your app
+
+Errors will be displayed where the uppermost component is located in the view:
+
+.. idom:: _examples/debug_error_example
+
+
+Choosing a Server Implementation
+--------------------------------
+
+Without extra care, running an IDOM app with the ``run()`` function can be somewhat
+inpredictable since the kind of server being used by default depends on what gets
+discovered first. To be more explicit about which server implementation you want to run
+with you can import your chosen server class and pass it to the ``server_type``
+parameter of ``run()``:
+
+.. code-block::
+
+ from idom import component, html, run
+ from idom.server.sanic import PerClientStateServer
+
+
+ @component
+ def App():
+ return html.h1(f"Hello, World!")
+
+
+ run(App, server_type=PerClientStateServer)
+
+Presently IDOM's core library supports the following server implementations:
+
+- :mod:`idom.server.fastapi`
+- :mod:`idom.server.sanic`
+- :mod:`idom.server.flask`
+- :mod:`idom.server.tornado`
+
+.. hint::
+
+ To install them, see the :ref:`Installing Other Servers` section.
+
+
+Available Server Types
+----------------------
+
+Some of server implementations have more than one server type available. The server type
+which exists for all implementations is the ``PerClientStateServer``. This server type
+displays a unique view to each user who visits the site. For those that support it,
+there may also be a ``SharedClientStateServer`` available. This server type presents the
+same view to all users who visit the site. For example, if you were to run the following
+code:
+
+.. code-block::
+
+ from idom import component, hooks, html, run
+ from idom.server.sanic import SharedClientStateServer
+
+
+ @component
+ def Slider():
+ value, set_value = hooks.use_state(50)
+ return html.input({"type": "range", "min": 1, "max": 100, "value": value})
+
+
+ run(Slider, server_type=SharedClientStateServer)
+
+Two clients could see the slider and see a synchronized view of it. That is, when one
+client moved the slider, the other would see the slider update without their action.
+This might look similar to the video below:
+
+.. image:: _static/shared-client-state-server-slider.gif
+
+Presently the following server implementations support the ``SharedClientStateServer``:
+
+- :func:`idom.server.fastapi.SharedClientStateServer`
+- :func:`idom.server.sanic.SharedClientStateServer`
+
+.. note::
+
+ If you need to, your can :ref:`write your own server implementation `.
+
+Common Server Settings
+----------------------
+
+Each server implementation has its own high-level settings that are defined by its
+respective ``Config`` (a typed dictionary). As a general rule, these ``Config`` types
+expose the same options across implementations. These configuration dictionaries can
+then be passed to the ``run()`` function via the ``config`` parameter:
+
+.. code-block::
+
+ from idom import run, component, html
+ from idom.server.sanic import PerClientStateServer, Config
+
+
+ @component
+ def App():
+ return html.h1(f"Hello, World!")
+
+
+ server_config = Config(
+ cors=False,
+ url_prefix="",
+ serve_static_files=True,
+ redirect_root_to_index=True,
+ )
+
+ run(App, server_type=PerClientStateServer, config=server_config)
+
+Here's the list of available configuration types:
+
+- :class:`idom.server.fastapi.Config`
+- :class:`idom.server.sanic.Config`
+- :class:`idom.server.flask.Config`
+- :class:`idom.server.tornado.Config`
+
+
+Specific Server Settings
+------------------------
+
+The ``Config`` :ref:`described above ` is meant to be an
+implementation agnostic - all ``Config`` objects support a similar set of options.
+However, there are inevitably cases where you need to set up your chosen server using
+implementation specific details. For example, you might want to add an extra route to
+the server your using in order to provide extra resources to your application.
+
+Doing this kind of set up can be achieved by passing in an instance of your chosen
+server implementation into the ``app`` parameter of the ``run()`` function. For example,
+if I'm making my application with ``sanic`` and I want to add an extra route I would
+do the following:
+
+.. code-block::
+
+ from sanic import Sanic
+ from idom import component, html, run
+ from idom.server.sanic import PerClientStateServer
+
+ app = Sanic(__name__)
+
+ # these are implementation specific settings only known to `sanic` servers
+ app.config.REQUEST_TIMEOUT = 60
+ app.config.RESPONSE_TIMEOUT = 60
+
+
+ @component
+ def SomeView():
+ return html.form({"action": })
+
+
+ run(SomeView, server_type=PerClientStateServer, app=app)
+
+
+Add to an Existing Web Server
+-----------------------------
+
+If you're already serving an application with one of the supported web servers listed
+above, you can add an IDOM to them as a server extension. Instead of using the ``run()``
+function, you'll instantiate one of IDOM's server implementations by passing it an
+instance of your existing application:
+
+.. code-block::
+
+ from sanic import Sanic
+
+ from idom import component, html
+ from idom.server.sanic import PerClientStateServer, Config
+
+ existing_app = Sanic(__name__)
+
+
+ @component
+ def IdomView():
+ return html.h1("This is an IDOM App")
+
+
+ PerClientStateServer(IdomView, app=existing_app, config=Config(url_prefix="app"))
+
+ existing_app.run(host="127.0.0.1", port=8000)
+
+To test that everything is working, you should be able to navigate to
+``https://127.0.0.1:8000/app`` where you should see the results from ``IdomView``.
+
+
+Embed in an Existing Webpage
+----------------------------
+
+IDOM provides a Javascript client called ``idom-client-react`` that can be used to embed
+IDOM views within an existing applications. This is actually how the interactive
+examples throughout this documentation have been created. You can try this out by
+embedding one the examples from this documentation into your own webpage:
+
+.. tab-set::
+
+ .. tab-item:: HTML
+
+ .. literalinclude:: _static/embed-doc-ex.html
+ :language: html
+
+ .. tab-item:: ▶️ Result
+
+ .. raw:: html
+ :file: _static/embed-doc-ex.html
+
+.. note::
+
+ For more information on how to use the client see the :ref:`Javascript API`
+ reference. Or if you need to, your can :ref:`write your own server implementation
+ `.
+
+As mentioned though, this is connecting to the server that is hosting this
+documentation. If you want to connect to a view from your own server, you'll need to
+change the URL above to one you provide. One way to do this might be to :ref:`add to an
+existing web server`. Another would be to run IDOM in an adjacent web server instance
+that you coordinate with something like `NGINX `__. For the sake
+of simplicity, we'll assume you do something similar to the following in an existing
+Python app:
+
+.. tab-set::
+
+ .. tab-item:: main.py
+
+ .. literalinclude:: _static/embed-idom-view/main.py
+ :language: python
+
+ .. tab-item:: index.html
+
+ .. literalinclude:: _static/embed-idom-view/index.html
+ :language: html
+
+After running ``python main.py``, you should be able to navigate to
+``http://127.0.0.1:8000/index.html`` and see:
+
+.. card::
+ :text-align: center
+
+ .. image:: _static/embed-idom-view/screenshot.png
+ :width: 500px
+
diff --git a/docs/source/handling-events.rst b/docs/source/handling-events.rst
deleted file mode 100644
index ed82ea909..000000000
--- a/docs/source/handling-events.rst
+++ /dev/null
@@ -1,62 +0,0 @@
-Handling Events
-===============
-
-When :ref:`Getting Started`, we saw how IDOM makes it possible to write server-side code
-that defines basic views and can react to client-side events. The simplest way to listen
-and respond to events is by assigning a callable object to a :ref:`VDOM`
-an attribute where event signals are sent. This is relatively similar to
-`handling events in React`_:
-
-.. _handling events in React: https://reactjs.org/docs/handling-events.html
-
-.. example:: show_click_event
-
-
-Differences With React Events
------------------------------
-
-Because IDOM operates server-side, there are inevitable limitations that prevent it from
-achieving perfect parity with all the behaviors of React. With that said, any feature
-that cannot be achieved in Python with IDOM, can be done by creating
-:ref:`Custom Javascript Components`.
-
-
-Preventing Default Event Actions
-................................
-
-Instead of calling an ``event.preventDefault()`` method as you would do in React, you
-must declare whether to prevent default behavior ahead of time. This can be accomplished
-using the :func:`~idom.core.events.event` decorator and setting ``prevent_default``. For
-example, we can stop a link from going to the specified URL:
-
-.. example:: prevent_default_event_actions
-
-Unfortunately this means you cannot conditionally prevent default behavior in response
-to event data without writing :ref:`Custom Javascript Components`.
-
-
-Stop Event Propogation
-......................
-
-Similarly to :ref:`preventing default behavior `, you
-can use the :func:`~idom.core.events.event` decorator to forward declare whether or not
-you want events from a child element propogate up through the document to parent
-elements by setting ``stop_propagation``. In the example below we place a red ``div``
-inside a parent blue ``div``. When propogation is turned on, clicking the red element
-will cause the handler for the outer blue one to fire. Conversely, when it's off, only
-the handler for the red element will fire.
-
-.. example:: stop_event_propagation
-
-
-Event Data Serialization
-........................
-
-Not all event data is serialized. The most notable example of this is the lack of a
-``target`` key in the dictionary sent back to the handler. Instead, data which is not
-inherhently JSON serializable must be treated on a case-by-case basis. A simple case
-to demonstrate this is the ``currentTime`` attribute of ``audio`` and ``video``
-elements. Normally this would be accessible via ``event.target.currenTime``, but here
-it's simply passed in under the key ``currentTime``:
-
-.. example:: audio_player
diff --git a/docs/source/index.rst b/docs/source/index.rst
index a83bd26a2..fe73e66a6 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,114 +1,205 @@
-IDOM
-====
+.. card::
-.. toctree::
- :hidden:
- :caption: User Guide
+ This documentation is still under construction 🚧. We welcome your `feedback
+ `__!
- installation
- getting-started
- handling-events
- life-cycle-hooks
- auto/api-reference
- examples
- faq
+
+What is IDOM?
+=============
.. toctree::
:hidden:
- :caption: Advanced Topics
+ :caption: User Guide
- core-abstractions
- javascript-components
- architectural-patterns
- specifications
+ getting-started/index
+ creating-interfaces/index
+ adding-interactivity/index
+ managing-state/index
+ escape-hatches/index
+ understanding-idom/index
.. toctree::
:hidden:
- :caption: Development
+ :caption: Other Resources
- contributing
- developer-guide
- auto/developer-apis
- changelog
- roadmap
+ developing-idom/index
+ reference-material/index
+ credits-and-licenses
.. toctree::
:hidden:
- :caption: External Resources
+ :caption: External Links
Source Code
Community
Issues
-A package for building responsive user interfaces in pure Python.
-- Create full stack applications without writing a single line of Javascript.
-- Rapidly and easily develop :ref:`interactive data dashboards `.
-- Use many existing Javascript packages without any extra work.
-- Leverage time-tested :ref:`declarative ` design patterns
- inspired by `ReactJS `__.
-- Write :ref:`custom Javascript components` when you need client-side performance.
-- Is :ref:`ecosystem independent ` - works with
- `Jupyter `__,
- `Dash `__,
- `Django `__, and more.
-- Add to existing applications with the
- `Javascript client `__.
+IDOM is a Python web framework for building **interactive websites without needing a
+single line of Javascript**. This is accomplished by breaking down complex applications
+into nestable and reusable chunks of code called :ref:`"components" ` that allow you to focus on what your application does rather than how it
+does it.
-.. grid:: 1 1 2 2
- :gutter: 1
+IDOM is also ecosystem independent. It can be added to existing applications built on a
+variety of sync and async web servers, as well as integrated with other frameworks like
+Django, Jupyter, and Plotly Dash. Not only does this mean you're free to choose what
+technology stack to run on, but on top of that, you can run the exact same components
+wherever you need them. Consider the case where, you can take a component originally
+developed in a Jupyter Notebook and embed it in your production application without
+changing anything about the component itself.
- .. grid-item::
- .. grid:: 1 1 1 1
- :gutter: 1
+At a Glance
+-----------
+
+To get a rough idea of how to write apps in IDOM, take a look at the tiny `"hello world"
+`__ application below:
+
+.. idom:: getting-started/_examples/hello_world
- .. grid-item-card::
+.. hint::
- .. interactive-widget:: pigeon_maps
- :no-activate-button:
+ Try clicking the **▶️ result** tab to see what this displays!
- .. grid-item-card::
+So what exactly does this code do? First, it imports a few tools from ``idom`` that will
+get used to describe and execute an application. Then, we create an ``App`` function
+which will define the content the application displays. Specifically, it displays a kind
+of HTML element called an ``h1`` `section heading
+`__.
+Importantly though, a ``@component`` decorator has been applied to the ``App`` function
+to turn it into a :ref:`component `. Finally, we :ref:`run
+` an application server by passing the ``App`` component to the ``run()``
+function.
- .. interactive-widget:: network_graph
- :no-activate-button:
- .. grid-item-card::
+Learning IDOM
+-------------
- .. interactive-widget:: snake_game
- :no-activate-button:
+This documentation is broken up into chapters and sections that introduce you to
+concepts step by step with detailed explanations and lots of examples. You should feel
+free to dive into any content that seems interesting. While each chapter assumes
+knowledge from those that came before, when you encounter a concept you're unfamiliar
+with you should look for links that will help direct you to the place where it was
+originally taught.
- .. grid-item-card::
- .. interactive-widget:: slideshow
- :no-activate-button:
+Chapter 1 - :ref:`Getting Started`
+-----------------------------------
- .. grid-item-card::
+If you want to follow along with examples in the sections that follow, you'll want to
+start here so you can :ref:`install IDOM `. This section also contains
+more detailed information about how to :ref:`run IDOM ` in different
+contexts. For example, if you want to embed IDOM into an existing application, or run
+IDOM within a Jupyter Notebook, this is where you can learn how to do those things.
- .. interactive-widget:: audio_player
- :no-activate-button:
+.. grid:: 2
.. grid-item::
- .. grid:: 1 1 1 1
- :gutter: 1
+ .. image:: _static/install-and-run-idom.gif
+
+ .. grid-item::
+
+ .. image:: getting-started/_static/idom-in-jupyterlab.gif
+
+.. card::
+ :link: getting-started/index
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Install IDOM and run it in a variety of different ways - with different web servers
+ and frameworks. You'll even embed IDOM into an existing app.
+
+
+Chapter 2 - :ref:`Creating Interfaces`
+--------------------------------------
+
+IDOM is a Python package for making user interfaces (UI). These interfaces are built
+from small elements of functionality like buttons text and images. IDOM allows you to
+combine these elements into reusable :ref:`"components" `. In the
+sections that follow you'll learn how these UI elements are created and organized into
+components. Then, you'll use this knowledge to create interfaces from raw data:
+
+.. idom:: creating-interfaces/_examples/todo_list_with_keys
+
+.. card::
+ :link: creating-interfaces/index
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn to construct user interfaces from basic HTML elements and reusable components.
+
+
+Chapter 3 - :ref:`Adding Interactivity`
+---------------------------------------
+
+Components often need to change what’s on the screen as a result of an interaction. For
+example, typing into the form should update the input field, clicking a “Comment” button
+should bring up a text input field, clicking “Buy” should put a product in the shopping
+cart. Components need to “remember” things like the current input value, the current
+image, the shopping cart. In IDOM, this kind of component-specific memory is created and
+updated with a "hook" called ``use_state()`` that creates a **state variable** and
+**state setter** respectively:
+
+.. idom:: adding-interactivity/_examples/adding_state_variable
+
+In IDOM, ``use_state``, as well as any other function whose name starts with ``use``, is
+called a "hook". These are special functions that should only be called while IDOM is
+:ref:`rendering `. They let you "hook into" the different
+capabilities of IDOM's components of which ``use_state`` is just one (well get into the
+other :ref:`later `).
+
+.. card::
+ :link: adding-interactivity/index
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Learn how user interfaces can be made to respond to user interaction in real-time.
+
+
+Chapter 4 - :ref:`Managing State`
+---------------------------------
+
+.. card::
+ :link: managing-state/index
+ :link-type: doc
+
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Under construction 🚧
+
+
+
+Chapter 5 - :ref:`Escape Hatches`
+---------------------------------
+
+.. card::
+ :link: escape-hatches/index
+ :link-type: doc
- .. grid-item-card::
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
- .. interactive-widget:: simple_dashboard
- :no-activate-button:
+ Under construction 🚧
- .. grid-item-card::
- .. interactive-widget:: matplotlib_plot
- :no-activate-button:
+Chapter 6 - :ref:`Understanding IDOM`
+-------------------------------------
- .. grid-item-card::
+.. card::
+ :link: escape-hatches/index
+ :link-type: doc
- .. interactive-widget:: material_ui_button_on_click
- :no-activate-button:
+ :octicon:`book` Read More
+ ^^^^^^^^^^^^^^^^^^^^^^^^^
- .. grid-item-card::
+ Under construction 🚧
- .. interactive-widget:: todo
- :no-activate-button:
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
deleted file mode 100644
index 2b1ab6f2a..000000000
--- a/docs/source/installation.rst
+++ /dev/null
@@ -1,59 +0,0 @@
-Installation
-============
-
-To install IDOM and a default implementation for all features:
-
-.. code-block:: bash
-
- pip install idom[stable]
-
-For a minimal installation that lacks implementations for some features:
-
-.. code-block:: bash
-
- pip install idom
-
-For more installation options see the :ref:`Extra Features` section.
-
-.. note::
-
- IDOM also supplies a :ref:`Flake8 Plugin` to help enforce the :ref:`Rules of Hooks`
-
-
-Extra Features
---------------
-
-Optionally installable features of IDOM. To include, them use the given "Name" from the
-table below:
-
-.. code-block:: bash
-
- pip install idom[NAME]
-
-.. list-table::
- :header-rows: 1
- :widths: 1 3
-
- * - Name
- - Description
-
- * - ``stable``
- - Default implementations for all IDOM's features
-
- * - ``testing``
- - Utilities for testing IDOM using `Selenium `__
-
- * - ``sanic``
- - `Sanic `__ as a :ref:`Layout Server`
-
- * - ``fastapi``
- - `FastAPI `__ as a :ref:`Layout Server`
-
- * - ``tornado``
- - `Tornado `__ as a :ref:`Layout Server`
-
- * - ``flask``
- - `Flask `__ as a :ref:`Layout Server`
-
- * - ``all``
- - All the features listed above (not usually needed)
diff --git a/docs/source/managing-state/index.rst b/docs/source/managing-state/index.rst
new file mode 100644
index 000000000..4ef9850ac
--- /dev/null
+++ b/docs/source/managing-state/index.rst
@@ -0,0 +1,14 @@
+Managing State
+==============
+
+.. toctree::
+ :hidden:
+
+ keeping-components-pure
+ logical-flow-of-state
+ structuring-your-state
+ shared-component-state
+ when-to-reset-state
+ writing-tests
+
+Under construction 🚧
diff --git a/docs/source/managing-state/keeping-components-pure.rst b/docs/source/managing-state/keeping-components-pure.rst
new file mode 100644
index 000000000..a2fc1a15b
--- /dev/null
+++ b/docs/source/managing-state/keeping-components-pure.rst
@@ -0,0 +1,8 @@
+.. _Keeping Components Pure:
+
+Keeping Components Pure 🚧
+==========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/managing-state/logical-flow-of-state.rst b/docs/source/managing-state/logical-flow-of-state.rst
new file mode 100644
index 000000000..53bf0cff9
--- /dev/null
+++ b/docs/source/managing-state/logical-flow-of-state.rst
@@ -0,0 +1,8 @@
+.. _Logical Flow of State:
+
+Logical Flow of State 🚧
+========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/managing-state/shared-component-state.rst b/docs/source/managing-state/shared-component-state.rst
new file mode 100644
index 000000000..3c9f66617
--- /dev/null
+++ b/docs/source/managing-state/shared-component-state.rst
@@ -0,0 +1,8 @@
+.. _Shared Component State:
+
+Shared Component State 🚧
+=========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/managing-state/structuring-your-state.rst b/docs/source/managing-state/structuring-your-state.rst
new file mode 100644
index 000000000..68209cccf
--- /dev/null
+++ b/docs/source/managing-state/structuring-your-state.rst
@@ -0,0 +1,8 @@
+.. _Structuring Your State:
+
+Structuring Your State 🚧
+=========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/managing-state/when-to-reset-state.rst b/docs/source/managing-state/when-to-reset-state.rst
new file mode 100644
index 000000000..2a0b8c3ae
--- /dev/null
+++ b/docs/source/managing-state/when-to-reset-state.rst
@@ -0,0 +1,8 @@
+.. _When to Reset State:
+
+When to Reset State 🚧
+======================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/managing-state/writing-tests.rst b/docs/source/managing-state/writing-tests.rst
new file mode 100644
index 000000000..ffac27df6
--- /dev/null
+++ b/docs/source/managing-state/writing-tests.rst
@@ -0,0 +1,8 @@
+.. _Writing Tests:
+
+Writing Tests 🚧
+================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/reference-material/_examples/character_movement/app.py b/docs/source/reference-material/_examples/character_movement/app.py
new file mode 100644
index 000000000..fbf257a32
--- /dev/null
+++ b/docs/source/reference-material/_examples/character_movement/app.py
@@ -0,0 +1,70 @@
+from pathlib import Path
+from typing import NamedTuple
+
+from idom import component, html, run, use_state
+from idom.widgets import image
+
+
+HERE = Path(__file__)
+CHARACTER_IMAGE = (HERE.parent / "static" / "bunny.png").read_bytes()
+
+
+class Position(NamedTuple):
+ x: int
+ y: int
+ angle: int
+
+
+def rotate(degrees):
+ return lambda old_position: Position(
+ old_position.x,
+ old_position.y,
+ old_position.angle + degrees,
+ )
+
+
+def translate(x=0, y=0):
+ return lambda old_position: Position(
+ old_position.x + x,
+ old_position.y + y,
+ old_position.angle,
+ )
+
+
+@component
+def Scene():
+ position, set_position = use_state(Position(100, 100, 0))
+
+ return html.div(
+ {"style": {"width": "225px"}},
+ html.div(
+ {
+ "style": {
+ "width": "200px",
+ "height": "200px",
+ "backgroundColor": "slategray",
+ }
+ },
+ image(
+ "png",
+ CHARACTER_IMAGE,
+ {
+ "style": {
+ "position": "relative",
+ "left": f"{position.x}px",
+ "top": f"{position.y}.px",
+ "transform": f"rotate({position.angle}deg) scale(2, 2)",
+ }
+ },
+ ),
+ ),
+ html.button({"onClick": lambda e: set_position(translate(x=-10))}, "Move Left"),
+ html.button({"onClick": lambda e: set_position(translate(x=10))}, "Move Right"),
+ html.button({"onClick": lambda e: set_position(translate(y=-10))}, "Move Up"),
+ html.button({"onClick": lambda e: set_position(translate(y=10))}, "Move Down"),
+ html.button({"onClick": lambda e: set_position(rotate(-30))}, "Rotate Left"),
+ html.button({"onClick": lambda e: set_position(rotate(30))}, "Rotate Right"),
+ )
+
+
+run(Scene)
diff --git a/docs/source/reference-material/_examples/character_movement/static/bunny.png b/docs/source/reference-material/_examples/character_movement/static/bunny.png
new file mode 100644
index 000000000..ce1f989c5
Binary files /dev/null and b/docs/source/reference-material/_examples/character_movement/static/bunny.png differ
diff --git a/docs/source/examples/click_count.py b/docs/source/reference-material/_examples/click_count.py
similarity index 100%
rename from docs/source/examples/click_count.py
rename to docs/source/reference-material/_examples/click_count.py
diff --git a/docs/source/examples/material_ui_switch.py b/docs/source/reference-material/_examples/material_ui_switch.py
similarity index 100%
rename from docs/source/examples/material_ui_switch.py
rename to docs/source/reference-material/_examples/material_ui_switch.py
diff --git a/docs/source/examples/matplotlib_plot.py b/docs/source/reference-material/_examples/matplotlib_plot.py
similarity index 100%
rename from docs/source/examples/matplotlib_plot.py
rename to docs/source/reference-material/_examples/matplotlib_plot.py
diff --git a/docs/source/examples/network_graph.py b/docs/source/reference-material/_examples/network_graph.py
similarity index 100%
rename from docs/source/examples/network_graph.py
rename to docs/source/reference-material/_examples/network_graph.py
diff --git a/docs/source/examples/pigeon_maps.py b/docs/source/reference-material/_examples/pigeon_maps.py
similarity index 100%
rename from docs/source/examples/pigeon_maps.py
rename to docs/source/reference-material/_examples/pigeon_maps.py
diff --git a/docs/source/examples/simple_dashboard.py b/docs/source/reference-material/_examples/simple_dashboard.py
similarity index 97%
rename from docs/source/examples/simple_dashboard.py
rename to docs/source/reference-material/_examples/simple_dashboard.py
index 9b8d76da3..540082f58 100644
--- a/docs/source/examples/simple_dashboard.py
+++ b/docs/source/reference-material/_examples/simple_dashboard.py
@@ -84,7 +84,7 @@ def update_value(value):
return idom.html.fieldset(
{"class": "number-input-container"},
- [idom.html.legend({"style": {"font-size": "medium"}}, label)],
+ idom.html.legend({"style": {"font-size": "medium"}}, label),
Input(update_value, "number", value, attributes=attrs, cast=float),
Input(update_value, "range", value, attributes=attrs, cast=float),
)
diff --git a/docs/source/examples/slideshow.py b/docs/source/reference-material/_examples/slideshow.py
similarity index 100%
rename from docs/source/examples/slideshow.py
rename to docs/source/reference-material/_examples/slideshow.py
diff --git a/docs/source/examples/snake_game.py b/docs/source/reference-material/_examples/snake_game.py
similarity index 100%
rename from docs/source/examples/snake_game.py
rename to docs/source/reference-material/_examples/snake_game.py
diff --git a/docs/source/examples/todo.py b/docs/source/reference-material/_examples/todo.py
similarity index 100%
rename from docs/source/examples/todo.py
rename to docs/source/reference-material/_examples/todo.py
diff --git a/docs/source/examples/use_reducer_counter.py b/docs/source/reference-material/_examples/use_reducer_counter.py
similarity index 100%
rename from docs/source/examples/use_reducer_counter.py
rename to docs/source/reference-material/_examples/use_reducer_counter.py
diff --git a/docs/source/examples/use_state_counter.py b/docs/source/reference-material/_examples/use_state_counter.py
similarity index 100%
rename from docs/source/examples/use_state_counter.py
rename to docs/source/reference-material/_examples/use_state_counter.py
diff --git a/docs/source/examples/victory_chart.py b/docs/source/reference-material/_examples/victory_chart.py
similarity index 100%
rename from docs/source/examples/victory_chart.py
rename to docs/source/reference-material/_examples/victory_chart.py
diff --git a/docs/source/reference-material/_static/vdom-json-schema.json b/docs/source/reference-material/_static/vdom-json-schema.json
new file mode 100644
index 000000000..d3ab5bd6d
--- /dev/null
+++ b/docs/source/reference-material/_static/vdom-json-schema.json
@@ -0,0 +1,122 @@
+{
+ "$ref": "#/definitions/element",
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "definitions": {
+ "element": {
+ "dependentSchemas": {
+ "error": {
+ "properties": {
+ "tagName": {
+ "maxLength": 0
+ }
+ }
+ }
+ },
+ "properties": {
+ "attributes": {
+ "type": "object"
+ },
+ "children": {
+ "$ref": "#/definitions/elementChildren"
+ },
+ "error": {
+ "type": "string"
+ },
+ "eventHandlers": {
+ "$ref": "#/definitions/elementEventHandlers"
+ },
+ "importSource": {
+ "$ref": "#/definitions/importSource"
+ },
+ "key": {
+ "type": "string"
+ },
+ "tagName": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "tagName"
+ ],
+ "type": "object"
+ },
+ "elementChildren": {
+ "items": {
+ "$ref": "#/definitions/elementOrString"
+ },
+ "type": "array"
+ },
+ "elementEventHandlers": {
+ "patternProperties": {
+ ".*": {
+ "$ref": "#/definitions/eventHander"
+ }
+ },
+ "type": "object"
+ },
+ "elementOrString": {
+ "if": {
+ "type": "object"
+ },
+ "then": {
+ "$ref": "#/definitions/element"
+ },
+ "type": [
+ "object",
+ "string"
+ ]
+ },
+ "eventHander": {
+ "properties": {
+ "preventDefault": {
+ "type": "boolean"
+ },
+ "stopPropagation": {
+ "type": "boolean"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+ },
+ "importSource": {
+ "properties": {
+ "fallback": {
+ "if": {
+ "not": {
+ "type": "null"
+ }
+ },
+ "then": {
+ "$ref": "#/definitions/elementOrString"
+ },
+ "type": [
+ "object",
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": "string"
+ },
+ "sourceType": {
+ "enum": [
+ "URL",
+ "NAME"
+ ]
+ },
+ "unmountBeforeUpdate": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "type": "object"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/source/reference-material/browser-events.rst b/docs/source/reference-material/browser-events.rst
new file mode 100644
index 000000000..632be410a
--- /dev/null
+++ b/docs/source/reference-material/browser-events.rst
@@ -0,0 +1,65 @@
+.. _Browser Events:
+
+Browser Events 🚧
+=================
+
+The event types below are triggered by an event in the bubbling phase. To register an
+event handler for the capture phase, append Capture to the event name; for example,
+instead of using ``onClick``, you would use ``onClickCapture`` to handle the click event
+in the capture phase.
+
+.. note::
+
+ Under construction 🚧
+
+
+Clipboard Events
+----------------
+
+Composition Events
+------------------
+
+Keyboard Events
+---------------
+
+Focus Events
+------------
+
+Form Events
+-----------
+
+Generic Events
+--------------
+
+Mouse Events
+------------
+
+Pointer Events
+--------------
+
+Selection Events
+----------------
+
+Touch Events
+------------
+
+UI Events
+---------
+
+Wheel Events
+------------
+
+Media Events
+------------
+
+Image Events
+------------
+
+Animation Events
+----------------
+
+Transition Events
+-----------------
+
+Other Events
+------------
diff --git a/docs/source/faq.rst b/docs/source/reference-material/faq.rst
similarity index 96%
rename from docs/source/faq.rst
rename to docs/source/reference-material/faq.rst
index 5b23978d4..df1628dcd 100644
--- a/docs/source/faq.rst
+++ b/docs/source/reference-material/faq.rst
@@ -1,8 +1,7 @@
FAQ
===
-See our `Discussion Forum `__ for more
-questions and answers.
+See our :discussion-type:`Discussion Forum ` for more questions and answers.
Do UI components run client-side?
diff --git a/docs/source/examples.rst b/docs/source/reference-material/gallery.rst
similarity index 58%
rename from docs/source/examples.rst
rename to docs/source/reference-material/gallery.rst
index 5e0b5cebf..09dcd0ead 100644
--- a/docs/source/examples.rst
+++ b/docs/source/reference-material/gallery.rst
@@ -1,19 +1,20 @@
-Examples
-========
-
+Gallery
+=======
Slideshow
---------
Try clicking the image 🖱️
-.. example:: slideshow
+.. idom:: _examples/slideshow
+ :result-is-default-tab:
Click Counter
-------------
-.. example:: click_count
+.. idom:: _examples/click_count
+ :result-is-default-tab:
To Do List
@@ -21,7 +22,15 @@ To Do List
Try typing in the text box and pressing 'Enter' 📋
-.. example:: todo
+.. idom:: _examples/todo
+ :result-is-default-tab:
+
+
+Simple Image Movement
+---------------------
+
+.. idom:: _examples/character_movement
+ :result-is-default-tab:
The Game Snake
@@ -31,7 +40,8 @@ Click to start playing and use the arrow keys to move 🎮
Slow internet may cause inconsistent frame pacing 😅
-.. example:: snake_game
+.. idom:: _examples/snake_game
+ :result-is-default-tab:
Matplotlib Plot
@@ -39,7 +49,8 @@ Matplotlib Plot
Pick the polynomial coefficients (separate each coefficient by a space) 🔢:
-.. example:: matplotlib_plot
+.. idom:: _examples/matplotlib_plot
+ :result-is-default-tab:
Simple Dashboard
@@ -47,7 +58,8 @@ Simple Dashboard
Try interacting with the sliders 📈
-.. example:: simple_dashboard
+.. idom:: _examples/simple_dashboard
+ :result-is-default-tab:
Dynamically Loaded React Components
@@ -56,15 +68,8 @@ Dynamically Loaded React Components
This method is not recommended for use in production applications, but it's great while
you're experimenting:
-.. example:: victory_chart
-
-
-Define Javascript Modules
--------------------------
-
-Shows a very simple chart implemented in vanilla Javascript:
-
-.. example:: super_simple_chart
+.. idom:: _examples/victory_chart
+ :result-is-default-tab:
Material UI Button
@@ -72,7 +77,8 @@ Material UI Button
Click the button to change the indicator 👇
-.. example:: material_ui_switch
+.. idom:: _examples/material_ui_switch
+ :result-is-default-tab:
Pigeon Maps
@@ -80,15 +86,17 @@ Pigeon Maps
Click the map to create pinned location 📍:
-.. example:: pigeon_maps
+.. idom:: _examples/pigeon_maps
+ :result-is-default-tab:
-Cytoscape Notework Graph
-------------------------
+Cytoscape Network Graph
+-----------------------
You can move the nodes in the graph 🕸️:
-.. example:: network_graph
+.. idom:: _examples/network_graph
+ :result-is-default-tab:
.. Links
diff --git a/docs/source/life-cycle-hooks.rst b/docs/source/reference-material/hooks-api.rst
similarity index 98%
rename from docs/source/life-cycle-hooks.rst
rename to docs/source/reference-material/hooks-api.rst
index 9060203d5..15b1ccc8a 100644
--- a/docs/source/life-cycle-hooks.rst
+++ b/docs/source/reference-material/hooks-api.rst
@@ -1,6 +1,6 @@
-================
-Life Cycle Hooks
-================
+=========
+Hooks API
+=========
Hooks are functions that allow you to "hook into" the life cycle events and state of
Components. Their usage should always follow the :ref:`Rules of Hooks`. For most use
@@ -55,7 +55,7 @@ accepts a single argument (the previous state) and returns the next state. Consi
simply use case of a counter where we've pulled out logic for increment and
decremented the count:
-.. example:: use_state_counter
+.. idom:: _examples/use_state_counter
We use the functional form for the "+" and "-" buttons since the next ``count`` depends
on the previous value, while for the "Reset" button we simple assign the
@@ -207,7 +207,7 @@ may be slightly more performant as well as being preferable since there is only
We can rework the :ref:`Functional Updates` counter example to use ``use_reducer``:
-.. example:: use_reducer_counter
+.. idom:: _examples/use_reducer_counter
.. note::
@@ -289,6 +289,14 @@ hook alongside :ref:`Use Effect` or in response to component event handlers.
:ref:`The Game Snake` provides a good use case for ``use_ref``.
+.. links
+.. =====
+
+.. _React Hooks: https://reactjs.org/docs/hooks-reference.html
+.. _side effects: https://en.wikipedia.org/wiki/Side_effect_(computer_science)
+.. _memoization: https://en.wikipedia.org/wiki/Memoization
+
+
Rules of Hooks
==============
@@ -307,8 +315,8 @@ IDOM to preserve the state of hooks between multiple calls to ``useState`` and
``useEffect`` calls.
-Only call in IDOM functions
----------------------------
+Only call in render functions
+-----------------------------
**Don't call hooks from regular Python functions.** Instead you should:
@@ -350,7 +358,4 @@ See the Flake8 docs for
.. links
.. =====
-.. _React Hooks: https://reactjs.org/docs/hooks-reference.html
-.. _side effects: https://en.wikipedia.org/wiki/Side_effect_(computer_science)
-.. _memoization: https://en.wikipedia.org/wiki/Memoization
.. _Flake8 Linter Plugin: https://github.com/idom-team/flake8-idom-hooks
diff --git a/docs/source/reference-material/index.rst b/docs/source/reference-material/index.rst
new file mode 100644
index 000000000..ad792e444
--- /dev/null
+++ b/docs/source/reference-material/index.rst
@@ -0,0 +1,17 @@
+Reference Material
+==================
+
+.. toctree::
+ :hidden:
+
+ gallery
+ hooks-api
+ javascript-api
+ browser-events
+ /_autogen/user-apis
+ specifications
+ faq
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/reference-material/javascript-api.rst b/docs/source/reference-material/javascript-api.rst
new file mode 100644
index 000000000..2587be82d
--- /dev/null
+++ b/docs/source/reference-material/javascript-api.rst
@@ -0,0 +1,8 @@
+.. _Javascript API:
+
+Javascript API 🚧
+=================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/specifications.rst b/docs/source/reference-material/specifications.rst
similarity index 98%
rename from docs/source/specifications.rst
rename to docs/source/reference-material/specifications.rst
index e479d4854..c2f9f68e4 100644
--- a/docs/source/specifications.rst
+++ b/docs/source/reference-material/specifications.rst
@@ -153,7 +153,7 @@ VDOM JSON Schema
To clearly describe the VDOM spec we've created a `JSON Schema `_:
-.. literalinclude:: ./vdom-json-schema.json
+.. literalinclude:: _static/vdom-json-schema.json
:language: json
@@ -162,7 +162,7 @@ JSON Patch
Updates to VDOM modules are sent using the `JSON Patch`_ specification.
-... this section is still under construction :)
+... this section is still Under construction 🚧
.. Links
diff --git a/docs/source/_static/idom-flow-diagram.svg b/docs/source/understanding-idom/_static/idom-flow-diagram.svg
similarity index 100%
rename from docs/source/_static/idom-flow-diagram.svg
rename to docs/source/understanding-idom/_static/idom-flow-diagram.svg
diff --git a/docs/source/_static/live-examples-in-docs.gif b/docs/source/understanding-idom/_static/live-examples-in-docs.gif
similarity index 100%
rename from docs/source/_static/live-examples-in-docs.gif
rename to docs/source/understanding-idom/_static/live-examples-in-docs.gif
diff --git a/docs/source/_static/mvc-flow-diagram.svg b/docs/source/understanding-idom/_static/mvc-flow-diagram.svg
similarity index 100%
rename from docs/source/_static/mvc-flow-diagram.svg
rename to docs/source/understanding-idom/_static/mvc-flow-diagram.svg
diff --git a/docs/source/_static/npm-download-trends.png b/docs/source/understanding-idom/_static/npm-download-trends.png
similarity index 100%
rename from docs/source/_static/npm-download-trends.png
rename to docs/source/understanding-idom/_static/npm-download-trends.png
diff --git a/docs/source/understanding-idom/index.rst b/docs/source/understanding-idom/index.rst
new file mode 100644
index 000000000..5c1b94231
--- /dev/null
+++ b/docs/source/understanding-idom/index.rst
@@ -0,0 +1,12 @@
+Understanding IDOM
+==================
+
+.. toctree::
+ :hidden:
+
+ representing-html
+ what-are-components
+ the-rendering-pipeline
+ why-idom-needs-keys
+ the-rendering-process
+ layout-render-servers
diff --git a/docs/source/understanding-idom/layout-render-servers.rst b/docs/source/understanding-idom/layout-render-servers.rst
new file mode 100644
index 000000000..9a7cceb54
--- /dev/null
+++ b/docs/source/understanding-idom/layout-render-servers.rst
@@ -0,0 +1,8 @@
+.. _Layout Render Servers:
+
+Layout Render Servers 🚧
+========================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/understanding-idom/representing-html.rst b/docs/source/understanding-idom/representing-html.rst
new file mode 100644
index 000000000..ffae0d331
--- /dev/null
+++ b/docs/source/understanding-idom/representing-html.rst
@@ -0,0 +1,76 @@
+.. _Representing HTML:
+
+Representing HTML 🚧
+====================
+
+.. note::
+
+ Under construction 🚧
+
+We've already discussed how to contruct HTML with IDOM in a :ref:`previous section `, but we skimmed over the question of the data structure we use to represent
+it. Let's reconsider the examples from before - on the top is some HTML and on the
+bottom is the corresponding code to create it in IDOM:
+
+.. code-block:: html
+
+
+
My Todo List
+
+ Build a cool new app
+ Share it with the world!
+
+
+
+.. testcode::
+
+ from idom import html
+
+ layout = html.div(
+ html.h1("My Todo List"),
+ html.ul(
+ html.li("Build a cool new app"),
+ html.li("Share it with the world!"),
+ )
+ )
+
+Since we've captured our HTML into out the ``layout`` variable, we can inspect what it
+contains. And, as it turns out, it holds a dictionary. Printing it produces the
+following output:
+
+.. testsetup::
+
+ from pprint import pprint
+ print = lambda *args, **kwargs: pprint(*args, **kwargs, sort_dicts=False)
+
+.. testcode::
+
+ assert layout == {
+ 'tagName': 'div',
+ 'children': [
+ {
+ 'tagName': 'h1',
+ 'children': ['My Todo List']
+ },
+ {
+ 'tagName': 'ul',
+ 'children': [
+ {'tagName': 'li', 'children': ['Build a cool new app']},
+ {'tagName': 'li', 'children': ['Share it with the world!']}
+ ]
+ }
+ ]
+ }
+
+This may look complicated, but let's take a moment to consider what's going on here. We
+have a series of nested dictionaries that, in some way, represents the HTML structure
+given above. If we look at their contents we should see a common form. Each has a
+``tagName`` key which contains, as the name would suggest, the tag name of an HTML
+element. Then within the ``children`` key is a list that either contains strings or
+other dictionaries that represent HTML elements.
+
+What we're seeing here is called a "virtual document object model" or :ref:`VDOM`. This
+is just a fancy way of saying we have a representation of the document object model or
+`DOM
+`__
+that is not the actual DOM.
diff --git a/docs/source/understanding-idom/the-rendering-pipeline.rst b/docs/source/understanding-idom/the-rendering-pipeline.rst
new file mode 100644
index 000000000..cdde27f08
--- /dev/null
+++ b/docs/source/understanding-idom/the-rendering-pipeline.rst
@@ -0,0 +1,10 @@
+.. _The Rendering Pipeline:
+
+The Rendering Pipeline 🚧
+=========================
+
+.. talk about layouts and dispatchers
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/understanding-idom/the-rendering-process.rst b/docs/source/understanding-idom/the-rendering-process.rst
new file mode 100644
index 000000000..00215a887
--- /dev/null
+++ b/docs/source/understanding-idom/the-rendering-process.rst
@@ -0,0 +1,10 @@
+.. _The Rendering Process:
+
+The Rendering Process 🚧
+========================
+
+.. refer to https://beta.reactjs.org/learn/render-and-commit
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/understanding-idom/what-are-components.rst b/docs/source/understanding-idom/what-are-components.rst
new file mode 100644
index 000000000..4c22dda13
--- /dev/null
+++ b/docs/source/understanding-idom/what-are-components.rst
@@ -0,0 +1,8 @@
+.. _What Are Components:
+
+What Are Components? 🚧
+=======================
+
+.. note::
+
+ Under construction 🚧
diff --git a/docs/source/understanding-idom/why-idom-needs-keys.rst b/docs/source/understanding-idom/why-idom-needs-keys.rst
new file mode 100644
index 000000000..02d8613c4
--- /dev/null
+++ b/docs/source/understanding-idom/why-idom-needs-keys.rst
@@ -0,0 +1,8 @@
+.. _Why IDOM Needs Keys:
+
+Why IDOM Needs Keys 🚧
+======================
+
+.. note::
+
+ Under construction 🚧
diff --git a/noxfile.py b/noxfile.py
index 7ee541b50..1aa6d3771 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -4,37 +4,77 @@
import os
import re
from pathlib import Path
-from typing import Any, Callable
+from typing import Any, Callable, TypeVar
import nox
from nox.sessions import Session
ROOT = Path(__file__).parent
+SRC = ROOT / "src"
POSARGS_PATTERN = re.compile(r"^(\w+)\[(.+)\]$")
+TRUE_VALUES = {"true", "True", "TRUE", "1"}
-def apply_standard_pip_upgrades(
- function: Callable[[Session], Any]
-) -> Callable[[Session], Any]:
- @functools.wraps(function)
- def wrapper(session: Session) -> None:
- session.install("--upgrade", "pip", "setuptools", "wheel")
- return function(session)
+_Return = TypeVar("_Return")
- return wrapper
+
+def do_first(
+ first_session_func: Callable[[Session], None]
+) -> Callable[[Callable[[Session], _Return]], Callable[[Session], _Return]]:
+ """Decorator for functions defining session actions that should happen first
+
+ >>> @do_first
+ >>> def setup(session):
+ >>> ... # do some setup
+ >>>
+ >>> @setup
+ >>> def the_actual_session(session):
+ >>> ... # so actual work
+
+ This makes it quick an easy to define common setup actions.
+ """
+
+ def setup(
+ second_session_func: Callable[[Session], _Return]
+ ) -> Callable[[Session], _Return]:
+ @functools.wraps(second_session_func)
+ def wrapper(session: Session) -> Any:
+ first_session_func(session)
+ return second_session_func(session)
+
+ return wrapper
+
+ return setup
+
+
+@do_first
+def apply_standard_pip_upgrades(session: Session) -> None:
+ session.install("--upgrade", "pip")
+
+
+@do_first
+def install_latest_npm_in_ci(session: Session) -> None:
+ if os.environ.get("CI") in TRUE_VALUES:
+ session.log("Running in CI environment - installing latest NPM")
+ session.run("npm", "install", "-g", "npm@latest", external=True)
@nox.session(reuse_venv=True)
@apply_standard_pip_upgrades
def format(session: Session) -> None:
+ """Auto format Python and Javascript code"""
# format Python
install_requirements_file(session, "check-style")
session.run("black", ".")
session.run("isort", ".")
- # format Javascript
- session.chdir(ROOT / "src" / "client")
+ # format client Javascript
+ session.chdir(SRC / "client")
+ session.run("npm", "run", "format", external=True)
+
+ # format docs Javascript
+ session.chdir(ROOT / "docs" / "source" / "_custom_js")
session.run("npm", "run", "format", external=True)
@@ -42,18 +82,18 @@ def format(session: Session) -> None:
@apply_standard_pip_upgrades
def example(session: Session) -> None:
"""Run an example"""
- if not session.posargs:
- print("No example name given. Choose from:")
- for found_example_file in (ROOT / "docs" / "source" / "examples").glob("*.py"):
- print("-", found_example_file.stem)
- return None
-
session.install("matplotlib")
install_idom_dev(session)
- session.run("python", "scripts/one_example.py", *session.posargs)
+ session.run(
+ "python",
+ "scripts/one_example.py",
+ *session.posargs,
+ env=get_idom_script_env(),
+ )
@nox.session(reuse_venv=True)
+@install_latest_npm_in_ci
@apply_standard_pip_upgrades
def docs(session: Session) -> None:
"""Build and display documentation in the browser (automatically reloads on change)"""
@@ -66,7 +106,7 @@ def docs(session: Session) -> None:
# watch python source too
"--watch=src/idom",
# for some reason this matches absolute paths
- "--ignore=**/auto/*",
+ "--ignore=**/_autogen/*",
"--ignore=**/_static/custom.js",
"--ignore=**/node_modules/*",
"--ignore=**/package-lock.json",
@@ -76,12 +116,13 @@ def docs(session: Session) -> None:
"html",
"docs/source",
"docs/build",
- env={"PYTHONPATH": os.getcwd(), "IDOM_DEBUG_MODE": "1"},
+ env=get_idom_script_env(),
)
@nox.session
def docs_in_docker(session: Session) -> None:
+ """Build a docker image for the documentation and run it to mimic production"""
session.run(
"docker",
"build",
@@ -109,15 +150,32 @@ def docs_in_docker(session: Session) -> None:
@nox.session
def test(session: Session) -> None:
"""Run the complete test suite"""
- session.notify("test_suite", posargs=session.posargs)
- session.notify("test_types")
- session.notify("test_style")
+ session.notify("test_python", posargs=session.posargs)
session.notify("test_docs")
+ session.notify("test_javascript")
+
+
+@nox.session
+def test_python(session: Session) -> None:
+ """Run all Python checks"""
+ session.notify("test_python_suite", posargs=session.posargs)
+ session.notify("test_python_types")
+ session.notify("test_python_style")
+ session.notify("test_python_build")
+
+
+@nox.session
+def test_javascript(session: Session) -> None:
+ """Run all Javascript checks"""
+ session.notify("test_javascript_suite")
+ session.notify("test_javascript_build")
+ session.notify("test_javascript_style")
@nox.session
+@install_latest_npm_in_ci
@apply_standard_pip_upgrades
-def test_suite(session: Session) -> None:
+def test_python_suite(session: Session) -> None:
"""Run the Python-based test suite"""
session.env["IDOM_DEBUG_MODE"] = "1"
install_requirements_file(session, "test-env")
@@ -135,8 +193,8 @@ def test_suite(session: Session) -> None:
@nox.session
@apply_standard_pip_upgrades
-def test_types(session: Session) -> None:
- """Perform a static type analysis of the codebase"""
+def test_python_types(session: Session) -> None:
+ """Perform a static type analysis of the Python codebase"""
install_requirements_file(session, "check-types")
install_requirements_file(session, "pkg-deps")
install_requirements_file(session, "pkg-extras")
@@ -145,8 +203,8 @@ def test_types(session: Session) -> None:
@nox.session
@apply_standard_pip_upgrades
-def test_style(session: Session) -> None:
- """Check that style guidelines are being followed"""
+def test_python_style(session: Session) -> None:
+ """Check that Python style guidelines are being followed"""
install_requirements_file(session, "check-style")
session.run("flake8", "src/idom", "tests", "docs")
black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist"
@@ -162,6 +220,15 @@ def test_style(session: Session) -> None:
@nox.session
@apply_standard_pip_upgrades
+def test_python_build(session: Session) -> None:
+ """Test whether the Python package can be build for distribution"""
+ install_requirements_file(session, "build-pkg")
+ session.run("python", "setup.py", "bdist_wheel", "sdist")
+
+
+@nox.session
+@install_latest_npm_in_ci
+@apply_standard_pip_upgrades
def test_docs(session: Session) -> None:
"""Verify that the docs build and that doctests pass"""
install_requirements_file(session, "build-docs")
@@ -178,10 +245,48 @@ def test_docs(session: Session) -> None:
"docs/build",
)
session.run("sphinx-build", "-b", "doctest", "docs/source", "docs/build")
+ # ensure docker image build works too
+ session.run("docker", "build", ".", "--file", "docs/Dockerfile", external=True)
+
+
+@do_first
+@install_latest_npm_in_ci
+def setup_client_env(session: Session) -> None:
+ session.chdir(SRC / "client")
+ session.run("npm", "install", external=True)
@nox.session
-def tag(session: Session):
+@setup_client_env
+def test_javascript_suite(session: Session) -> None:
+ """Run the Javascript-based test suite and ensure it bundles succesfully"""
+ session.run("npm", "run", "test", external=True)
+
+
+@nox.session
+@setup_client_env
+def test_javascript_build(session: Session) -> None:
+ """Run the Javascript-based test suite and ensure it bundles succesfully"""
+ session.run("npm", "run", "test", external=True)
+
+
+@nox.session
+@setup_client_env
+def test_javascript_style(session: Session) -> None:
+ """Check that Javascript style guidelines are being followed"""
+ session.run("npm", "run", "check-format", external=True)
+
+
+@nox.session
+def build_js(session: Session) -> None:
+ """Build javascript client code"""
+ session.chdir(SRC / "client")
+ session.run("npm", "run", "build", external=True)
+
+
+@nox.session
+def tag(session: Session) -> None:
+ """Create a new git tag"""
try:
session.run(
"git",
@@ -216,6 +321,7 @@ def tag(session: Session):
@nox.session
def update_version(session: Session) -> None:
+ """Update the version of all Python and Javascript packages in this repo"""
if len(session.posargs) > 1:
session.error("To many arguments")
@@ -273,3 +379,11 @@ def get_version() -> str:
def set_version(new: str) -> None:
(ROOT / "VERSION").write_text(new.strip() + "\n")
+
+
+def get_idom_script_env() -> dict[str, str]:
+ return {
+ "PYTHONPATH": os.getcwd(),
+ "IDOM_DEBUG_MODE": os.environ.get("IDOM_DEBUG_MODE", "1"),
+ "IDOM_CHECK_VDOM_SPEC": os.environ.get("IDOM_CHECK_VDOM_SPEC", "0"),
+ }
diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt
index 7581e7fa3..3d7fe634e 100644
--- a/requirements/build-docs.txt
+++ b/requirements/build-docs.txt
@@ -1,8 +1,9 @@
sphinx ==4.1.2
sphinx-autodoc-typehints ==1.7.0
-furo ==2021.07.28.b40
+furo ==2021.10.09
setuptools_scm
sphinx-copybutton ==0.3.0
sphinx-autobuild ==2020.9.1
sphinx-reredirects ==0.0.1
-sphinx-design ==0.0.12
+sphinx-design ==0.0.13
+sphinx-resolve-py-references ==0.1.0
diff --git a/scripts/README.md b/scripts/README.md
index a730b86ff..a9e8fa35c 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,4 +1,5 @@
# Scripts
-All scripts should be run from the repository root
-(typically using [`nox`](https://nox.thea.codes/en/stable/)).
+All scripts should be run from the repository root (typically using
+[`nox`](https://nox.thea.codes/en/stable/)). Use `nox --list` to see the list of
+available scripts.
diff --git a/scripts/live_docs.py b/scripts/live_docs.py
index 0f42a1ca6..f0173d436 100644
--- a/scripts/live_docs.py
+++ b/scripts/live_docs.py
@@ -9,7 +9,7 @@
get_parser,
)
-from docs.main import IDOM_MODEL_SERVER_URL_PREFIX, make_app, make_component
+from docs.app import IDOM_MODEL_SERVER_URL_PREFIX, make_app, make_examples_component
from idom.server.sanic import PerClientStateServer
from idom.testing import clear_idom_web_modules_dir
@@ -28,7 +28,7 @@ def new_builder():
clear_idom_web_modules_dir()
server = PerClientStateServer(
- make_component(),
+ make_examples_component(),
{"cors": True, "url_prefix": IDOM_MODEL_SERVER_URL_PREFIX},
make_app(),
)
diff --git a/scripts/one_example.py b/scripts/one_example.py
index fd81ce0b6..a017f5090 100644
--- a/scripts/one_example.py
+++ b/scripts/one_example.py
@@ -1,72 +1,78 @@
import sys
import time
from os.path import getmtime
-from pathlib import Path
-from threading import Thread
+from threading import Event, Thread
import idom
+from docs.examples import all_example_names, get_example_files_by_name, load_one_example
from idom.widgets import hotswap
-here = Path(__file__).parent
-examples_dir = here.parent / "docs" / "source" / "examples"
-sys.path.insert(0, str(examples_dir))
-
-for file in examples_dir.iterdir():
- if not file.is_file() or not file.suffix == ".py" or file.stem.startswith("_"):
- continue
+EXAMPLE_NAME_SET = all_example_names()
+EXAMPLE_NAME_LIST = tuple(sorted(EXAMPLE_NAME_SET))
def on_file_change(path, callback):
+ did_call_back_once = Event()
+
def watch_for_change():
last_modified = 0
while True:
modified_at = getmtime(path)
if modified_at != last_modified:
callback()
+ did_call_back_once.set()
last_modified = modified_at
time.sleep(1)
Thread(target=watch_for_change, daemon=True).start()
+ did_call_back_once.wait()
def main():
- try:
- ex_name = sys.argv[1]
- except IndexError:
- print("No example argument given. Choose from:")
- _print_available_options()
- return
+ ex_name = _example_name_input()
+
+ mount, component = hotswap(update_on_change=True)
+
+ def update_component():
+ print(f"Loading example: {ex_name!r}")
+ mount(load_one_example(ex_name))
- example_file = examples_dir / (ex_name + ".py")
+ for file in get_example_files_by_name(ex_name):
+ on_file_change(file, update_component)
- if not example_file.exists():
- print(f"No example {ex_name!r} exists. Choose from:")
+ idom.run(component)
+
+
+def _example_name_input() -> str:
+ if len(sys.argv) == 1:
+ print("No example argument given. Provide an example's number or name:")
_print_available_options()
- return
+ sys.exit(1)
- idom_run = idom.run
- idom.run, component = hotswap(update_on_change=True)
+ ex_name = sys.argv[1]
- def update_component():
- print(f"Reloading {ex_name}")
- with example_file.open() as f:
- exec(
- f.read(),
- {
- "__file__": str(file),
- "__name__": f"__main__.examples.{file.stem}",
- },
- )
+ if ex_name in EXAMPLE_NAME_SET:
+ return ex_name
- on_file_change(example_file, update_component)
+ try:
+ ex_num = int(ex_name)
+ except ValueError:
+ print(f"No example {ex_name!r} exists. Provide an example's number or name:")
+ _print_available_options()
+ sys.exit(1)
- idom_run(component, port=8000)
+ ex_index = ex_num - 1
+ try:
+ return EXAMPLE_NAME_LIST[ex_index]
+ except IndexError:
+ print(f"No example #{ex_num} exists.")
+ sys.exit(1)
def _print_available_options():
- for found_example_file in sorted(examples_dir.glob("*.py")):
- print("-", found_example_file.stem)
+ for i, name in enumerate(EXAMPLE_NAME_LIST):
+ print(f"{i + 1}.", name)
if __name__ == "__main__":
diff --git a/scripts/run_docs.py b/scripts/run_docs.py
new file mode 100644
index 000000000..4a224ece8
--- /dev/null
+++ b/scripts/run_docs.py
@@ -0,0 +1,13 @@
+import os
+import sys
+
+
+# all scripts should be run from the repository root so we need to insert cwd to path
+# to import docs
+sys.path.insert(0, os.getcwd())
+
+
+if __name__ == "__main__":
+ from docs import run
+
+ run()
diff --git a/src/client/package-lock.json b/src/client/package-lock.json
index 963e91a00..4f6d7624c 100644
--- a/src/client/package-lock.json
+++ b/src/client/package-lock.json
@@ -10,9 +10,6 @@
"workspaces": [
"./packages/*"
],
- "dependencies": {
- "idom-client-react": "file:packages/idom-client-react"
- },
"devDependencies": {
"prettier": "^2.2.1",
"snowpack": "^3.5.2"
@@ -2232,16 +2229,6 @@
"node": ">= 6"
}
},
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@@ -4415,13 +4402,6 @@
"socks": "^2.3.3"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
- "optional": true
- },
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
diff --git a/src/client/package.json b/src/client/package.json
index 5470ae1aa..c01995679 100644
--- a/src/client/package.json
+++ b/src/client/package.json
@@ -1,9 +1,5 @@
{
- "dependencies": {
- "idom-client-react": "file:packages/idom-client-react"
- },
"devDependencies": {
- "prettier": "^2.2.1",
"snowpack": "^3.5.2"
},
"license": "MIT",
@@ -15,7 +11,8 @@
"build": "snowpack build",
"format": "npm --workspaces run format",
"publish": "npm --workspaces publish",
- "test": "npm --workspaces test"
+ "test": "npm --workspaces test",
+ "check-format": "npm --workspaces run check-format"
},
"version": "0.33.3",
"workspaces": [
diff --git a/src/client/packages/idom-app-react/package.json b/src/client/packages/idom-app-react/package.json
index 6333eaf45..4195882cb 100644
--- a/src/client/packages/idom-app-react/package.json
+++ b/src/client/packages/idom-app-react/package.json
@@ -7,7 +7,7 @@
},
"description": "A client application for IDOM implemented in React",
"devDependencies": {
- "prettier": "^2.2.1"
+ "prettier": "^2.5.1"
},
"license": "MIT",
"main": "src/index.js",
@@ -18,6 +18,7 @@
},
"scripts": {
"format": "prettier --write ./src",
+ "check-format": "prettier --check ./src",
"test": "echo 'no tests'"
},
"version": "0.33.3"
diff --git a/src/client/packages/idom-app-react/src/index.js b/src/client/packages/idom-app-react/src/index.js
index 0e24439ac..8d4725603 100644
--- a/src/client/packages/idom-app-react/src/index.js
+++ b/src/client/packages/idom-app-react/src/index.js
@@ -1,32 +1,15 @@
-import { mountLayoutWithWebSocket } from "idom-client-react";
+import { mountWithLayoutServer, LayoutServerInfo } from "idom-client-react";
export function mount(mountPoint) {
- mountLayoutWithWebSocket(
- mountPoint,
- getWebSocketEndpoint(),
- loadImportSource,
- shouldReconnect() ? 45 : 0
- );
-}
-
-function getWebSocketEndpoint() {
- const uri = document.location.hostname + ":" + document.location.port;
- const url = (uri + document.location.pathname).split("/").slice(0, -1);
- url[url.length - 1] = "stream";
- const secure = document.location.protocol === "https:";
-
- let protocol;
- if (secure) {
- protocol = "wss:";
- } else {
- protocol = "ws:";
- }
-
- return protocol + "//" + url.join("/") + "?" + queryParams.user.toString();
-}
+ const serverInfo = new LayoutServerInfo({
+ host: document.location.hostname,
+ port: document.location.port,
+ path: "/",
+ query: queryParams.user.toString(),
+ secture: document.location.protocol == "https",
+ });
-function loadImportSource(source, sourceType) {
- return import(sourceType == "NAME" ? `/modules/${source}` : source);
+ mountWithLayoutServer(mountPoint, serverInfo, shouldReconnect() ? 45 : 0);
}
function shouldReconnect() {
diff --git a/src/client/packages/idom-client-react/package.json b/src/client/packages/idom-client-react/package.json
index 6914fcaf8..38d4ad663 100644
--- a/src/client/packages/idom-client-react/package.json
+++ b/src/client/packages/idom-client-react/package.json
@@ -7,7 +7,7 @@
"description": "A client for IDOM implemented in React",
"devDependencies": {
"jsdom": "16.3.0",
- "prettier": "^2.2.1",
+ "prettier": "^2.5.1",
"uvu": "^0.5.1"
},
"files": [
@@ -26,6 +26,7 @@
},
"scripts": {
"format": "prettier --write ./src",
+ "check-format": "prettier --check ./src",
"test": "uvu tests"
},
"type": "module",
diff --git a/src/client/packages/idom-client-react/src/event-to-object.js b/src/client/packages/idom-client-react/src/event-to-object.js
index eedeeed8c..f4d48dd85 100644
--- a/src/client/packages/idom-client-react/src/event-to-object.js
+++ b/src/client/packages/idom-client-react/src/event-to-object.js
@@ -39,7 +39,17 @@ const targetTransformCategories = {
};
const targetTagCategories = {
- hasValue: ["BUTTON", "INPUT", "OPTION", "LI", "METER", "PROGRESS", "PARAM"],
+ hasValue: [
+ "BUTTON",
+ "INPUT",
+ "OPTION",
+ "LI",
+ "METER",
+ "PROGRESS",
+ "PARAM",
+ "SELECT",
+ "TEXTAREA",
+ ],
hasCurrentTime: ["AUDIO", "VIDEO"],
hasFiles: ["INPUT"],
};
diff --git a/src/client/packages/idom-client-react/src/index.js b/src/client/packages/idom-client-react/src/index.js
index 2834cdf53..bee7de4fd 100644
--- a/src/client/packages/idom-client-react/src/index.js
+++ b/src/client/packages/idom-client-react/src/index.js
@@ -1,2 +1,3 @@
export * from "./mount.js";
export * from "./components.js";
+export * from "./server.js";
diff --git a/src/client/packages/idom-client-react/src/mount.js b/src/client/packages/idom-client-react/src/mount.js
index d354fd809..926f2a8ae 100644
--- a/src/client/packages/idom-client-react/src/mount.js
+++ b/src/client/packages/idom-client-react/src/mount.js
@@ -66,7 +66,9 @@ function mountLayoutWithReconnectingWebSocket(
mountState
);
- console.info(`IDOM WebSocket connection lost. Reconnecting in ${reconnectTimeout} seconds...`);
+ console.info(
+ `IDOM WebSocket connection lost. Reconnecting in ${reconnectTimeout} seconds...`
+ );
setTimeout(function () {
mountState.reconnectAttempts++;
diff --git a/src/client/packages/idom-client-react/src/server.js b/src/client/packages/idom-client-react/src/server.js
new file mode 100644
index 000000000..725e49e56
--- /dev/null
+++ b/src/client/packages/idom-client-react/src/server.js
@@ -0,0 +1,35 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { mountLayoutWithWebSocket } from "./mount.js";
+
+export function mountWithLayoutServer(
+ element,
+ serverInfo,
+ maxReconnectTimeout
+) {
+ const loadImportSource = (source, sourceType) =>
+ import(sourceType == "NAME" ? serverInfo.path.module(source) : source);
+
+ mountLayoutWithWebSocket(
+ element,
+ serverInfo.path.stream,
+ loadImportSource,
+ maxReconnectTimeout
+ );
+}
+
+export function LayoutServerInfo({ host, port, path, query, secure }) {
+ const wsProtocol = "ws" + (secure ? "s" : "");
+ const httpProtocol = "http" + (secure ? "s" : "");
+
+ const uri = host + ":" + port;
+ const url = (uri + path).split("/").slice(0, -1).join("/");
+
+ const wsBaseUrl = wsProtocol + "://" + url;
+ const httpBaseUrl = httpProtocol + "://" + url;
+
+ this.path = {
+ stream: wsBaseUrl + "/stream" + "?" + query,
+ module: (source) => httpBaseUrl + `/modules/${source}`,
+ };
+}
diff --git a/src/idom/__init__.py b/src/idom/__init__.py
index 5975c86dd..f7133fe16 100644
--- a/src/idom/__init__.py
+++ b/src/idom/__init__.py
@@ -3,8 +3,18 @@
from .core.component import Component, component
from .core.dispatcher import Stop
from .core.events import EventHandler, event
+from .core.hooks import (
+ use_callback,
+ use_effect,
+ use_memo,
+ use_reducer,
+ use_ref,
+ use_state,
+)
from .core.layout import Layout
+from .core.proto import ComponentType, VdomDict
from .core.vdom import vdom
+from .sample import run_sample_app
from .server.prefab import run
from .utils import Ref, html_to_vdom
from .widgets import hotswap, multiview
@@ -16,6 +26,7 @@
__all__ = [
"component",
"Component",
+ "ComponentType",
"config",
"event",
"EventHandler",
@@ -27,8 +38,16 @@
"log",
"multiview",
"Ref",
+ "run_sample_app",
"run",
"Stop",
+ "use_callback",
+ "use_effect",
+ "use_memo",
+ "use_reducer",
+ "use_ref",
+ "use_state",
"vdom",
+ "VdomDict",
"web",
]
diff --git a/src/idom/config.py b/src/idom/config.py
index 216ae268a..585e4265b 100644
--- a/src/idom/config.py
+++ b/src/idom/config.py
@@ -24,8 +24,24 @@
The string values ``1`` and ``0`` are mapped to ``True`` and ``False`` respectively.
When debug is on, extra validation measures are applied that negatively impact
-performance but can be used to catch bugs. Additionally, the default log level for IDOM
-is set to ``DEBUG``.
+performance but can be used to catch bugs during development. Additionally, the default
+log level for IDOM is set to ``DEBUG``.
+"""
+
+IDOM_CHECK_VDOM_SPEC = _Option(
+ "IDOM_CHECK_VDOM_SPEC",
+ default=IDOM_DEBUG_MODE.current,
+ mutable=False,
+ validator=lambda x: bool(int(x)),
+)
+"""This immutable option turns on/off checks which ensure VDOM is rendered to spec
+
+The string values ``1`` and ``0`` are mapped to ``True`` and ``False`` respectively.
+
+By default this check is off. When ``IDOM_DEBUG_MODE=1`` this will be turned on but can
+be manually disablled by setting ``IDOM_CHECK_VDOM_SPEC=0`` in addition.
+
+For more info on the VDOM spec, see here: :ref:`VDOM JSON Schema`
"""
# Because these web modules will be linked dynamically at runtime this can be temporary
diff --git a/src/idom/core/component.py b/src/idom/core/component.py
index d16fee105..65fb12c6c 100644
--- a/src/idom/core/component.py
+++ b/src/idom/core/component.py
@@ -6,7 +6,6 @@
from __future__ import annotations
import inspect
-import warnings
from functools import wraps
from typing import Any, Callable, Dict, Optional, Tuple, Union
@@ -26,11 +25,9 @@ def component(
inspect.Parameter.KEYWORD_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
- if key_is_kwarg: # pragma: no cover
- warnings.warn(
- f"Component render function {function} uses reserved parameter 'key' - this "
- "will produce an error in a future release",
- DeprecationWarning,
+ if key_is_kwarg:
+ raise TypeError(
+ f"Component render function {function} uses reserved parameter 'key'"
)
@wraps(function)
@@ -74,6 +71,6 @@ def __repr__(self) -> str:
else:
items = ", ".join(f"{k}={v!r}" for k, v in args.items())
if items:
- return f"{self._func.__name__}({id(self)}, {items})"
+ return f"{self._func.__name__}({id(self):02x}, {items})"
else:
- return f"{self._func.__name__}({id(self)})"
+ return f"{self._func.__name__}({id(self):02x})"
diff --git a/src/idom/core/dispatcher.py b/src/idom/core/dispatcher.py
index 10a3acf6b..ef6e5e5cc 100644
--- a/src/idom/core/dispatcher.py
+++ b/src/idom/core/dispatcher.py
@@ -5,7 +5,7 @@
from __future__ import annotations
-from asyncio import Future, Queue
+from asyncio import Future, Queue, ensure_future
from asyncio.tasks import FIRST_COMPLETED, ensure_future, gather, wait
from contextlib import asynccontextmanager
from logging import getLogger
@@ -220,7 +220,9 @@ async def _single_incoming_loop(
layout: LayoutType[LayoutUpdate, LayoutEvent], recv: RecvCoroutine
) -> None:
while True:
- await layout.deliver(await recv())
+ # We need to fire and forget here so that we avoid waiting on the completion
+ # of this event handler before receiving and running the next one.
+ ensure_future(layout.deliver(await recv()))
async def _shared_outgoing_loop(
diff --git a/src/idom/core/events.py b/src/idom/core/events.py
index 7e8d74ee9..f79367eec 100644
--- a/src/idom/core/events.py
+++ b/src/idom/core/events.py
@@ -5,17 +5,40 @@
from __future__ import annotations
import asyncio
-from typing import Any, Callable, List, Optional, Sequence
+from typing import Any, Callable, List, Optional, Sequence, overload
from anyio import create_task_group
+from typing_extensions import Literal
from idom.core.proto import EventHandlerFunc, EventHandlerType
+@overload
def event(
+ function: Callable[..., Any],
+ *,
+ stop_propagation: bool = ...,
+ prevent_default: bool = ...,
+) -> EventHandler:
+ ...
+
+
+@overload
+def event(
+ function: Literal[None] = None,
+ *,
+ stop_propagation: bool = ...,
+ prevent_default: bool = ...,
+) -> Callable[[Callable[..., Any]], EventHandler]:
+ ...
+
+
+def event(
+ function: Callable[..., Any] | None = None,
+ *,
stop_propagation: bool = False,
prevent_default: bool = False,
-) -> Callable[[Callable[..., Any]], EventHandler]:
+) -> EventHandler | Callable[[Callable[..., Any]], EventHandler]:
"""A decorator for constructing an :class:`EventHandler`.
While you're always free to add callbacks by assigning them to an element's attributes
@@ -52,7 +75,10 @@ def setup(function: Callable[..., Any]) -> EventHandler:
prevent_default,
)
- return setup
+ if function is not None:
+ return setup(function)
+ else:
+ return setup
class EventHandler:
diff --git a/src/idom/core/layout.py b/src/idom/core/layout.py
index a86e29d20..2923dea20 100644
--- a/src/idom/core/layout.py
+++ b/src/idom/core/layout.py
@@ -26,7 +26,11 @@
from uuid import uuid4
from weakref import ref as weakref
-from idom.config import IDOM_DEBUG_MODE, IDOM_FEATURE_INDEX_AS_DEFAULT_KEY
+from idom.config import (
+ IDOM_CHECK_VDOM_SPEC,
+ IDOM_DEBUG_MODE,
+ IDOM_FEATURE_INDEX_AS_DEFAULT_KEY,
+)
from idom.utils import Ref
from .hooks import LifeCycleHook
@@ -79,7 +83,7 @@ class Layout:
def __init__(self, root: "ComponentType") -> None:
super().__init__()
if not isinstance(root, ComponentType):
- raise TypeError("Expected an ComponentType, not %r" % root)
+ raise TypeError(f"Expected a ComponentType, not {type(root)!r}.")
self.root = root
def __enter__(self: _Self) -> _Self:
@@ -141,10 +145,10 @@ async def render(self) -> LayoutUpdate:
else:
return self._create_layout_update(model_state)
- if IDOM_DEBUG_MODE.current:
+ if IDOM_CHECK_VDOM_SPEC.current:
# If in debug mode inject a function that ensures all returned updates
- # contain valid VDOM models. We only do this in debug mode in order to
- # avoid unnecessarily impacting performance.
+ # contain valid VDOM models. We only do this in debug mode or when this check
+ # is explicitely turned in order to avoid unnecessarily impacting performance.
_debug_render = render
@@ -701,7 +705,6 @@ def _process_child_type_and_key(
def _default_key(index: int) -> Any: # pragma: no cover
return index
-
else:
def _default_key(index: int) -> Any:
diff --git a/src/idom/core/vdom.py b/src/idom/core/vdom.py
index 898b5e32f..967c0c66e 100644
--- a/src/idom/core/vdom.py
+++ b/src/idom/core/vdom.py
@@ -174,7 +174,7 @@ def vdom(
if event_handlers:
model["eventHandlers"] = event_handlers
- if key:
+ if key != "":
model["key"] = key
if import_source is not None:
@@ -222,7 +222,11 @@ def constructor(
# replicate common function attributes
constructor.__name__ = tag
- constructor.__doc__ = f"Return a new ``<{tag}/>`` :class:`VdomDict` element"
+ constructor.__doc__ = (
+ "Return a new "
+ f"`<{tag}> `__ "
+ "element represented by a :class:`VdomDict`."
+ )
frame = inspect.currentframe()
if frame is not None and frame.f_back is not None and frame.f_back is not None:
@@ -333,6 +337,6 @@ def _is_single_child(value: Any) -> bool:
if (isinstance(child, ComponentType) and child.key is None) or (
isinstance(child, Mapping) and "key" not in child
):
- logger.error(f"Key not specified for dynamic child {child}")
+ logger.error(f"Key not specified for child in list {child}")
return False
diff --git a/src/idom/html.py b/src/idom/html.py
index 594dec77e..a3381c5c0 100644
--- a/src/idom/html.py
+++ b/src/idom/html.py
@@ -100,14 +100,18 @@
Forms
-----
-- :func:`meter`
-- :func:`output`
-- :func:`progress`
-- :func:`input`
- :func:`button`
-- :func:`label`
- :func:`fieldset`
+- :func:`form`
+- :func:`input`
+- :func:`label`
- :func:`legend`
+- :func:`meter`
+- :func:`option`
+- :func:`output`
+- :func:`progress`
+- :func:`select`
+- :func:`textarea`
Interactive Elements
@@ -201,14 +205,18 @@
tr = make_vdom_constructor("tr")
# Forms
-meter = make_vdom_constructor("meter")
-output = make_vdom_constructor("output")
-progress = make_vdom_constructor("progress")
-input = make_vdom_constructor("input", allow_children=False)
button = make_vdom_constructor("button")
-label = make_vdom_constructor("label")
fieldset = make_vdom_constructor("fieldset")
+form = make_vdom_constructor("form")
+input = make_vdom_constructor("input", allow_children=False)
+label = make_vdom_constructor("label")
legend = make_vdom_constructor("legend")
+meter = make_vdom_constructor("meter")
+option = make_vdom_constructor("option")
+output = make_vdom_constructor("output")
+progress = make_vdom_constructor("progress")
+select = make_vdom_constructor("select")
+textarea = make_vdom_constructor("textarea")
# Interactive elements
details = make_vdom_constructor("details")
diff --git a/src/idom/sample.py b/src/idom/sample.py
new file mode 100644
index 000000000..63ffdd243
--- /dev/null
+++ b/src/idom/sample.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+import webbrowser
+from typing import Any
+
+from idom.server.proto import ServerType
+
+from . import html
+from .core.component import component
+from .core.proto import VdomDict
+from .server.utils import find_available_port, find_builtin_server_type
+
+
+@component
+def App() -> VdomDict:
+ return html.div(
+ {"style": {"padding": "15px"}},
+ html.h1("Sample Application"),
+ html.p(
+ "This is a basic application made with IDOM. Click ",
+ html.a(
+ {"href": "https://pypi.org/project/idom/", "target": "_blank"},
+ "here",
+ ),
+ " to learn more.",
+ ),
+ )
+
+
+def run_sample_app(
+ host: str = "127.0.0.1",
+ port: int | None = None,
+ open_browser: bool = False,
+ run_in_thread: bool | None = None,
+) -> ServerType[Any]:
+ """Run a sample application.
+
+ Args:
+ host: host where the server should run
+ port: the port on the host to serve from
+ open_browser: whether to open a browser window after starting the server
+ """
+ port = port or find_available_port(host)
+ server_type = find_builtin_server_type("PerClientStateServer")
+ server = server_type(App)
+
+ run_in_thread = open_browser or run_in_thread
+
+ if not run_in_thread: # pragma: no cover
+ server.run(host=host, port=port)
+ return server
+
+ thread = server.run_in_thread(host=host, port=port)
+ server.wait_until_started(5)
+
+ if open_browser: # pragma: no cover
+ webbrowser.open(f"http://{host}:{port}")
+ thread.join()
+
+ return server
diff --git a/src/idom/server/fastapi.py b/src/idom/server/fastapi.py
index 17c3762b0..f15cfd664 100644
--- a/src/idom/server/fastapi.py
+++ b/src/idom/server/fastapi.py
@@ -45,9 +45,19 @@ class Config(TypedDict, total=False):
"""Config for :class:`FastApiRenderServer`"""
cors: Union[bool, Dict[str, Any]]
- url_prefix: str
- serve_static_files: bool
+ """Enable or configure Cross Origin Resource Sharing (CORS)
+
+ For more information see docs for ``fastapi.middleware.cors.CORSMiddleware``
+ """
+
redirect_root_to_index: bool
+ """Whether to redirect the root URL (with prefix) to ``index.html``"""
+
+ serve_static_files: bool
+ """Whether or not to serve static files (i.e. web modules)"""
+
+ url_prefix: str
+ """The URL prefix where IDOM resources will be served from"""
def PerClientStateServer(
diff --git a/src/idom/server/flask.py b/src/idom/server/flask.py
index 708dbe590..8918722c2 100644
--- a/src/idom/server/flask.py
+++ b/src/idom/server/flask.py
@@ -37,11 +37,26 @@
class Config(TypedDict, total=False):
"""Render server config for :class:`FlaskRenderServer`"""
- import_name: str
- url_prefix: str
cors: Union[bool, Dict[str, Any]]
- serve_static_files: bool
+ """Enable or configure Cross Origin Resource Sharing (CORS)
+
+ For more information see docs for ``flask_cors.CORS``
+ """
+
+ import_name: str
+ """The module where the application instance was created
+
+ For more info see :class:`flask.Flask`.
+ """
+
redirect_root_to_index: bool
+ """Whether to redirect the root URL (with prefix) to ``index.html``"""
+
+ serve_static_files: bool
+ """Whether or not to serve static files (i.e. web modules)"""
+
+ url_prefix: str
+ """The URL prefix where IDOM resources will be served from"""
def PerClientStateServer(
diff --git a/src/idom/server/prefab.py b/src/idom/server/prefab.py
index 5ac2fdc01..74026098b 100644
--- a/src/idom/server/prefab.py
+++ b/src/idom/server/prefab.py
@@ -9,7 +9,7 @@
from idom.core.proto import ComponentConstructor
from idom.widgets import MountFunc, MultiViewMount, hotswap, multiview
-from .proto import Server, ServerFactory
+from .proto import ServerFactory, ServerType
from .utils import find_available_port, find_builtin_server_type
@@ -28,7 +28,7 @@ def run(
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
daemon: bool = False,
-) -> Server[_App]:
+) -> ServerType[_App]:
"""A utility for quickly running a render server with minimal boilerplate
Parameters:
@@ -77,7 +77,7 @@ def multiview_server(
server_config: Optional[_Config] = None,
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
-) -> Tuple[MultiViewMount, Server[_App]]:
+) -> Tuple[MultiViewMount, ServerType[_App]]:
"""Set up a server where views can be dynamically added.
In other words this allows the user to work with IDOM in an imperative manner. Under
@@ -120,7 +120,7 @@ def hotswap_server(
run_kwargs: Optional[Dict[str, Any]] = None,
app: Optional[Any] = None,
sync_views: bool = False,
-) -> Tuple[MountFunc, Server[_App]]:
+) -> Tuple[MountFunc, ServerType[_App]]:
"""Set up a server where views can be dynamically swapped out.
In other words this allows the user to work with IDOM in an imperative manner. Under
diff --git a/src/idom/server/proto.py b/src/idom/server/proto.py
index b59166d21..d0db29eee 100644
--- a/src/idom/server/proto.py
+++ b/src/idom/server/proto.py
@@ -20,11 +20,11 @@ def __call__(
constructor: ComponentConstructor,
config: Optional[_Config] = None,
app: Optional[_App] = None,
- ) -> Server[_App]:
+ ) -> ServerType[_App]:
...
-class Server(Protocol[_App]):
+class ServerType(Protocol[_App]):
"""A thin wrapper around a web server that provides a common operational interface"""
app: _App
diff --git a/src/idom/server/sanic.py b/src/idom/server/sanic.py
index 91ad5d7b9..bfbee1bd6 100644
--- a/src/idom/server/sanic.py
+++ b/src/idom/server/sanic.py
@@ -41,9 +41,19 @@ class Config(TypedDict, total=False):
"""Config for :class:`SanicRenderServer`"""
cors: Union[bool, Dict[str, Any]]
- url_prefix: str
- serve_static_files: bool
+ """Enable or configure Cross Origin Resource Sharing (CORS)
+
+ For more information see docs for ``sanic_cors.CORS``
+ """
+
redirect_root_to_index: bool
+ """Whether to redirect the root URL (with prefix) to ``index.html``"""
+
+ serve_static_files: bool
+ """Whether or not to serve static files (i.e. web modules)"""
+
+ url_prefix: str
+ """The URL prefix where IDOM resources will be served from"""
def PerClientStateServer(
diff --git a/src/idom/server/tornado.py b/src/idom/server/tornado.py
index 02f611a79..a703e769a 100644
--- a/src/idom/server/tornado.py
+++ b/src/idom/server/tornado.py
@@ -32,9 +32,14 @@
class Config(TypedDict, total=False):
"""Render server config for :class:`TornadoRenderServer` subclasses"""
- url_prefix: str
- serve_static_files: bool
redirect_root_to_index: bool
+ """Whether to redirect the root URL (with prefix) to ``index.html``"""
+
+ serve_static_files: bool
+ """Whether or not to serve static files (i.e. web modules)"""
+
+ url_prefix: str
+ """The URL prefix where IDOM resources will be served from"""
def PerClientStateServer(
diff --git a/src/idom/server/utils.py b/src/idom/server/utils.py
index ebb270211..5fa2a5f9c 100644
--- a/src/idom/server/utils.py
+++ b/src/idom/server/utils.py
@@ -1,10 +1,10 @@
import asyncio
+import socket
import time
from contextlib import closing
from functools import wraps
from importlib import import_module
from pathlib import Path
-from socket import socket
from threading import Event, Thread
from typing import Any, Callable, List, Optional
@@ -95,11 +95,23 @@ def find_builtin_server_type(type_name: str) -> ServerFactory[Any, Any]:
)
-def find_available_port(host: str, port_min: int = 8000, port_max: int = 9000) -> int:
+def find_available_port(
+ host: str,
+ port_min: int = 8000,
+ port_max: int = 9000,
+ allow_reuse_waiting_ports: bool = True,
+) -> int:
"""Get a port that's available for the given host and port range"""
for port in range(port_min, port_max):
- with closing(socket()) as sock:
+ with closing(socket.socket()) as sock:
try:
+ if allow_reuse_waiting_ports:
+ # As per this answer: https://stackoverflow.com/a/19247688/3159288
+ # setting can be somewhat unreliable because we allow the use of
+ # ports that are stuck in TIME_WAIT. However, not setting the option
+ # means we're overly cautious and almost always use a different addr
+ # even if it could have actually been used.
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
except OSError:
pass
diff --git a/src/idom/testing.py b/src/idom/testing.py
index 7b62f579d..3c32d239e 100644
--- a/src/idom/testing.py
+++ b/src/idom/testing.py
@@ -31,7 +31,7 @@
from idom.core.events import EventHandler, to_event_handler_function
from idom.core.hooks import LifeCycleHook, current_hook
from idom.server.prefab import hotswap_server
-from idom.server.proto import Server, ServerFactory
+from idom.server.proto import ServerFactory, ServerType
from idom.server.utils import find_available_port
@@ -60,7 +60,7 @@ def create_simple_selenium_web_driver(
_Self = TypeVar("_Self", bound="ServerMountPoint[Any, Any]")
_Mount = TypeVar("_Mount")
-_Server = TypeVar("_Server", bound=Server[Any])
+_Server = TypeVar("_Server", bound=ServerType[Any])
_App = TypeVar("_App")
_Config = TypeVar("_Config")
@@ -85,7 +85,7 @@ def __init__(
**other_options: Any,
) -> None:
self.host = host
- self.port = port or find_available_port(host)
+ self.port = port or find_available_port(host, allow_reuse_waiting_ports=False)
self._mount_and_server_constructor: "Callable[[], Tuple[_Mount, _Server]]" = (
lambda: mount_and_server_constructor(
server_type,
diff --git a/src/idom/widgets.py b/src/idom/widgets.py
index f21943a82..1cd9996dd 100644
--- a/src/idom/widgets.py
+++ b/src/idom/widgets.py
@@ -194,7 +194,10 @@ def World():
@component
def MultiView(view_id: str) -> Any:
- return views[view_id]()
+ try:
+ return views[view_id]()
+ except KeyError:
+ raise ValueError(f"Unknown view {view_id!r}")
return MultiViewMount(views), MultiView
diff --git a/tests/conftest.py b/tests/conftest.py
index 228f2f4e7..f8281182a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -51,7 +51,7 @@ def mount_and_display(component_constructor, query=None, check_mount=True):
)
driver.get(server_mount_point.url(query=query))
if check_mount:
- driver.find_element_by_id(component_id)
+ driver.find_element("id", component_id)
return component_id
yield mount_and_display
diff --git a/tests/driver_utils.py b/tests/driver_utils.py
index a31bd2fc9..93032486a 100644
--- a/tests/driver_utils.py
+++ b/tests/driver_utils.py
@@ -12,7 +12,7 @@ def send_keys(element: WebElement, keys: Any) -> None:
def no_such_element(driver: WebDriver, method: str, param: Any) -> bool:
try:
- getattr(driver, f"find_element_by_{method}")(param)
+ driver.find_element(method, param)
except NoSuchElementException:
return True
else:
diff --git a/tests/test_client.py b/tests/test_client.py
index b2dc43708..017d9abb9 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -21,10 +21,10 @@ def OldComponent():
mount_point.mount(OldComponent)
driver.get(mount_point.url())
# ensure the element is displayed before stopping the server
- driver.find_element_by_id("old-component")
+ driver.find_element("id", "old-component")
# the server is disconnected but the last view state is still shown
- driver.find_element_by_id("old-component")
+ driver.find_element("id", "old-component")
set_state = idom.Ref(None)
@@ -38,11 +38,11 @@ def NewComponent():
# Note the lack of a page refresh before looking up this new component. The
# client should attempt to reconnect and display the new view automatically.
- driver.find_element_by_id("new-component-0")
+ driver.find_element("id", "new-component-0")
# check that we can resume normal operation
set_state.current(1)
- driver.find_element_by_id("new-component-1")
+ driver.find_element("id", "new-component-1")
def test_style_can_be_changed(display, driver, driver_wait):
@@ -69,7 +69,7 @@ def ButtonWithChangingColor():
display(ButtonWithChangingColor)
- button = driver.find_element_by_id("my-button")
+ button = driver.find_element("id", "my-button")
assert get_style(button)["background-color"] == "red"
diff --git a/tests/test_core/test_component.py b/tests/test_core/test_component.py
index 2fad5e61a..6880d9270 100644
--- a/tests/test_core/test_component.py
+++ b/tests/test_core/test_component.py
@@ -8,7 +8,7 @@ def MyComponent(a, *b, **c):
mc1 = MyComponent(1, 2, 3, x=4, y=5)
- expected = f"MyComponent({id(mc1)}, a=1, b=(2, 3), c={{'x': 4, 'y': 5}})"
+ expected = f"MyComponent({id(mc1):02x}, a=1, b=(2, 3), c={{'x': 4, 'y': 5}})"
assert repr(mc1) == expected
# not enough args supplied to function
@@ -50,7 +50,7 @@ def Hello():
display(Hello)
- assert driver.find_element_by_id("hello")
+ assert driver.find_element("id", "hello")
def test_pre_tags_are_rendered_correctly(driver, display):
@@ -65,7 +65,7 @@ def PreFormated():
display(PreFormated)
- pre = driver.find_element_by_id("pre-form-test")
+ pre = driver.find_element("id", "pre-form-test")
assert (
pre.get_attribute("innerHTML")
diff --git a/tests/test_core/test_dispatcher.py b/tests/test_core/test_dispatcher.py
index 0d613dfa3..a0e6bef36 100644
--- a/tests/test_core/test_dispatcher.py
+++ b/tests/test_core/test_dispatcher.py
@@ -176,3 +176,45 @@ async def recv():
# queue has not been cleaned up and that the patch we just pushed was added to
# it.
assert not sys.getrefcount(patch) > patch_ref_count
+
+
+async def test_dispatcher_handles_more_than_one_event_at_a_time():
+ block_and_never_set = asyncio.Event()
+ will_block = asyncio.Event()
+ second_event_did_execute = asyncio.Event()
+
+ blocked_handler = StaticEventHandler()
+ non_blocked_handler = StaticEventHandler()
+
+ @idom.component
+ def ComponentWithTwoEventHandlers():
+ @blocked_handler.use
+ async def block_forever():
+ will_block.set()
+ await block_and_never_set.wait()
+
+ @non_blocked_handler.use
+ async def handle_event():
+ second_event_did_execute.set()
+
+ return idom.html.div(
+ idom.html.button({"onClick": block_forever}),
+ idom.html.button({"onClick": handle_event}),
+ )
+
+ send_queue = asyncio.Queue()
+ recv_queue = asyncio.Queue()
+
+ asyncio.ensure_future(
+ dispatch_single_view(
+ idom.Layout(ComponentWithTwoEventHandlers()),
+ send_queue.put,
+ recv_queue.get,
+ )
+ )
+
+ await recv_queue.put(LayoutEvent(blocked_handler.target, []))
+ await will_block.wait()
+
+ await recv_queue.put(LayoutEvent(non_blocked_handler.target, []))
+ await second_event_did_execute.wait()
diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py
index 1981e3c18..9be849f4c 100644
--- a/tests/test_core/test_events.py
+++ b/tests/test_core/test_events.py
@@ -153,7 +153,7 @@ async def on_key_down(value):
display(Input)
- inp = driver.find_element_by_id("input")
+ inp = driver.find_element("id", "input")
inp.send_keys("hello")
# the default action of updating the element's value did not take place
assert inp.get_attribute("value") == ""
@@ -174,9 +174,9 @@ async def on_click(event):
display(Button)
- button = driver.find_element_by_id("click")
+ button = driver.find_element("id", "click")
button.click()
- driver.find_element_by_id("complete")
+ driver.find_element("id", "complete")
def test_can_stop_event_propogation(driver, driver_wait, display):
@@ -217,7 +217,7 @@ def outer_click_is_not_triggered(event):
display(DivInDiv)
- inner = driver.find_element_by_id("inner")
+ inner = driver.find_element("id", "inner")
inner.click()
driver_wait.until(lambda _: clicked.current)
diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py
index 9b6269991..fc34f332b 100644
--- a/tests/test_core/test_hooks.py
+++ b/tests/test_core/test_hooks.py
@@ -117,9 +117,9 @@ async def on_click(event):
display(Outer)
- outer = driver.find_element_by_id("outer")
- inner = driver.find_element_by_id("inner")
- count = driver.find_element_by_id("count-view")
+ outer = driver.find_element("id", "outer")
+ inner = driver.find_element("id", "inner")
+ count = driver.find_element("id", "count-view")
driver_wait.until(lambda d: constructor_call_count.current == 1)
driver_wait.until(lambda d: count.get_attribute("innerHTML") == "0")
@@ -157,7 +157,7 @@ def Counter():
display(Counter)
- client_counter = driver.find_element_by_id("counter")
+ client_counter = driver.find_element("id", "counter")
for i in range(3):
assert client_counter.get_attribute("innerHTML") == f"Count: {i}"
@@ -207,8 +207,8 @@ def TestComponent():
display(TestComponent)
- client_r_1_button = driver.find_element_by_id("r_1")
- client_r_2_button = driver.find_element_by_id("r_2")
+ client_r_1_button = driver.find_element("id", "r_1")
+ client_r_2_button = driver.find_element("id", "r_2")
assert render_count.current == 1
assert event_count.current == 0
@@ -248,9 +248,9 @@ async def on_change(event):
display(Input)
- button = driver.find_element_by_id("input")
+ button = driver.find_element("id", "input")
button.send_keys("this is a test")
- driver.find_element_by_id("complete")
+ driver.find_element("id", "complete")
assert message_ref.current == "this is a test"
@@ -273,9 +273,9 @@ def double_set_state(event):
display(SomeComponent)
- button = driver.find_element_by_id("button")
- first = driver.find_element_by_id("first")
- second = driver.find_element_by_id("second")
+ button = driver.find_element("id", "button")
+ first = driver.find_element("id", "first")
+ second = driver.find_element("id", "second")
assert first.get_attribute("value") == "0"
assert second.get_attribute("value") == "0"
diff --git a/tests/test_core/test_layout.py b/tests/test_core/test_layout.py
index 5ac0078ad..adf307bf6 100644
--- a/tests/test_core/test_layout.py
+++ b/tests/test_core/test_layout.py
@@ -7,6 +7,7 @@
import pytest
import idom
+from idom.config import IDOM_DEBUG_MODE
from idom.core.dispatcher import render_json_patch
from idom.core.layout import LayoutEvent
from idom.testing import HookCatcher, StaticEventHandler
@@ -20,13 +21,13 @@ def MyComponent():
my_component = MyComponent()
layout = idom.Layout(my_component)
- assert str(layout) == f"Layout(MyComponent({id(my_component)}))"
+ assert str(layout) == f"Layout(MyComponent({id(my_component):02x}))"
def test_layout_expects_abstract_component():
- with pytest.raises(TypeError, match="Expected an ComponentType"):
+ with pytest.raises(TypeError, match="Expected a ComponentType"):
idom.Layout(None)
- with pytest.raises(TypeError, match="Expected an ComponentType"):
+ with pytest.raises(TypeError, match="Expected a ComponentType"):
idom.Layout(idom.html.div())
@@ -74,12 +75,12 @@ async def test_nested_component_layout():
child_set_state = idom.Ref(None)
@idom.component
- def Parent(key):
+ def Parent():
state, parent_set_state.current = idom.hooks.use_state(0)
return idom.html.div(state, Child(key="c"))
@idom.component
- def Child(key):
+ def Child():
state, child_set_state.current = idom.hooks.use_state(0)
return idom.html.div(state)
@@ -112,7 +113,11 @@ def Child(key):
assert changes == [{"op": "replace", "path": "/children/0", "value": "1"}]
-async def test_layout_render_error_has_partial_update():
+@pytest.mark.skipif(
+ not IDOM_DEBUG_MODE.current,
+ reason="errors only reported in debug mode",
+)
+async def test_layout_render_error_has_partial_update_with_error_message():
@idom.component
def Main():
return idom.html.div([OkChild(), BadChild(), OkChild()])
@@ -144,6 +149,42 @@ def BadChild():
)
+@pytest.mark.skipif(
+ IDOM_DEBUG_MODE.current,
+ reason="errors only reported in debug mode",
+)
+async def test_layout_render_error_has_partial_update_without_error_message():
+ @idom.component
+ def Main():
+ return idom.html.div([OkChild(), BadChild(), OkChild()])
+
+ @idom.component
+ def OkChild():
+ return idom.html.div(["hello"])
+
+ @idom.component
+ def BadChild():
+ raise ValueError("Something went wrong :(")
+
+ with idom.Layout(Main()) as layout:
+ patch = await render_json_patch(layout)
+ assert_same_items(
+ patch.changes,
+ [
+ {
+ "op": "add",
+ "path": "/children",
+ "value": [
+ {"tagName": "div", "children": ["hello"]},
+ {"tagName": "", "error": ""},
+ {"tagName": "div", "children": ["hello"]},
+ ],
+ },
+ {"op": "add", "path": "/tagName", "value": "div"},
+ ],
+ )
+
+
async def test_render_raw_vdom_dict_with_single_component_object_as_children():
@idom.component
def Main():
@@ -431,7 +472,10 @@ async def test_model_key_preserves_callback_identity_for_components():
def RootComponent():
reverse_children, set_reverse_children = use_toggle()
- children = [Trigger(set_reverse_children, key=name) for name in ["good", "bad"]]
+ children = [
+ Trigger(set_reverse_children, name=name, key=name)
+ for name in ["good", "bad"]
+ ]
if reverse_children:
children.reverse()
@@ -439,8 +483,8 @@ def RootComponent():
return idom.html.div(children)
@idom.component
- def Trigger(set_reverse_children, key):
- if key == "good":
+ def Trigger(set_reverse_children, name):
+ if name == "good":
@good_handler.use
def callback():
@@ -501,15 +545,15 @@ async def test_hooks_for_keyed_components_get_garbage_collected():
def Outer():
items, set_items = idom.hooks.use_state([1, 2, 3])
pop_item.current = lambda: set_items(items[:-1])
- return idom.html.div(Inner(key=k) for k in items)
+ return idom.html.div(Inner(key=k, finalizer_id=k) for k in items)
@idom.component
- def Inner(key):
- if key not in registered_finalizers:
+ def Inner(finalizer_id):
+ if finalizer_id not in registered_finalizers:
hook = idom.hooks.current_hook()
- finalize(hook, lambda: garbage_collect_items.append(key))
- registered_finalizers.add(key)
- return idom.html.div(key)
+ finalize(hook, lambda: garbage_collect_items.append(finalizer_id))
+ registered_finalizers.add(finalizer_id)
+ return idom.html.div(finalizer_id)
with idom.Layout(Outer()) as layout:
await layout.render()
@@ -596,8 +640,8 @@ def Outer():
@idom.component
@inner_hook.capture
- def Inner(key):
- return idom.html.div(key)
+ def Inner():
+ return idom.html.div()
with idom.Layout(Outer()) as layout:
await layout.render()
@@ -635,15 +679,15 @@ async def test_schedule_render_from_unmounted_hook(caplog):
@idom.component
def Parent():
state, parent_set_state.current = idom.hooks.use_state(1)
- return Child(key=state)
+ return Child(key=state, state=state)
child_hook = HookCatcher()
@idom.component
@child_hook.capture
- def Child(key):
- idom.hooks.use_effect(lambda: lambda: print("unmount", key))
- return idom.html.div(key)
+ def Child(state):
+ idom.hooks.use_effect(lambda: lambda: print("unmount", state))
+ return idom.html.div(state)
with idom.Layout(Parent()) as layout:
await layout.render()
@@ -678,7 +722,7 @@ def Root():
return idom.html.div(child_nodes[child_type])
@idom.component
- def Child(key):
+ def Child():
return idom.html.div()
child_nodes = {
@@ -706,7 +750,7 @@ def Root():
return idom.html.div(child_nodes[child_type])
@idom.component
- def Child(key):
+ def Child():
return idom.html.div()
child_nodes = {
diff --git a/tests/test_core/test_vdom.py b/tests/test_core/test_vdom.py
index 5a54dc6ef..6339b8177 100644
--- a/tests/test_core/test_vdom.py
+++ b/tests/test_core/test_vdom.py
@@ -336,7 +336,7 @@ def test_debug_log_cannot_verify_keypath_for_genereators(caplog):
def test_debug_log_dynamic_children_must_have_keys(caplog):
idom.vdom("div", [idom.vdom("div")])
assert len(caplog.records) == 1
- assert caplog.records[0].message.startswith("Key not specified for dynamic child")
+ assert caplog.records[0].message.startswith("Key not specified for child")
caplog.records.clear()
@@ -346,4 +346,4 @@ def MyComponent():
idom.vdom("div", [MyComponent()])
assert len(caplog.records) == 1
- assert caplog.records[0].message.startswith("Key not specified for dynamic child")
+ assert caplog.records[0].message.startswith("Key not specified for child")
diff --git a/tests/test_sample.py b/tests/test_sample.py
new file mode 100644
index 000000000..be9135820
--- /dev/null
+++ b/tests/test_sample.py
@@ -0,0 +1,15 @@
+from idom.sample import run_sample_app
+from idom.server.utils import find_available_port
+
+
+def test_sample_app(driver):
+ host = "127.0.0.1"
+ port = find_available_port(host, allow_reuse_waiting_ports=False)
+
+ run_sample_app(host=host, port=port, run_in_thread=True)
+
+ driver.get(f"http://{host}:{port}")
+
+ h1 = driver.find_element("tag name", "h1")
+
+ assert h1.get_attribute("innerHTML") == "Sample Application"
diff --git a/tests/test_server/test_common/test_multiview.py b/tests/test_server/test_common/test_multiview.py
index 91014d1c4..f8c936aef 100644
--- a/tests/test_server/test_common/test_multiview.py
+++ b/tests/test_server/test_common/test_multiview.py
@@ -40,10 +40,10 @@ def test_multiview_server(driver_get, driver, server_mount_point):
)
driver_get({"view_id": manual_id})
- driver.find_element_by_id("e1")
+ driver.find_element("id", "e1")
driver_get({"view_id": auto_view_id})
- driver.find_element_by_id("e2")
+ driver.find_element("id", "e2")
server_mount_point.mount.remove(auto_view_id)
server_mount_point.mount.remove(manual_id)
diff --git a/tests/test_server/test_common/test_per_client_state.py b/tests/test_server/test_common/test_per_client_state.py
index 23ef6e480..fe6d0c8bf 100644
--- a/tests/test_server/test_common/test_per_client_state.py
+++ b/tests/test_server/test_common/test_per_client_state.py
@@ -31,12 +31,12 @@ def Hello():
display(Hello)
- assert driver.find_element_by_id("hello")
+ assert driver.find_element("id", "hello")
# test that we can reconnect succefully
driver.refresh()
- assert driver.find_element_by_id("hello")
+ assert driver.find_element("id", "hello")
def test_display_simple_click_counter(driver, driver_wait, display):
@@ -56,7 +56,7 @@ def Counter():
display(Counter)
- client_counter = driver.find_element_by_id("counter")
+ client_counter = driver.find_element("id", "counter")
for i in range(3):
driver_wait.until(
diff --git a/tests/test_server/test_common/test_shared_state_client.py b/tests/test_server/test_common/test_shared_state_client.py
index f29cbb785..b40d14993 100644
--- a/tests/test_server/test_common/test_shared_state_client.py
+++ b/tests/test_server/test_common/test_shared_state_client.py
@@ -54,21 +54,21 @@ def Counter(count):
driver_1.get(server_mount_point.url())
driver_2.get(server_mount_point.url())
- client_1_button = driver_1.find_element_by_id("incr-button")
- client_2_button = driver_2.find_element_by_id("incr-button")
+ client_1_button = driver_1.find_element("id", "incr-button")
+ client_2_button = driver_2.find_element("id", "incr-button")
- driver_1.find_element_by_id("count-is-0")
- driver_2.find_element_by_id("count-is-0")
+ driver_1.find_element("id", "count-is-0")
+ driver_2.find_element("id", "count-is-0")
client_1_button.click()
- driver_1.find_element_by_id("count-is-1")
- driver_2.find_element_by_id("count-is-1")
+ driver_1.find_element("id", "count-is-1")
+ driver_2.find_element("id", "count-is-1")
client_2_button.click()
- driver_1.find_element_by_id("count-is-2")
- driver_2.find_element_by_id("count-is-2")
+ driver_1.find_element("id", "count-is-2")
+ driver_2.find_element("id", "count-is-2")
assert was_garbage_collected.wait(1)
was_garbage_collected.clear()
@@ -79,18 +79,18 @@ def Counter(count):
driver_1.refresh()
driver_2.refresh()
- client_1_button = driver_1.find_element_by_id("incr-button")
- client_2_button = driver_2.find_element_by_id("incr-button")
+ client_1_button = driver_1.find_element("id", "incr-button")
+ client_2_button = driver_2.find_element("id", "incr-button")
client_1_button.click()
- driver_1.find_element_by_id("count-is-3")
- driver_2.find_element_by_id("count-is-3")
+ driver_1.find_element("id", "count-is-3")
+ driver_2.find_element("id", "count-is-3")
client_1_button.click()
- driver_1.find_element_by_id("count-is-4")
- driver_2.find_element_by_id("count-is-4")
+ driver_1.find_element("id", "count-is-4")
+ driver_2.find_element("id", "count-is-4")
client_2_button.click()
diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py
index 7f78a836d..15067b896 100644
--- a/tests/test_web/test_module.py
+++ b/tests/test_web/test_module.py
@@ -35,17 +35,17 @@ def ShowCurrentComponent():
display(ShowCurrentComponent)
- driver.find_element_by_id("some-component")
+ driver.find_element("id", "some-component")
set_current_component.current(
idom.html.h1({"id": "some-other-component"}, "some other component")
)
# the new component has been displayed
- driver.find_element_by_id("some-other-component")
+ driver.find_element("id", "some-other-component")
# the unmount callback for the old component was called
- driver.find_element_by_id("unmount-flag")
+ driver.find_element("id", "unmount-flag")
def test_module_from_url(driver):
@@ -70,7 +70,7 @@ def ShowSimpleButton():
with ServerMountPoint(PerClientStateServer, app=app) as mount_point:
mount_point.mount(ShowSimpleButton)
driver.get(mount_point.url())
- driver.find_element_by_id("my-button")
+ driver.find_element("id", "my-button")
def test_module_from_template_where_template_does_not_exist():
@@ -108,7 +108,7 @@ def ShowSimpleButton():
display(ShowSimpleButton)
- button = driver.find_element_by_id("my-button")
+ button = driver.find_element("id", "my-button")
button.click()
driver_wait.until(lambda d: is_clicked.current)
@@ -178,11 +178,11 @@ def test_module_exports_multiple_components(driver, display):
display(lambda: Header1({"id": "my-h1"}, "My Header 1"))
- driver.find_element_by_id("my-h1")
+ driver.find_element("id", "my-h1")
display(lambda: Header2({"id": "my-h2"}, "My Header 2"))
- driver.find_element_by_id("my-h2")
+ driver.find_element("id", "my-h2")
def test_imported_components_can_render_children(driver, display):
@@ -199,7 +199,7 @@ def test_imported_components_can_render_children(driver, display):
)
)
- parent = driver.find_element_by_id("the-parent")
+ parent = driver.find_element("id", "the-parent")
children = parent.find_elements_by_tag_name("li")
assert len(children) == 3
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index 40e7512ff..867883186 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -51,13 +51,13 @@ async def on_click(event):
display(ButtonSwapsDivs)
- client_incr_button = driver.find_element_by_id("incr-button")
+ client_incr_button = driver.find_element("id", "incr-button")
- driver.find_element_by_id("hotswap-1")
+ driver.find_element("id", "hotswap-1")
client_incr_button.click()
- driver.find_element_by_id("hotswap-2")
+ driver.find_element("id", "hotswap-2")
client_incr_button.click()
- driver.find_element_by_id("hotswap-3")
+ driver.find_element("id", "hotswap-3")
IMAGE_SRC_BYTES = b"""
@@ -71,14 +71,14 @@ async def on_click(event):
def test_image_from_string(driver, display):
src = IMAGE_SRC_BYTES.decode()
display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"}))
- client_img = driver.find_element_by_id("a-circle-1")
+ client_img = driver.find_element("id", "a-circle-1")
assert BASE64_IMAGE_SRC in client_img.get_attribute("src")
def test_image_from_bytes(driver, display):
src = IMAGE_SRC_BYTES
display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"}))
- client_img = driver.find_element_by_id("a-circle-1")
+ client_img = driver.find_element("id", "a-circle-1")
assert BASE64_IMAGE_SRC in client_img.get_attribute("src")
@@ -94,7 +94,7 @@ def test_input_callback(driver, driver_wait, display):
)
)
- client_inp = driver.find_element_by_id("inp")
+ client_inp = driver.find_element("id", "inp")
assert client_inp.get_attribute("value") == "initial-value"
client_inp.clear()
@@ -132,8 +132,8 @@ def InputWrapper():
display(InputWrapper)
- client_inp_ignore = driver.find_element_by_id("inp-ignore")
- client_inp_not_ignore = driver.find_element_by_id("inp-not-ignore")
+ client_inp_ignore = driver.find_element("id", "inp-ignore")
+ client_inp_not_ignore = driver.find_element("id", "inp-not-ignore")
send_keys(client_inp_ignore, Keys.BACKSPACE)
time.sleep(0.1) # waiting and deleting again seems to decrease flakiness