Skip to content

Commit a0b75e0

Browse files
committed
parametrized components + serve web modules
1 parent b4f256b commit a0b75e0

File tree

16 files changed

+137
-51
lines changed

16 files changed

+137
-51
lines changed

src/django_idom/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
from .websocket_consumer import (
2-
IdomAsyncWebSocketConsumer,
3-
django_idom_websocket_consumer_url,
4-
)
1+
from .app_paths import django_idom_web_modules_path, django_idom_websocket_consumer_path
52

63

74
__version__ = "0.0.1"
8-
__all__ = ["IdomAsyncWebSocketConsumer", "django_idom_websocket_consumer_url"]
5+
__all__ = ["django_idom_websocket_consumer_path", "django_idom_web_modules_path"]

src/django_idom/app_paths.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.urls import path
2+
3+
from . import views
4+
from .app_settings import IDOM_WEB_MODULES_URL, IDOM_WEBSOCKET_URL
5+
from .websocket_consumer import IdomAsyncWebSocketConsumer
6+
7+
8+
def django_idom_websocket_consumer_path(*args, **kwargs):
9+
"""Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
10+
11+
While this is relatively uncommon in most Django apps, because the URL of the
12+
websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
13+
to allow users to configure the URL themselves
14+
"""
15+
return path(
16+
IDOM_WEBSOCKET_URL + "<view_id>/",
17+
IdomAsyncWebSocketConsumer.as_asgi(),
18+
*args,
19+
**kwargs,
20+
)
21+
22+
23+
def django_idom_web_modules_path(*args, **kwargs):
24+
return path(
25+
IDOM_WEB_MODULES_URL + "<path:file>",
26+
views.web_modules_file,
27+
*args,
28+
**kwargs,
29+
)

src/django_idom/app_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
for file in (APP_DIR / "templates" / "idom").iterdir()
1111
}
1212

13-
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "_idom/")
14-
if not IDOM_WEBSOCKET_URL.endswith("/"):
15-
IDOM_WEBSOCKET_URL += "/"
13+
IDOM_BASE_URL = getattr(settings, "IDOM_BASE_URL", "_idom/")
14+
IDOM_WEBSOCKET_URL = IDOM_BASE_URL + "websocket/"
15+
IDOM_WEB_MODULES_URL = IDOM_BASE_URL + "web_module/"

src/django_idom/templates/idom/view.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
mountViewToElement(
77
mountPoint,
88
"{{ idom_websocket_url }}",
9+
"{{ idom_web_modules_url }}",
910
"{{ idom_view_id }}",
1011
"{{ idom_view_params }}"
1112
);

src/django_idom/templatetags/idom.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
import json
2+
from urllib.parse import urlencode
13
from uuid import uuid4
24

35
from django import template
46

5-
from django_idom.app_settings import IDOM_WEBSOCKET_URL, TEMPLATE_FILE_PATHS
7+
from django_idom.app_settings import (
8+
IDOM_WEB_MODULES_URL,
9+
IDOM_WEBSOCKET_URL,
10+
TEMPLATE_FILE_PATHS,
11+
)
12+
from ..app_components import has_component
613

714

815
register = template.Library()
@@ -15,10 +22,16 @@ def idom_head():
1522

1623

1724
@register.inclusion_tag(TEMPLATE_FILE_PATHS["view"])
18-
def idom_view(view_id, view_params=""):
25+
def idom_view(_component_id_, **kwargs):
26+
if not has_component(_component_id_):
27+
raise ValueError(f"No component {_component_id_!r} exists")
28+
29+
json_kwargs = json.dumps(kwargs, separators=(",", ":"))
30+
1931
return {
2032
"idom_websocket_url": IDOM_WEBSOCKET_URL,
33+
"idom_web_modules_url": IDOM_WEB_MODULES_URL,
2134
"idom_mount_uuid": uuid4().hex,
22-
"idom_view_id": view_id,
23-
"idom_view_params": view_params,
35+
"idom_view_id": _component_id_,
36+
"idom_view_params": urlencode({"kwargs": json_kwargs}),
2437
}

src/django_idom/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.http import HttpResponse
2+
from idom.config import IDOM_WED_MODULES_DIR
3+
4+
5+
def web_modules_file(request, file: str) -> HttpResponse:
6+
file_path = IDOM_WED_MODULES_DIR.current.joinpath(*file.split("/"))
7+
return HttpResponse(file_path.read_text(), content_type="text/javascript")

src/django_idom/websocket_consumer.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,20 @@
11
"""Anything used to construct a websocket endpoint"""
22
import asyncio
3+
import json
34
import logging
45
from typing import Any
56
from urllib.parse import parse_qsl
67

78
from channels.generic.websocket import AsyncJsonWebsocketConsumer
8-
from django.urls import path
99
from idom.core.dispatcher import dispatch_single_view
1010
from idom.core.layout import Layout, LayoutEvent
1111

1212
from .app_components import get_component, has_component
13-
from .app_settings import IDOM_WEBSOCKET_URL
1413

1514

1615
logger = logging.getLogger(__name__)
1716

1817

19-
def django_idom_websocket_consumer_url(*args, **kwargs):
20-
"""Return a URL resolver for :class:`IdomAsyncWebSocketConsumer`
21-
22-
While this is relatively uncommon in most Django apps, because the URL of the
23-
websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
24-
to allow users to configure the URL themselves
25-
"""
26-
return path(
27-
IDOM_WEBSOCKET_URL + "<view_id>/",
28-
IdomAsyncWebSocketConsumer.as_asgi(),
29-
*args,
30-
**kwargs,
31-
)
32-
33-
3418
class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
3519
"""Communicates with the browser to perform actions on-demand."""
3620

@@ -59,7 +43,9 @@ async def _run_dispatch_loop(self):
5943
return
6044

6145
component_constructor = get_component(view_id)
62-
component_kwargs = dict(parse_qsl(self.scope["query_string"]))
46+
47+
query_dict = dict(parse_qsl(self.scope["query_string"].decode()))
48+
component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
6349

6450
try:
6551
component_instance = component_constructor(**component_kwargs)

src/js/package-lock.json

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

src/js/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
"build": "rollup --config",
1111
"format": "prettier --ignore-path .gitignore --write ."
1212
},
13-
"dependencies": {
14-
"idom-client-react": "^0.8.2"
15-
},
1613
"devDependencies": {
1714
"prettier": "^2.2.1",
1815
"rollup": "^2.35.1",
1916
"rollup-plugin-commonjs": "^10.1.0",
2017
"rollup-plugin-node-resolve": "^5.2.0",
2118
"rollup-plugin-replace": "^2.2.0"
19+
},
20+
"dependencies": {
21+
"idom-client-react": "^0.8.5"
2222
}
2323
}

src/js/src/index.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { mountLayoutWithWebSocket } from "idom-client-react";
22

3-
43
// Set up a websocket at the base endpoint
54
let LOCATION = window.location;
65
let WS_PROTOCOL = "";
@@ -11,8 +10,22 @@ if (LOCATION.protocol == "https:") {
1110
}
1211
let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
1312

13+
export function mountViewToElement(
14+
mountPoint,
15+
idomWebsocketUrl,
16+
idomWebModulesUrl,
17+
viewId,
18+
queryParams
19+
) {
20+
const fullWebsocketUrl =
21+
WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/?" + queryParams;
22+
23+
const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl
24+
const loadImportSource = (source, sourceType) => {
25+
return import(
26+
sourceType == "NAME" ? `${fullWebModulesUrl}${source}` : source
27+
);
28+
};
1429

15-
export function mountViewToElement(mountPoint, idomWebsocketUrl, viewId, queryParams) {
16-
const fullWebsocketUrl = WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/";
17-
mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl);
30+
mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl, loadImportSource);
1831
}

tests/test_app/asgi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from django.core.asgi import get_asgi_application
1313

14-
from django_idom import django_idom_websocket_consumer_url
14+
from django_idom import django_idom_websocket_consumer_path
1515

1616

1717
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
@@ -25,6 +25,6 @@
2525
application = ProtocolTypeRouter(
2626
{
2727
"http": http_asgi_app,
28-
"websocket": URLRouter([django_idom_websocket_consumer_url()]),
28+
"websocket": URLRouter([django_idom_websocket_consumer_path()]),
2929
}
3030
)

tests/test_app/components.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ def Button():
1919
f"Current count is: {count}",
2020
),
2121
)
22+
23+
24+
@idom.component
25+
def ParametrizedComponent(x, y):
26+
total = x + y
27+
return idom.html.h1({"id": "parametrized-component", "data-value": total}, total)
28+
29+
30+
victory = idom.web.module_from_template("react", "victory", fallback="...")
31+
VictoryBar = idom.web.export(victory, "VictoryBar")
32+
33+
34+
@idom.component
35+
def SimpleBarChart():
36+
return VictoryBar()

tests/test_app/idom.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from .components import Button, HelloWorld
1+
from .components import Button, HelloWorld, ParametrizedComponent, SimpleBarChart
22

33

44
components = [
55
HelloWorld,
66
Button,
7+
ParametrizedComponent,
8+
SimpleBarChart,
79
]

tests/test_app/templates/base.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@
1717
<h1>IDOM Test Page</h1>
1818
<div>{% idom_view "test_app.HelloWorld" %}</div>
1919
<div>{% idom_view "test_app.Button" %}</div>
20+
<div>{% idom_view "test_app.ParametrizedComponent" x=123 y=456 %}</div>
21+
<div>{% idom_view "test_app.SimpleBarChart" %}</div>
2022
</body>
2123
</html>

tests/test_app/tests.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from channels.testing import ChannelsLiveServerTestCase
44
from selenium import webdriver
5+
from selenium.webdriver.common.by import By
6+
from selenium.webdriver.support import expected_conditions
57
from selenium.webdriver.support.ui import WebDriverWait
68

79

@@ -13,8 +15,11 @@ def setUp(self):
1315
def tearDown(self) -> None:
1416
self.driver.quit()
1517

18+
def wait(self, timeout=5):
19+
return WebDriverWait(self.driver, timeout)
20+
1621
def wait_until(self, condition, timeout=5):
17-
WebDriverWait(self.driver, timeout).until(lambda driver: condition())
22+
return self.wait(timeout).until(lambda driver: condition())
1823

1924
def test_hello_world(self):
2025
self.driver.find_element_by_id("hello-world")
@@ -27,6 +32,17 @@ def test_counter(self):
2732
self.wait_until(lambda: count.get_attribute("data-count") == str(i))
2833
button.click()
2934

35+
def test_parametrized_component(self):
36+
element = self.driver.find_element_by_id("parametrized-component")
37+
self.assertEqual(element.get_attribute("data-value"), "579")
38+
39+
def test_component_from_web_module(self):
40+
self.wait(10).until(
41+
expected_conditions.visibility_of_element_located(
42+
(By.CLASS_NAME, "VictoryContainer")
43+
)
44+
)
45+
3046

3147
def make_driver(page_load_timeout, implicit_wait_timeout):
3248
options = webdriver.ChromeOptions()

tests/test_app/urls.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
"""
2020
from django.urls import path
2121

22+
from django_idom import django_idom_web_modules_path
23+
2224
from .views import base_template
2325

2426

25-
urlpatterns = [path("", base_template)]
27+
urlpatterns = [
28+
path("", base_template),
29+
django_idom_web_modules_path(),
30+
]

0 commit comments

Comments
 (0)