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 ``