From 30a481ff23a69db3e358c93a2f70f4ec6a1b56c4 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 22 Jul 2021 00:58:30 -0700
Subject: [PATCH 01/36] create barebones templatetags
---
src/django_idom/static/js/idom.js | 2 ++
src/django_idom/templates/idom/head_content.html | 2 ++
src/django_idom/templates/idom/root.html | 4 ++++
src/django_idom/templatetags/__init__.py | 0
src/django_idom/templatetags/tags.py | 15 +++++++++++++++
5 files changed, 23 insertions(+)
create mode 100644 src/django_idom/static/js/idom.js
create mode 100644 src/django_idom/templates/idom/head_content.html
create mode 100644 src/django_idom/templates/idom/root.html
create mode 100644 src/django_idom/templatetags/__init__.py
create mode 100644 src/django_idom/templatetags/tags.py
diff --git a/src/django_idom/static/js/idom.js b/src/django_idom/static/js/idom.js
new file mode 100644
index 00000000..297f9808
--- /dev/null
+++ b/src/django_idom/static/js/idom.js
@@ -0,0 +1,2 @@
+// Build.js is going to need to be moved here via your deployment scripts
+// This will allow developers to import IDOM the Django way
\ No newline at end of file
diff --git a/src/django_idom/templates/idom/head_content.html b/src/django_idom/templates/idom/head_content.html
new file mode 100644
index 00000000..1140b2ab
--- /dev/null
+++ b/src/django_idom/templates/idom/head_content.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/django_idom/templates/idom/root.html b/src/django_idom/templates/idom/root.html
new file mode 100644
index 00000000..fbd81269
--- /dev/null
+++ b/src/django_idom/templates/idom/root.html
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/src/django_idom/templatetags/__init__.py b/src/django_idom/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/django_idom/templatetags/tags.py b/src/django_idom/templatetags/tags.py
new file mode 100644
index 00000000..9f8ffd52
--- /dev/null
+++ b/src/django_idom/templatetags/tags.py
@@ -0,0 +1,15 @@
+from django import template
+from django.urls import reverse
+
+
+register = template.Library()
+
+# Template tag that renders the IDOM scripts
+@register.inclusion_tag("idom/head_content.html")
+def idom_scripts():
+ pass
+
+# Template tag that renders an empty idom root object
+@register.inclusion_tag("idom/root.html")
+def idom_root(html_id):
+ return {"html_id": html_id}
From 8b0b363b1deb7641b25b52d309688c27d3f14106 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Thu, 22 Jul 2021 01:09:28 -0700
Subject: [PATCH 02/36] styling fix
attempt re-adding tag stuff
styling fix
---
src/django_idom/templatetags/{tags.py => idom.py} | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
rename src/django_idom/templatetags/{tags.py => idom.py} (91%)
diff --git a/src/django_idom/templatetags/tags.py b/src/django_idom/templatetags/idom.py
similarity index 91%
rename from src/django_idom/templatetags/tags.py
rename to src/django_idom/templatetags/idom.py
index 9f8ffd52..268b73b0 100644
--- a/src/django_idom/templatetags/tags.py
+++ b/src/django_idom/templatetags/idom.py
@@ -1,14 +1,15 @@
from django import template
-from django.urls import reverse
register = template.Library()
+
# Template tag that renders the IDOM scripts
@register.inclusion_tag("idom/head_content.html")
def idom_scripts():
pass
+
# Template tag that renders an empty idom root object
@register.inclusion_tag("idom/root.html")
def idom_root(html_id):
From 4bcd9bfaea06e8c4e2136a97b0715bad324e3a77 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Sun, 25 Jul 2021 20:06:20 -0700
Subject: [PATCH 03/36] idom_view
---
src/django_idom/templatetags/idom.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 268b73b0..ac0a3686 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -12,5 +12,5 @@ def idom_scripts():
# Template tag that renders an empty idom root object
@register.inclusion_tag("idom/root.html")
-def idom_root(html_id):
+def idom_view(html_id):
return {"html_id": html_id}
From f8f094ee1abeee6d3df835b7d03bee504739233b Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Sun, 25 Jul 2021 21:28:42 -0700
Subject: [PATCH 04/36] skeleton of django installable app
---
.gitignore | 3 +
MANIFEST.in | 0
setup.py | 72 ++++++++++++++++---
src/django_idom/app_settings.py | 3 +
src/django_idom/static/js/idom.js | 2 -
.../templates/idom/head_content.html | 2 +-
src/django_idom/templates/idom/root.html | 6 +-
src/django_idom/templates/idom/view.html | 5 ++
src/django_idom/templatetags/idom.py | 9 +--
src/django_idom/view_loader.py | 29 ++++++++
src/django_idom/websocket_consumer.py | 34 +++++++--
{tests => src}/js/.gitignore | 0
{tests => src}/js/package-lock.json | 0
{tests => src}/js/package.json | 4 +-
{tests => src}/js/rollup.config.js | 2 +-
src/js/src/index.js | 18 +++++
tests/js/src/index.js | 13 ----
tests/test_app/asgi.py | 9 ++-
tests/test_app/idom/__init__.py | 0
tests/test_app/idom/button.py | 6 ++
tests/test_app/idom/hello_world.py | 0
tests/test_app/settings.py | 1 +
22 files changed, 177 insertions(+), 41 deletions(-)
create mode 100644 MANIFEST.in
create mode 100644 src/django_idom/app_settings.py
delete mode 100644 src/django_idom/static/js/idom.js
create mode 100644 src/django_idom/templates/idom/view.html
create mode 100644 src/django_idom/view_loader.py
rename {tests => src}/js/.gitignore (100%)
rename {tests => src}/js/package-lock.json (100%)
rename {tests => src}/js/package.json (90%)
rename {tests => src}/js/rollup.config.js (93%)
create mode 100644 src/js/src/index.js
delete mode 100644 tests/js/src/index.js
create mode 100644 tests/test_app/idom/__init__.py
create mode 100644 tests/test_app/idom/button.py
create mode 100644 tests/test_app/idom/hello_world.py
diff --git a/.gitignore b/.gitignore
index 434278f2..1a06cffb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Django IDOM Build Artifacts
+src/django_idom/static/js
+
# Django #
logs
*.log
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..e69de29b
diff --git a/setup.py b/setup.py
index dbdbbf67..adeb5937 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,25 @@
-import os
+from __future__ import print_function
+
+import pipes
+import shutil
+import subprocess
import sys
+import traceback
+from distutils import log
+from distutils.command.build import build # type: ignore
+from distutils.command.sdist import sdist # type: ignore
from pathlib import Path
from setuptools import find_packages, setup
+from setuptools.command.develop import develop
+
+
+if sys.platform == "win32":
+ from subprocess import list2cmdline
+else:
+
+ def list2cmdline(cmd_list):
+ return " ".join(map(pipes.quote, cmd_list))
# the name of the project
@@ -31,6 +48,7 @@
"license": "MIT",
"platforms": "Linux, Mac OS X, Windows",
"keywords": ["interactive", "widgets", "DOM", "React"],
+ "include_package_data": True,
"zip_safe": False,
"classifiers": [
"Framework :: Django",
@@ -52,14 +70,14 @@
# Library Version
# -----------------------------------------------------------------------------
-with open(os.path.join(package_dir, "__init__.py")) as f:
- for line in f.read().split("\n"):
- if line.startswith("__version__ = "):
- package["version"] = eval(line.split("=", 1)[1])
- break
- else:
- print("No version found in %s/__init__.py" % package_dir)
- sys.exit(1)
+
+for line in (package_dir / "__init__.py").read_text().split("\n"):
+ if line.startswith("__version__ = "):
+ package["version"] = eval(line.split("=", 1)[1])
+ break
+else:
+ print("No version found in %s/__init__.py" % package_dir)
+ sys.exit(1)
# -----------------------------------------------------------------------------
@@ -87,6 +105,42 @@
package["long_description_content_type"] = "text/markdown"
+# ----------------------------------------------------------------------------
+# Build Javascript
+# ----------------------------------------------------------------------------
+
+
+def build_javascript_first(cls):
+ class Command(cls):
+ def run(self):
+ log.info("Installing Javascript...")
+ try:
+ js_dir = str(src_dir / "js")
+ npm = shutil.which("npm") # this is required on windows
+ if npm is None:
+ raise RuntimeError("NPM is not installed.")
+ for args in (f"{npm} install", f"{npm} run build"):
+ args_list = args.split()
+ log.info(f"> {list2cmdline(args_list)}")
+ subprocess.run(args_list, cwd=js_dir, check=True)
+ except Exception:
+ log.error("Failed to install Javascript")
+ log.error(traceback.format_exc())
+ raise
+ else:
+ log.info("Successfully installed Javascript")
+ super().run()
+
+ return Command
+
+
+package["cmdclass"] = {
+ "sdist": build_javascript_first(sdist),
+ "build": build_javascript_first(build),
+ "develop": build_javascript_first(develop),
+}
+
+
# -----------------------------------------------------------------------------
# Install It
# -----------------------------------------------------------------------------
diff --git a/src/django_idom/app_settings.py b/src/django_idom/app_settings.py
new file mode 100644
index 00000000..074e9cb1
--- /dev/null
+++ b/src/django_idom/app_settings.py
@@ -0,0 +1,3 @@
+from django.conf import settings
+
+IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
diff --git a/src/django_idom/static/js/idom.js b/src/django_idom/static/js/idom.js
deleted file mode 100644
index 297f9808..00000000
--- a/src/django_idom/static/js/idom.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// Build.js is going to need to be moved here via your deployment scripts
-// This will allow developers to import IDOM the Django way
\ No newline at end of file
diff --git a/src/django_idom/templates/idom/head_content.html b/src/django_idom/templates/idom/head_content.html
index 1140b2ab..27a0a4b4 100644
--- a/src/django_idom/templates/idom/head_content.html
+++ b/src/django_idom/templates/idom/head_content.html
@@ -1,2 +1,2 @@
-
\ No newline at end of file
+
diff --git a/src/django_idom/templates/idom/root.html b/src/django_idom/templates/idom/root.html
index fbd81269..958b1669 100644
--- a/src/django_idom/templates/idom/root.html
+++ b/src/django_idom/templates/idom/root.html
@@ -1,4 +1,8 @@
-
\ No newline at end of file
+
+
diff --git a/src/django_idom/templates/idom/view.html b/src/django_idom/templates/idom/view.html
new file mode 100644
index 00000000..8280a871
--- /dev/null
+++ b/src/django_idom/templates/idom/view.html
@@ -0,0 +1,5 @@
+
+
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index ac0a3686..35763778 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -1,5 +1,7 @@
from django import template
+from django_idom.app_settings import IDOM_WEBSOCKET_URL
+
register = template.Library()
@@ -10,7 +12,6 @@ def idom_scripts():
pass
-# Template tag that renders an empty idom root object
-@register.inclusion_tag("idom/root.html")
-def idom_view(html_id):
- return {"html_id": html_id}
+@register.inclusion_tag("idom/view.html")
+def idom_view(view_id):
+ return {"idom_websocket_url": IDOM_WEBSOCKET_URL, "view_id": view_id}
diff --git a/src/django_idom/view_loader.py b/src/django_idom/view_loader.py
new file mode 100644
index 00000000..f1eec9f9
--- /dev/null
+++ b/src/django_idom/view_loader.py
@@ -0,0 +1,29 @@
+import logging
+from importlib import import_module
+from pathlib import Path
+from typing import Dict
+
+from django.conf import settings
+from idom.core.proto import ComponentConstructor
+
+
+ALL_VIEWS: Dict[str, ComponentConstructor] = {}
+logger = logging.getLogger(__name__)
+
+for app_name in settings.INSTALLED_APPS:
+ app_mod = import_module(app_name)
+ if not hasattr(app_mod, "idom"):
+ continue
+
+ for idom_view_path in Path(app_mod.__file__).iterdir():
+ if idom_view_path.suffix == ".py" and idom_view_path.is_file():
+ idom_view_mod_name = ".".join([app_name, "idom", idom_view_path.stem])
+ idom_view_mod = import_module(idom_view_mod_name)
+
+ if hasattr(idom_view_mod, "Root") and callable(idom_view_mod.Root):
+ ALL_VIEWS[idom_view_mod_name] = idom_view_mod.Root
+ else:
+ logger.warning(
+ f"Expected module {idom_view_mod_name} to expose a 'Root' "
+ " attribute that is an IDOM component."
+ )
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py
index 2fae2869..329e9ddd 100644
--- a/src/django_idom/websocket_consumer.py
+++ b/src/django_idom/websocket_consumer.py
@@ -1,20 +1,22 @@
"""Anything used to construct a websocket endpoint"""
import asyncio
+import logging
from typing import Any
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from idom.core.dispatcher import dispatch_single_view
from idom.core.layout import Layout, LayoutEvent
-from idom.core.proto import ComponentConstructor
+
+from .view_loader import ALL_VIEWS
+
+
+logger = logging.getLogger(__name__)
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""
- def __init__(
- self, component: ComponentConstructor, *args: Any, **kwargs: Any
- ) -> None:
- self._idom_component_constructor = component
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
async def connect(self) -> None:
@@ -32,10 +34,30 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None:
await self._idom_recv_queue.put(LayoutEvent(**content))
async def _run_dispatch_loop(self):
+ # get the URL parameters and grab the view ID
+ view_id = ...
+ # get component ags from the URL params too
+ component_args = ...
+
+ if view_id not in ALL_VIEWS:
+ logger.warning(f"Uknown IDOM view ID {view_id}")
+ return
+
+ component_constructor = ALL_VIEWS[view_id]
+
+ try:
+ component_instance = component_constructor(*component_args)
+ except Exception:
+ logger.exception(
+ f"Failed to construct component {component_constructor} "
+ f"with parameters {component_args}"
+ )
+ return
+
self._idom_recv_queue = recv_queue = asyncio.Queue()
try:
await dispatch_single_view(
- Layout(self._idom_component_constructor()),
+ Layout(component_instance),
self.send_json,
recv_queue.get,
)
diff --git a/tests/js/.gitignore b/src/js/.gitignore
similarity index 100%
rename from tests/js/.gitignore
rename to src/js/.gitignore
diff --git a/tests/js/package-lock.json b/src/js/package-lock.json
similarity index 100%
rename from tests/js/package-lock.json
rename to src/js/package-lock.json
diff --git a/tests/js/package.json b/src/js/package.json
similarity index 90%
rename from tests/js/package.json
rename to src/js/package.json
index 3844cf32..832480a8 100644
--- a/tests/js/package.json
+++ b/src/js/package.json
@@ -1,6 +1,6 @@
{
- "name": "tests",
- "version": "1.0.0",
+ "name": "django-idom-client",
+ "version": "0.0.1",
"description": "test app for idom_django websocket server",
"main": "src/index.js",
"files": [
diff --git a/tests/js/rollup.config.js b/src/js/rollup.config.js
similarity index 93%
rename from tests/js/rollup.config.js
rename to src/js/rollup.config.js
index ad597a61..2c1bb55f 100644
--- a/tests/js/rollup.config.js
+++ b/src/js/rollup.config.js
@@ -7,7 +7,7 @@ const { PRODUCTION } = process.env;
export default {
input: "src/index.js",
output: {
- file: "../test_app/static/build.js",
+ file: "../django_idom/static/js/idom.js",
format: "esm",
},
plugins: [
diff --git a/src/js/src/index.js b/src/js/src/index.js
new file mode 100644
index 00000000..33df4ba6
--- /dev/null
+++ b/src/js/src/index.js
@@ -0,0 +1,18 @@
+import { mountLayoutWithWebSocket } from "idom-client-react";
+
+
+// Set up a websocket at the base endpoint
+let LOCATION = window.location;
+let WS_PROTOCOL = "";
+if (LOCATION.protocol == "https:") {
+ WS_PROTOCOL = "wss://";
+} else {
+ WS_PROTOCOL = "ws://";
+}
+let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
+
+
+export function mountViewToElement(idomWebsocketUrl, viewId) {
+ const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl
+ mountLayoutWithWebSocket(document.getElementById(viewId), fullWebsocketUrl);
+}
diff --git a/tests/js/src/index.js b/tests/js/src/index.js
deleted file mode 100644
index 613515dc..00000000
--- a/tests/js/src/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { mountLayoutWithWebSocket } from "idom-client-react";
-
-// Set up a websocket at the base endpoint
-let LOCATION = window.location;
-let WS_PROTOCOL = "";
-if (LOCATION.protocol == "https:") {
- WS_PROTOCOL = "wss://";
-} else {
- WS_PROTOCOL = "ws://";
-}
-let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host;
-
-mountLayoutWithWebSocket(document.getElementById("mount"), WS_ENDPOINT_URL);
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
index 113b147f..c0355bef 100644
--- a/tests/test_app/asgi.py
+++ b/tests/test_app/asgi.py
@@ -9,10 +9,12 @@
import os
+from django.conf import settings
from django.conf.urls import url
from django.core.asgi import get_asgi_application
-from django_idom import IdomAsyncWebSocketConsumer # noqa: E402
+from django_idom import IdomAsyncWebSocketConsumer
+from django_idom.app_settings import IDOM_WEBSOCKET_URL # noqa: E402
from .views import Root
@@ -25,11 +27,14 @@
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
+IDOM_WEBSOCKET_URL = settings.IDOM_WEBSOCKET_URL
+
+
application = ProtocolTypeRouter(
{
"http": http_asgi_app,
"websocket": URLRouter(
- [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))]
+ [url(IDOM_WEBSOCKET_URL, IdomAsyncWebSocketConsumer.as_asgi())]
),
}
)
diff --git a/tests/test_app/idom/__init__.py b/tests/test_app/idom/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/idom/button.py b/tests/test_app/idom/button.py
new file mode 100644
index 00000000..54866181
--- /dev/null
+++ b/tests/test_app/idom/button.py
@@ -0,0 +1,6 @@
+import idom
+
+
+@idom.component
+def Root():
+ ...
diff --git a/tests/test_app/idom/hello_world.py b/tests/test_app/idom/hello_world.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 73eda0fa..ac7d6ab9 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -37,6 +37,7 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"channels", # Websocket library
+ "django_idom", # Django compatible IDOM client
"test_app", # This test application
]
MIDDLEWARE = [
From 937da205da8f8555a232fd5a4b486dbdfc40d15e Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Mon, 26 Jul 2021 01:32:57 -0700
Subject: [PATCH 05/36] get test_app runserver to work
---
MANIFEST.in | 3 ++
noxfile.py | 1 -
src/django_idom/__init__.py | 7 ++-
src/django_idom/app_components.py | 50 +++++++++++++++++++
src/django_idom/app_settings.py | 3 ++
src/django_idom/templates/idom/root.html | 8 ---
src/django_idom/templates/idom/view.html | 13 +++--
src/django_idom/templatetags/idom.py | 13 +++--
src/django_idom/view_loader.py | 29 -----------
src/django_idom/websocket_consumer.py | 36 +++++++++----
src/js/package-lock.json | 8 +--
src/js/src/index.js | 6 +--
tests/.gitignore | 2 +-
tests/test_app/asgi.py | 14 +-----
tests/test_app/idom.py | 24 +++++++++
tests/test_app/idom/__init__.py | 0
tests/test_app/idom/button.py | 6 ---
tests/test_app/idom/hello_world.py | 0
tests/test_app/management/__init__.py | 0
.../test_app/management/commands/__init__.py | 0
.../test_app/management/commands/build_js.py | 16 ------
tests/test_app/templates/base.html | 6 +--
tests/test_app/views.py | 26 ----------
23 files changed, 144 insertions(+), 127 deletions(-)
create mode 100644 src/django_idom/app_components.py
delete mode 100644 src/django_idom/templates/idom/root.html
delete mode 100644 src/django_idom/view_loader.py
create mode 100644 tests/test_app/idom.py
delete mode 100644 tests/test_app/idom/__init__.py
delete mode 100644 tests/test_app/idom/button.py
delete mode 100644 tests/test_app/idom/hello_world.py
delete mode 100644 tests/test_app/management/__init__.py
delete mode 100644 tests/test_app/management/commands/__init__.py
delete mode 100644 tests/test_app/management/commands/build_js.py
diff --git a/MANIFEST.in b/MANIFEST.in
index e69de29b..1faaf63a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include src/django_idom/static/js/idom.js
+include src/django_idom/templates/head_content.html
+include src/django_idom/templates/view.html
diff --git a/noxfile.py b/noxfile.py
index 4a943345..12ee62fa 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -52,7 +52,6 @@ def test_suite(session: Session) -> None:
session.chdir(HERE / "tests")
session.env["IDOM_DEBUG_MODE"] = "1"
session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs))
- session.run("python", "manage.py", "build_js")
session.run("python", "manage.py", "test")
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 60932c7a..cc31e4dc 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,5 +1,8 @@
-from .websocket_consumer import IdomAsyncWebSocketConsumer
+from .websocket_consumer import (
+ IdomAsyncWebSocketConsumer,
+ django_idom_websocket_consumer_url,
+)
__version__ = "0.0.1"
-__all__ = ["IdomAsyncWebSocketConsumer"]
+__all__ = ["IdomAsyncWebSocketConsumer", "django_idom_websocket_consumer_url"]
diff --git a/src/django_idom/app_components.py b/src/django_idom/app_components.py
new file mode 100644
index 00000000..f41e760f
--- /dev/null
+++ b/src/django_idom/app_components.py
@@ -0,0 +1,50 @@
+import logging
+from importlib import import_module
+from typing import Dict
+
+from django.conf import settings
+from idom.core.proto import ComponentConstructor
+
+
+logger = logging.getLogger(__name__)
+_LOADED_COMPONENTS: Dict[str, ComponentConstructor] = {}
+
+
+def get_component(name: str) -> ComponentConstructor:
+ return _LOADED_COMPONENTS[name]
+
+
+def has_component(name: str) -> bool:
+ return name in _LOADED_COMPONENTS
+
+
+for app_mod_name in settings.INSTALLED_APPS:
+ idom_mod_name = f"{app_mod_name}.idom"
+
+ try:
+ idom_mod = import_module(idom_mod_name)
+ except ImportError:
+ logger.debug(f"Skipping {idom_mod_name!r} - does not exist")
+ continue
+
+ if not hasattr(idom_mod, "__all__"):
+ logger.warning(
+ f"'django_idom' expected module {idom_mod_name!r} to have an "
+ "'__all__' attribute that lists its publically available components."
+ )
+ continue
+
+ for component_name in idom_mod.__all__:
+ try:
+ component_constructor = getattr(idom_mod, component_name)
+ except AttributeError:
+ logger.warning(
+ f"Module {idom_mod_name!r} has no attribute {component_name!r}"
+ )
+ continue
+
+ if not callable(component_constructor):
+ logger.warning(f"'{idom_mod_name}.{component_name}' is not a component")
+ continue
+
+ _LOADED_COMPONENTS[f"{app_mod_name}.{component_name}"] = component_constructor
diff --git a/src/django_idom/app_settings.py b/src/django_idom/app_settings.py
index 074e9cb1..6c586004 100644
--- a/src/django_idom/app_settings.py
+++ b/src/django_idom/app_settings.py
@@ -1,3 +1,6 @@
from django.conf import settings
+
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
+if not IDOM_WEBSOCKET_URL.endswith("/"):
+ IDOM_WEBSOCKET_URL += "/"
diff --git a/src/django_idom/templates/idom/root.html b/src/django_idom/templates/idom/root.html
deleted file mode 100644
index 958b1669..00000000
--- a/src/django_idom/templates/idom/root.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
diff --git a/src/django_idom/templates/idom/view.html b/src/django_idom/templates/idom/view.html
index 8280a871..02cc17ec 100644
--- a/src/django_idom/templates/idom/view.html
+++ b/src/django_idom/templates/idom/view.html
@@ -1,5 +1,12 @@
-
+{% load static %}
+
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 35763778..3f62edba 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -1,3 +1,5 @@
+from uuid import uuid4
+
from django import template
from django_idom.app_settings import IDOM_WEBSOCKET_URL
@@ -8,10 +10,15 @@
# Template tag that renders the IDOM scripts
@register.inclusion_tag("idom/head_content.html")
-def idom_scripts():
+def idom_head():
pass
@register.inclusion_tag("idom/view.html")
-def idom_view(view_id):
- return {"idom_websocket_url": IDOM_WEBSOCKET_URL, "view_id": view_id}
+def idom_view(view_id, view_params=""):
+ return {
+ "idom_websocket_url": IDOM_WEBSOCKET_URL,
+ "idom_mount_uuid": uuid4().hex,
+ "idom_view_id": view_id,
+ "idom_view_params": view_params,
+ }
diff --git a/src/django_idom/view_loader.py b/src/django_idom/view_loader.py
deleted file mode 100644
index f1eec9f9..00000000
--- a/src/django_idom/view_loader.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import logging
-from importlib import import_module
-from pathlib import Path
-from typing import Dict
-
-from django.conf import settings
-from idom.core.proto import ComponentConstructor
-
-
-ALL_VIEWS: Dict[str, ComponentConstructor] = {}
-logger = logging.getLogger(__name__)
-
-for app_name in settings.INSTALLED_APPS:
- app_mod = import_module(app_name)
- if not hasattr(app_mod, "idom"):
- continue
-
- for idom_view_path in Path(app_mod.__file__).iterdir():
- if idom_view_path.suffix == ".py" and idom_view_path.is_file():
- idom_view_mod_name = ".".join([app_name, "idom", idom_view_path.stem])
- idom_view_mod = import_module(idom_view_mod_name)
-
- if hasattr(idom_view_mod, "Root") and callable(idom_view_mod.Root):
- ALL_VIEWS[idom_view_mod_name] = idom_view_mod.Root
- else:
- logger.warning(
- f"Expected module {idom_view_mod_name} to expose a 'Root' "
- " attribute that is an IDOM component."
- )
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py
index 329e9ddd..841dc4ee 100644
--- a/src/django_idom/websocket_consumer.py
+++ b/src/django_idom/websocket_consumer.py
@@ -2,17 +2,35 @@
import asyncio
import logging
from typing import Any
+from urllib.parse import parse_qsl
from channels.generic.websocket import AsyncJsonWebsocketConsumer
+from django.urls import path
from idom.core.dispatcher import dispatch_single_view
from idom.core.layout import Layout, LayoutEvent
-from .view_loader import ALL_VIEWS
+from .app_components import get_component, has_component
+from .app_settings import IDOM_WEBSOCKET_URL
logger = logging.getLogger(__name__)
+def django_idom_websocket_consumer_url(*args, **kwargs):
+ """Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
+
+ While this is relatively uncommon in most Django apps, because the URL of the
+ websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
+ to allow users to configure the URL themselves
+ """
+ return path(
+ IDOM_WEBSOCKET_URL + "/",
+ IdomAsyncWebSocketConsumer.as_asgi(),
+ *args,
+ **kwargs,
+ )
+
+
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""
@@ -34,23 +52,21 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None:
await self._idom_recv_queue.put(LayoutEvent(**content))
async def _run_dispatch_loop(self):
- # get the URL parameters and grab the view ID
- view_id = ...
- # get component ags from the URL params too
- component_args = ...
+ view_id = self.scope["url_route"]["kwargs"]["view_id"]
- if view_id not in ALL_VIEWS:
- logger.warning(f"Uknown IDOM view ID {view_id}")
+ if not has_component(view_id):
+ logger.warning(f"Uknown IDOM view ID {view_id!r}")
return
- component_constructor = ALL_VIEWS[view_id]
+ component_constructor = get_component(view_id)
+ component_kwargs = dict(parse_qsl(self.scope["query_string"]))
try:
- component_instance = component_constructor(*component_args)
+ component_instance = component_constructor(**component_kwargs)
except Exception:
logger.exception(
f"Failed to construct component {component_constructor} "
- f"with parameters {component_args}"
+ f"with parameters {component_kwargs}"
)
return
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index 6639bf96..72e7483c 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "tests",
- "version": "1.0.0",
+ "name": "django-idom-client",
+ "version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "tests",
- "version": "1.0.0",
+ "name": "django-idom-client",
+ "version": "0.0.1",
"dependencies": {
"idom-client-react": "^0.8.2"
},
diff --git a/src/js/src/index.js b/src/js/src/index.js
index 33df4ba6..a8a16bb3 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -12,7 +12,7 @@ if (LOCATION.protocol == "https:") {
let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
-export function mountViewToElement(idomWebsocketUrl, viewId) {
- const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl
- mountLayoutWithWebSocket(document.getElementById(viewId), fullWebsocketUrl);
+export function mountViewToElement(mountPoint, idomWebsocketUrl, viewId, queryParams) {
+ const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/";
+ mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl);
}
diff --git a/tests/.gitignore b/tests/.gitignore
index bf4a6d58..60615839 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -1 +1 @@
-test_app/static/build.js
+*.sqlite3
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
index c0355bef..e9faad98 100644
--- a/tests/test_app/asgi.py
+++ b/tests/test_app/asgi.py
@@ -9,14 +9,9 @@
import os
-from django.conf import settings
-from django.conf.urls import url
from django.core.asgi import get_asgi_application
-from django_idom import IdomAsyncWebSocketConsumer
-from django_idom.app_settings import IDOM_WEBSOCKET_URL # noqa: E402
-
-from .views import Root
+from django_idom import django_idom_websocket_consumer_url
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
@@ -27,14 +22,9 @@
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
-IDOM_WEBSOCKET_URL = settings.IDOM_WEBSOCKET_URL
-
-
application = ProtocolTypeRouter(
{
"http": http_asgi_app,
- "websocket": URLRouter(
- [url(IDOM_WEBSOCKET_URL, IdomAsyncWebSocketConsumer.as_asgi())]
- ),
+ "websocket": URLRouter([django_idom_websocket_consumer_url()]),
}
)
diff --git a/tests/test_app/idom.py b/tests/test_app/idom.py
new file mode 100644
index 00000000..1f6b0d8d
--- /dev/null
+++ b/tests/test_app/idom.py
@@ -0,0 +1,24 @@
+import idom
+
+
+__all__ = "HelloWorld", "Button"
+
+
+@idom.component
+def HelloWorld():
+ return idom.html.h1({"id": "hello-world"}, "Hello World!")
+
+
+@idom.component
+def Button():
+ count, set_count = idom.hooks.use_state(0)
+ return idom.html.div(
+ idom.html.button(
+ {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
+ "Click me!",
+ ),
+ idom.html.p(
+ {"id": "counter-num", "data-count": count},
+ f"Current count is: {count}",
+ ),
+ )
diff --git a/tests/test_app/idom/__init__.py b/tests/test_app/idom/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/test_app/idom/button.py b/tests/test_app/idom/button.py
deleted file mode 100644
index 54866181..00000000
--- a/tests/test_app/idom/button.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import idom
-
-
-@idom.component
-def Root():
- ...
diff --git a/tests/test_app/idom/hello_world.py b/tests/test_app/idom/hello_world.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/test_app/management/__init__.py b/tests/test_app/management/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/test_app/management/commands/__init__.py b/tests/test_app/management/commands/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py
deleted file mode 100644
index 61af5ed9..00000000
--- a/tests/test_app/management/commands/build_js.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import subprocess
-from pathlib import Path
-
-from django.core.management.base import BaseCommand
-
-
-HERE = Path(__file__).parent
-JS_DIR = HERE.parent.parent.parent / "js"
-
-
-class Command(BaseCommand):
- help = "Build javascript source for test app"
-
- def handle(self, *args, **options):
- subprocess.run(["npm", "install"], cwd=JS_DIR, check=True)
- subprocess.run(["npm", "run", "build"], cwd=JS_DIR, check=True)
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index d2a88607..e3180b1d 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -1,4 +1,4 @@
-{% load static %}
+{% load static %} {% load idom %}
@@ -15,7 +15,7 @@
IDOM Test Page
-
-
+ {% idom_view "test_app.HelloWorld" %}
+ {% idom_view "test_app.Button" %}
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index a996eb1e..eb73ee53 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -1,4 +1,3 @@
-import idom
from django.http import HttpResponse
from django.template import loader
@@ -7,28 +6,3 @@ def base_template(request):
template = loader.get_template("base.html")
context = {}
return HttpResponse(template.render(context, request))
-
-
-@idom.component
-def Root():
- return idom.html.div(HelloWorld(), Counter())
-
-
-@idom.component
-def HelloWorld():
- return idom.html.h1({"id": "hello-world"}, "Hello World!")
-
-
-@idom.component
-def Counter():
- count, set_count = idom.hooks.use_state(0)
- return idom.html.div(
- idom.html.button(
- {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
- "Click me!",
- ),
- idom.html.p(
- {"id": "counter-num", "data-count": count},
- f"Current count is: {count}",
- ),
- )
From e76408b42bcc93d776c41cea74b03899f528f909 Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Wed, 28 Jul 2021 01:05:37 -0700
Subject: [PATCH 06/36] fix MANIFEST.in to include static/templates
---
MANIFEST.in | 5 ++---
noxfile.py | 12 ++++++++++--
src/django_idom/app_settings.py | 8 ++++++++
src/django_idom/templatetags/idom.py | 6 +++---
4 files changed, 23 insertions(+), 8 deletions(-)
diff --git a/MANIFEST.in b/MANIFEST.in
index 1faaf63a..a43e500c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,2 @@
-include src/django_idom/static/js/idom.js
-include src/django_idom/templates/head_content.html
-include src/django_idom/templates/view.html
+recursive-include src/django_idom/static/ *
+recursive-include src/django_idom/templates/ *.html
diff --git a/noxfile.py b/noxfile.py
index 12ee62fa..ebcd38de 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -51,8 +51,16 @@ def test_suite(session: Session) -> None:
session.chdir(HERE / "tests")
session.env["IDOM_DEBUG_MODE"] = "1"
- session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs))
- session.run("python", "manage.py", "test")
+
+ posargs = session.posargs[:]
+ if "--headless" in posargs:
+ posargs.remove("--headless")
+ session.env["SELENIUM_HEADLESS"] = "1"
+
+ if "--no-debug-mode" not in posargs:
+ posargs.append("--debug-mode")
+
+ session.run("python", "manage.py", "test", *posargs)
@nox.session
diff --git a/src/django_idom/app_settings.py b/src/django_idom/app_settings.py
index 6c586004..f97fefed 100644
--- a/src/django_idom/app_settings.py
+++ b/src/django_idom/app_settings.py
@@ -1,5 +1,13 @@
+from pathlib import Path
+
from django.conf import settings
+APP_DIR = Path(__file__).parent
+
+TEMPLATE_FILE_PATHS = {
+ file.stem: str(file.absolute())
+ for file in (APP_DIR / "templates" / "idom").iterdir()
+}
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
if not IDOM_WEBSOCKET_URL.endswith("/"):
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 3f62edba..8e3d50f4 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -2,19 +2,19 @@
from django import template
-from django_idom.app_settings import IDOM_WEBSOCKET_URL
+from django_idom.app_settings import IDOM_WEBSOCKET_URL, TEMPLATE_FILE_PATHS
register = template.Library()
# Template tag that renders the IDOM scripts
-@register.inclusion_tag("idom/head_content.html")
+@register.inclusion_tag(TEMPLATE_FILE_PATHS["head_content"])
def idom_head():
pass
-@register.inclusion_tag("idom/view.html")
+@register.inclusion_tag(TEMPLATE_FILE_PATHS["view"])
def idom_view(view_id, view_params=""):
return {
"idom_websocket_url": IDOM_WEBSOCKET_URL,
From f008f6bdcfe4bb0c510de7facd7a29ffdee1359b Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Wed, 28 Jul 2021 01:17:49 -0700
Subject: [PATCH 07/36] fix style
---
src/django_idom/app_settings.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/django_idom/app_settings.py b/src/django_idom/app_settings.py
index f97fefed..cb93910b 100644
--- a/src/django_idom/app_settings.py
+++ b/src/django_idom/app_settings.py
@@ -2,6 +2,7 @@
from django.conf import settings
+
APP_DIR = Path(__file__).parent
TEMPLATE_FILE_PATHS = {
From b4f256b7b39b591b9ebeb1c91c041501278d323c Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Wed, 28 Jul 2021 01:57:30 -0700
Subject: [PATCH 08/36] require a my_app.idom.components attribute
---
src/django_idom/app_components.py | 20 +++++++++++---------
tests/test_app/components.py | 21 +++++++++++++++++++++
tests/test_app/idom.py | 27 +++++----------------------
3 files changed, 37 insertions(+), 31 deletions(-)
create mode 100644 tests/test_app/components.py
diff --git a/src/django_idom/app_components.py b/src/django_idom/app_components.py
index f41e760f..1b129d6c 100644
--- a/src/django_idom/app_components.py
+++ b/src/django_idom/app_components.py
@@ -27,24 +27,26 @@ def has_component(name: str) -> bool:
logger.debug(f"Skipping {idom_mod_name!r} - does not exist")
continue
- if not hasattr(idom_mod, "__all__"):
+ if not hasattr(idom_mod, "components"):
logger.warning(
f"'django_idom' expected module {idom_mod_name!r} to have an "
- "'__all__' attribute that lists its publically available components."
+ "'components' attribute that lists its publically available components."
)
continue
- for component_name in idom_mod.__all__:
- try:
- component_constructor = getattr(idom_mod, component_name)
- except AttributeError:
+ for component_constructor in idom_mod.components:
+ if not callable(component_constructor):
logger.warning(
- f"Module {idom_mod_name!r} has no attribute {component_name!r}"
+ f"{component_constructor} is not a callable component constructor"
)
continue
- if not callable(component_constructor):
- logger.warning(f"'{idom_mod_name}.{component_name}' is not a component")
+ try:
+ component_name = getattr(component_constructor, "__name__")
+ except AttributeError:
+ logger.warning(
+ f"Component constructor {component_constructor} has not attribute '__name__'"
+ )
continue
_LOADED_COMPONENTS[f"{app_mod_name}.{component_name}"] = component_constructor
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
new file mode 100644
index 00000000..c0042570
--- /dev/null
+++ b/tests/test_app/components.py
@@ -0,0 +1,21 @@
+import idom
+
+
+@idom.component
+def HelloWorld():
+ return idom.html.h1({"id": "hello-world"}, "Hello World!")
+
+
+@idom.component
+def Button():
+ count, set_count = idom.hooks.use_state(0)
+ return idom.html.div(
+ idom.html.button(
+ {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
+ "Click me!",
+ ),
+ idom.html.p(
+ {"id": "counter-num", "data-count": count},
+ f"Current count is: {count}",
+ ),
+ )
diff --git a/tests/test_app/idom.py b/tests/test_app/idom.py
index 1f6b0d8d..007f171e 100644
--- a/tests/test_app/idom.py
+++ b/tests/test_app/idom.py
@@ -1,24 +1,7 @@
-import idom
+from .components import Button, HelloWorld
-__all__ = "HelloWorld", "Button"
-
-
-@idom.component
-def HelloWorld():
- return idom.html.h1({"id": "hello-world"}, "Hello World!")
-
-
-@idom.component
-def Button():
- count, set_count = idom.hooks.use_state(0)
- return idom.html.div(
- idom.html.button(
- {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
- "Click me!",
- ),
- idom.html.p(
- {"id": "counter-num", "data-count": count},
- f"Current count is: {count}",
- ),
- )
+components = [
+ HelloWorld,
+ Button,
+]
From a0b75e03f35f2473b7ec4bca48c4ead0f6032adc Mon Sep 17 00:00:00 2001
From: rmorshea
Date: Wed, 28 Jul 2021 16:56:42 -0700
Subject: [PATCH 09/36] parametrized components + serve web modules
---
src/django_idom/__init__.py | 7 ++----
src/django_idom/app_paths.py | 29 ++++++++++++++++++++++++
src/django_idom/app_settings.py | 6 ++---
src/django_idom/templates/idom/view.html | 1 +
src/django_idom/templatetags/idom.py | 21 +++++++++++++----
src/django_idom/views.py | 7 ++++++
src/django_idom/websocket_consumer.py | 22 ++++--------------
src/js/package-lock.json | 18 +++++++--------
src/js/package.json | 6 ++---
src/js/src/index.js | 21 +++++++++++++----
tests/test_app/asgi.py | 4 ++--
tests/test_app/components.py | 15 ++++++++++++
tests/test_app/idom.py | 4 +++-
tests/test_app/templates/base.html | 2 ++
tests/test_app/tests.py | 18 ++++++++++++++-
tests/test_app/urls.py | 7 +++++-
16 files changed, 137 insertions(+), 51 deletions(-)
create mode 100644 src/django_idom/app_paths.py
create mode 100644 src/django_idom/views.py
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index cc31e4dc..23c959aa 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,8 +1,5 @@
-from .websocket_consumer import (
- IdomAsyncWebSocketConsumer,
- django_idom_websocket_consumer_url,
-)
+from .app_paths import django_idom_web_modules_path, django_idom_websocket_consumer_path
__version__ = "0.0.1"
-__all__ = ["IdomAsyncWebSocketConsumer", "django_idom_websocket_consumer_url"]
+__all__ = ["django_idom_websocket_consumer_path", "django_idom_web_modules_path"]
diff --git a/src/django_idom/app_paths.py b/src/django_idom/app_paths.py
new file mode 100644
index 00000000..2afc1d2f
--- /dev/null
+++ b/src/django_idom/app_paths.py
@@ -0,0 +1,29 @@
+from django.urls import path
+
+from . import views
+from .app_settings import IDOM_WEB_MODULES_URL, IDOM_WEBSOCKET_URL
+from .websocket_consumer import IdomAsyncWebSocketConsumer
+
+
+def django_idom_websocket_consumer_path(*args, **kwargs):
+ """Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
+
+ While this is relatively uncommon in most Django apps, because the URL of the
+ websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
+ to allow users to configure the URL themselves
+ """
+ return path(
+ IDOM_WEBSOCKET_URL + "/",
+ IdomAsyncWebSocketConsumer.as_asgi(),
+ *args,
+ **kwargs,
+ )
+
+
+def django_idom_web_modules_path(*args, **kwargs):
+ return path(
+ IDOM_WEB_MODULES_URL + "",
+ views.web_modules_file,
+ *args,
+ **kwargs,
+ )
diff --git a/src/django_idom/app_settings.py b/src/django_idom/app_settings.py
index cb93910b..14b9f07e 100644
--- a/src/django_idom/app_settings.py
+++ b/src/django_idom/app_settings.py
@@ -10,6 +10,6 @@
for file in (APP_DIR / "templates" / "idom").iterdir()
}
-IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
-if not IDOM_WEBSOCKET_URL.endswith("/"):
- IDOM_WEBSOCKET_URL += "/"
+IDOM_BASE_URL = getattr(settings, "IDOM_BASE_URL", "_idom/")
+IDOM_WEBSOCKET_URL = IDOM_BASE_URL + "websocket/"
+IDOM_WEB_MODULES_URL = IDOM_BASE_URL + "web_module/"
diff --git a/src/django_idom/templates/idom/view.html b/src/django_idom/templates/idom/view.html
index 02cc17ec..f11534ef 100644
--- a/src/django_idom/templates/idom/view.html
+++ b/src/django_idom/templates/idom/view.html
@@ -6,6 +6,7 @@
mountViewToElement(
mountPoint,
"{{ idom_websocket_url }}",
+ "{{ idom_web_modules_url }}",
"{{ idom_view_id }}",
"{{ idom_view_params }}"
);
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 8e3d50f4..da941f76 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -1,8 +1,15 @@
+import json
+from urllib.parse import urlencode
from uuid import uuid4
from django import template
-from django_idom.app_settings import IDOM_WEBSOCKET_URL, TEMPLATE_FILE_PATHS
+from django_idom.app_settings import (
+ IDOM_WEB_MODULES_URL,
+ IDOM_WEBSOCKET_URL,
+ TEMPLATE_FILE_PATHS,
+)
+from ..app_components import has_component
register = template.Library()
@@ -15,10 +22,16 @@ def idom_head():
@register.inclusion_tag(TEMPLATE_FILE_PATHS["view"])
-def idom_view(view_id, view_params=""):
+def idom_view(_component_id_, **kwargs):
+ if not has_component(_component_id_):
+ raise ValueError(f"No component {_component_id_!r} exists")
+
+ json_kwargs = json.dumps(kwargs, separators=(",", ":"))
+
return {
"idom_websocket_url": IDOM_WEBSOCKET_URL,
+ "idom_web_modules_url": IDOM_WEB_MODULES_URL,
"idom_mount_uuid": uuid4().hex,
- "idom_view_id": view_id,
- "idom_view_params": view_params,
+ "idom_view_id": _component_id_,
+ "idom_view_params": urlencode({"kwargs": json_kwargs}),
}
diff --git a/src/django_idom/views.py b/src/django_idom/views.py
new file mode 100644
index 00000000..908b5534
--- /dev/null
+++ b/src/django_idom/views.py
@@ -0,0 +1,7 @@
+from django.http import HttpResponse
+from idom.config import IDOM_WED_MODULES_DIR
+
+
+def web_modules_file(request, file: str) -> HttpResponse:
+ file_path = IDOM_WED_MODULES_DIR.current.joinpath(*file.split("/"))
+ return HttpResponse(file_path.read_text(), content_type="text/javascript")
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py
index 841dc4ee..d29b9f73 100644
--- a/src/django_idom/websocket_consumer.py
+++ b/src/django_idom/websocket_consumer.py
@@ -1,36 +1,20 @@
"""Anything used to construct a websocket endpoint"""
import asyncio
+import json
import logging
from typing import Any
from urllib.parse import parse_qsl
from channels.generic.websocket import AsyncJsonWebsocketConsumer
-from django.urls import path
from idom.core.dispatcher import dispatch_single_view
from idom.core.layout import Layout, LayoutEvent
from .app_components import get_component, has_component
-from .app_settings import IDOM_WEBSOCKET_URL
logger = logging.getLogger(__name__)
-def django_idom_websocket_consumer_url(*args, **kwargs):
- """Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
-
- While this is relatively uncommon in most Django apps, because the URL of the
- websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
- to allow users to configure the URL themselves
- """
- return path(
- IDOM_WEBSOCKET_URL + "/",
- IdomAsyncWebSocketConsumer.as_asgi(),
- *args,
- **kwargs,
- )
-
-
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""
@@ -59,7 +43,9 @@ async def _run_dispatch_loop(self):
return
component_constructor = get_component(view_id)
- component_kwargs = dict(parse_qsl(self.scope["query_string"]))
+
+ query_dict = dict(parse_qsl(self.scope["query_string"].decode()))
+ component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
try:
component_instance = component_constructor(**component_kwargs)
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index 72e7483c..6ff0f5ee 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -8,7 +8,7 @@
"name": "django-idom-client",
"version": "0.0.1",
"dependencies": {
- "idom-client-react": "^0.8.2"
+ "idom-client-react": "^0.8.5"
},
"devDependencies": {
"prettier": "^2.2.1",
@@ -100,16 +100,16 @@
"integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
},
"node_modules/idom-client-react": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz",
- "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==",
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.5.tgz",
+ "integrity": "sha512-/S/J+BPGnQ4YrXWBD0mT0IqigDHrXglTX91qDyIjg6aoQwcprJ8ms9WFwdXcx0/QY85s08+/FP4FnF8lcqwx9w==",
"dependencies": {
"fast-json-patch": "^3.0.0-1",
"htm": "^3.0.3"
},
"peerDependencies": {
- "react": "^16.13.1",
- "react-dom": "^16.13.1"
+ "react": ">=16",
+ "react-dom": ">=16"
}
},
"node_modules/is-core-module": {
@@ -405,9 +405,9 @@
"integrity": "sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ=="
},
"idom-client-react": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.2.tgz",
- "integrity": "sha512-pK4FjyfVIaOVA/R0sj6Ulvpo3FATFU11TbnoqgzGbXjjY7kYPuR3x2pa/M6MY/Ot9yUb2Nsz9Gr1Vj8QPJ6GwA==",
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.8.5.tgz",
+ "integrity": "sha512-/S/J+BPGnQ4YrXWBD0mT0IqigDHrXglTX91qDyIjg6aoQwcprJ8ms9WFwdXcx0/QY85s08+/FP4FnF8lcqwx9w==",
"requires": {
"fast-json-patch": "^3.0.0-1",
"htm": "^3.0.3"
diff --git a/src/js/package.json b/src/js/package.json
index 832480a8..087045c9 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -10,14 +10,14 @@
"build": "rollup --config",
"format": "prettier --ignore-path .gitignore --write ."
},
- "dependencies": {
- "idom-client-react": "^0.8.2"
- },
"devDependencies": {
"prettier": "^2.2.1",
"rollup": "^2.35.1",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0"
+ },
+ "dependencies": {
+ "idom-client-react": "^0.8.5"
}
}
diff --git a/src/js/src/index.js b/src/js/src/index.js
index a8a16bb3..bddf2a55 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -1,6 +1,5 @@
import { mountLayoutWithWebSocket } from "idom-client-react";
-
// Set up a websocket at the base endpoint
let LOCATION = window.location;
let WS_PROTOCOL = "";
@@ -11,8 +10,22 @@ if (LOCATION.protocol == "https:") {
}
let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
+export function mountViewToElement(
+ mountPoint,
+ idomWebsocketUrl,
+ idomWebModulesUrl,
+ viewId,
+ queryParams
+) {
+ const fullWebsocketUrl =
+ WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/?" + queryParams;
+
+ const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl
+ const loadImportSource = (source, sourceType) => {
+ return import(
+ sourceType == "NAME" ? `${fullWebModulesUrl}${source}` : source
+ );
+ };
-export function mountViewToElement(mountPoint, idomWebsocketUrl, viewId, queryParams) {
- const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/";
- mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl);
+ mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl, loadImportSource);
}
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
index e9faad98..625e9236 100644
--- a/tests/test_app/asgi.py
+++ b/tests/test_app/asgi.py
@@ -11,7 +11,7 @@
from django.core.asgi import get_asgi_application
-from django_idom import django_idom_websocket_consumer_url
+from django_idom import django_idom_websocket_consumer_path
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
@@ -25,6 +25,6 @@
application = ProtocolTypeRouter(
{
"http": http_asgi_app,
- "websocket": URLRouter([django_idom_websocket_consumer_url()]),
+ "websocket": URLRouter([django_idom_websocket_consumer_path()]),
}
)
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index c0042570..f4fbd9e5 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -19,3 +19,18 @@ def Button():
f"Current count is: {count}",
),
)
+
+
+@idom.component
+def ParametrizedComponent(x, y):
+ total = x + y
+ return idom.html.h1({"id": "parametrized-component", "data-value": total}, total)
+
+
+victory = idom.web.module_from_template("react", "victory", fallback="...")
+VictoryBar = idom.web.export(victory, "VictoryBar")
+
+
+@idom.component
+def SimpleBarChart():
+ return VictoryBar()
diff --git a/tests/test_app/idom.py b/tests/test_app/idom.py
index 007f171e..f7f9c10a 100644
--- a/tests/test_app/idom.py
+++ b/tests/test_app/idom.py
@@ -1,7 +1,9 @@
-from .components import Button, HelloWorld
+from .components import Button, HelloWorld, ParametrizedComponent, SimpleBarChart
components = [
HelloWorld,
Button,
+ ParametrizedComponent,
+ SimpleBarChart,
]
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index e3180b1d..0968e6c1 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -17,5 +17,7 @@
IDOM Test Page
{% idom_view "test_app.HelloWorld" %}
{% idom_view "test_app.Button" %}
+ {% idom_view "test_app.ParametrizedComponent" x=123 y=456 %}
+ {% idom_view "test_app.SimpleBarChart" %}