Skip to content

Commit 18a419f

Browse files
authored
Deferred static files (#77)
- Deferred static file components - Docs for deferred static files - Bump minimum IDOM version - Create PR templates - Sync issue form template with `idom-team/idom` - Pin selenium to 4.2 for compatibility - Bump django-idom to v1.1.0 - Docstring for component template tag - Minor readme wordsmithing
1 parent b9ca296 commit 18a419f

File tree

18 files changed

+273
-19
lines changed

18 files changed

+273
-19
lines changed

Diff for: .github/ISSUE_TEMPLATE/issue-form.yml

-7
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,3 @@ body:
1414
description: Describe what ought to be done, and why that will address the reasons for action mentioned above.
1515
validations:
1616
required: false
17-
- type: textarea
18-
attributes:
19-
label: Work Items
20-
description: |
21-
An itemized list or detailed description of the work to be done to based on the proposed actions above.
22-
validations:
23-
required: false

Diff for: .github/pull_request_template.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Description
2+
3+
A summary of the changes.
4+
5+
# Checklist:
6+
7+
Please update this checklist as you complete each item:
8+
9+
- [ ] Tests have been included for all bug fixes or added functionality.
10+
- [ ] The `changelog.rst` has been updated with any significant changes, if necessary.
11+
- [ ] GitHub Issues which may be closed by this PR have been linked.

Diff for: CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ Types of changes are to be listed in this order
2424

2525
- Nothing (yet)
2626

27+
## [1.1.0] - 2022-06-25
28+
29+
### Added
30+
31+
- `django_css` and `django_js` components to defer loading CSS & JS files until needed.
32+
33+
### Changed
34+
35+
- Bumped the minimum IDOM version to 0.39.0
36+
2737
## [1.0.0] - 2022-05-22
2838

2939
### Added
@@ -103,6 +113,7 @@ Types of changes are to be listed in this order
103113
- Support for IDOM within the Django
104114

105115
[unreleased]: https://github.com/idom-team/django-idom/compare/1.0.0...HEAD
116+
[1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0
106117
[1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0
107118
[0.0.5]: https://github.com/idom-team/django-idom/compare/0.0.4...0.0.5
108119
[0.0.4]: https://github.com/idom-team/django-idom/compare/0.0.3...0.0.4

Diff for: README.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ Any Python web framework with Websockets can support IDOM. See below for what fr
1919

2020
<!--intro-end-->
2121

22-
---
23-
2422
# At a Glance
2523

2624
## `my_app/components.py`
@@ -46,7 +44,7 @@ def HelloWorld(recipient: str):
4644

4745
<!--html-header-start-->
4846

49-
In your **Django app**'s HTML located within your `templates` folder, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument.
47+
In your **Django app**'s HTML template, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument.
5048

5149
Additonally, you can pass in keyword arguments into your component function. For example, after reading the code below, pay attention to how the function definition for `HelloWorld` (_in the previous example_) accepts a `recipient` argument.
5250

@@ -65,8 +63,6 @@ Additonally, you can pass in keyword arguments into your component function. For
6563

6664
<!--html-code-end-->
6765

68-
---
69-
7066
# Resources
7167

7268
<!--resources-start-->

Diff for: docs/features/components.md

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
## Django CSS
2+
3+
Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/).
4+
5+
```python title="components.py"
6+
from idom import component, html
7+
from django_idom.components import django_css
8+
9+
@component
10+
def MyComponent():
11+
return html.div(
12+
django_css("css/buttons.css"),
13+
html.button("My Button!"),
14+
)
15+
```
16+
17+
??? question "Should I put `django_css` at the top of my component?"
18+
19+
Yes, if the stylesheet contains styling for your component.
20+
21+
??? question "Can I load static CSS using `html.link` instead?"
22+
23+
While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause some visual jankiness, so use this at your own discretion.
24+
25+
Here's an example on what you should avoid doing for Django static files:
26+
27+
```python
28+
from idom import component, html
29+
from django.templatetags.static import static
30+
31+
@component
32+
def MyComponent():
33+
return html.div(
34+
html.link({"rel": "stylesheet", "href": static("css/buttons.css")}),
35+
html.button("My Button!"),
36+
)
37+
```
38+
39+
??? question "How do I load external CSS?"
40+
41+
`django_css` can only be used with local static files.
42+
43+
For external CSS, substitute `django_css` with `html.link`.
44+
45+
```python
46+
from idom import component, html
47+
48+
@component
49+
def MyComponent():
50+
return html.div(
51+
html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}),
52+
html.button("My Button!"),
53+
)
54+
```
55+
56+
??? question "Why not load my CSS in `#!html <head>`?"
57+
58+
Traditionally, stylesheets are loaded in your `#!html <head>` using the `#!jinja {% load static %}` template tag.
59+
60+
To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed.
61+
62+
## Django JS
63+
64+
Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/).
65+
66+
```python title="components.py"
67+
from idom import component, html
68+
from django_idom.components import django_js
69+
70+
@component
71+
def MyComponent():
72+
return html.div(
73+
html.button("My Button!"),
74+
django_js("js/scripts.js"),
75+
)
76+
```
77+
78+
??? question "Should I put `django_js` at the bottom of my component?"
79+
80+
Yes, if your scripts are reliant on the contents of the component.
81+
82+
??? question "Can I load static JavaScript using `html.script` instead?"
83+
84+
While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed.
85+
86+
Here's an example on what you should avoid doing for Django static files:
87+
88+
```python
89+
from idom import component, html
90+
from django.templatetags.static import static
91+
92+
@component
93+
def MyComponent():
94+
return html.div(
95+
html.script({"src": static("js/scripts.js")}),
96+
html.button("My Button!"),
97+
)
98+
```
99+
100+
??? question "How do I load external JS?"
101+
102+
`django_js` can only be used with local static files.
103+
104+
For external JavaScript, substitute `django_js` with `html.script`.
105+
106+
```python
107+
from idom import component, html
108+
109+
@component
110+
def MyComponent():
111+
return html.div(
112+
html.script({"src": static("https://example.com/external-scripts.js")}),
113+
html.button("My Button!"),
114+
)
115+
```
116+
117+
??? question "Why not load my JS in `#!html <head>`?"
118+
119+
Traditionally, JavaScript is loaded in your `#!html <head>` using the `#!jinja {% load static %}` template tag.
120+
121+
To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed.

Diff for: docs/features/hooks.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def MyComponent():
1818
return html.div(my_websocket)
1919
```
2020

21-
---
21+
2222

2323
## Use Scope
2424

@@ -34,7 +34,6 @@ def MyComponent():
3434
return html.div(my_scope)
3535
```
3636

37-
---
3837

3938
## Use Location
4039

Diff for: docs/stylesheets/extra.css

+4
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66
.md-header {
77
background-color: var(--md-footer-bg-color--dark);
88
}
9+
10+
.md-typeset :is(.admonition, details) {
11+
margin: 1em 0;
12+
}

Diff for: mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ nav:
99
- 4. Render Your View: getting-started/render-view.md
1010
- 5. Learn More: getting-started/learn-more.md
1111
- Exclusive Features:
12+
- Components: features/components.md
1213
- Hooks: features/hooks.md
1314
- Template Tag: features/templatetag.md
1415
- Contribute:

Diff for: requirements/pkg-deps.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
channels >=3.0.0
2-
idom >=0.38.0, <0.39.0
2+
idom >=0.39.0, <0.40.0
33
aiofile >=3.0

Diff for: requirements/test-env.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
django
2-
selenium
2+
selenium <= 4.2.0
33
twisted

Diff for: src/django_idom/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from . import hooks
1+
from . import components, hooks
22
from .websocket.consumer import IdomWebsocket
33
from .websocket.paths import IDOM_WEBSOCKET_PATH
44

55

6-
__version__ = "1.0.0"
7-
__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks"]
6+
__version__ = "1.1.0"
7+
__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components"]

Diff for: src/django_idom/components.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
3+
from django.contrib.staticfiles.finders import find
4+
from idom import component, html
5+
6+
from django_idom.config import IDOM_CACHE
7+
8+
9+
@component
10+
def django_css(static_path: str):
11+
"""Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.
12+
13+
Args:
14+
static_path: The path to the static file. This path is identical to what you would
15+
use on a `static` template tag.
16+
"""
17+
return html._(
18+
html.script(
19+
"""
20+
let parentTag = document.currentScript;
21+
console.log(parentTag);
22+
//parentTag.attachShadow({ mode: 'open' });
23+
"""
24+
),
25+
html.style(_cached_static_contents(static_path)),
26+
)
27+
28+
29+
@component
30+
def django_js(static_path: str):
31+
"""Fetches a JS static file for use within IDOM. This allows for deferred JS loading.
32+
33+
Args:
34+
static_path: The path to the static file. This path is identical to what you would
35+
use on a `static` template tag.
36+
"""
37+
return html.script(_cached_static_contents(static_path))
38+
39+
40+
def _cached_static_contents(static_path: str):
41+
# Try to find the file within Django's static files
42+
abs_path = find(static_path)
43+
if not abs_path:
44+
raise FileNotFoundError(
45+
f"Could not find static file {static_path} within Django's static files."
46+
)
47+
48+
# Fetch the file from cache, if available
49+
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
50+
last_modified_time = os.stat(abs_path).st_mtime
51+
cache_key = f"django_idom:static_contents:{static_path}"
52+
file_contents = IDOM_CACHE.get(cache_key, version=last_modified_time)
53+
if file_contents is None:
54+
with open(abs_path, encoding="utf-8") as static_file:
55+
file_contents = static_file.read()
56+
IDOM_CACHE.delete(cache_key)
57+
IDOM_CACHE.set(
58+
cache_key, file_contents, timeout=None, version=last_modified_time
59+
)
60+
61+
return file_contents

Diff for: src/django_idom/templatetags/idom.py

+15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@
1515

1616
@register.inclusion_tag("idom/component.html")
1717
def component(_component_id_, **kwargs):
18+
"""
19+
This tag is used to embed an existing IDOM component into your HTML template.
20+
21+
The first argument within this tag is your dotted path to the component function.
22+
23+
Subsequent values are keyworded arguments are passed into your component::
24+
25+
{% load idom %}
26+
<!DOCTYPE html>
27+
<html>
28+
<body>
29+
{% component "example_project.my_app.components.HelloWorld" recipient="World" %}
30+
</body>
31+
</html>
32+
"""
1833
_register_component(_component_id_)
1934

2035
class_ = kwargs.pop("class", "")

Diff for: tests/test_app/components.py

+24
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,27 @@ def UseLocation():
7171
f"UseLocation: {location}",
7272
idom.html.hr(),
7373
)
74+
75+
76+
@idom.component
77+
def StaticCSS():
78+
return idom.html.div(
79+
{"id": "static-css"},
80+
django_idom.components.django_css("static-css-test.css"),
81+
idom.html.div({"style": {"display": "inline"}}, "StaticCSS: "),
82+
idom.html.button("This text should be blue."),
83+
idom.html.hr(),
84+
)
85+
86+
87+
@idom.component
88+
def StaticJS():
89+
success = False
90+
return idom.html._(
91+
idom.html.div(
92+
{"id": "static-js", "data-success": success},
93+
f"StaticJS: {success}",
94+
django_idom.components.django_js("static-js-test.js"),
95+
),
96+
idom.html.hr(),
97+
)

Diff for: tests/test_app/static/static-css-test.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#static-css button {
2+
color: rgba(0, 0, 255, 1);
3+
}

Diff for: tests/test_app/static/static-js-test.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
let el = document.body.querySelector("#static-js");
2+
el.textContent = "StaticJS: True";
3+
el.dataset.success = "true";

Diff for: tests/test_app/templates/base.html

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ <h1>IDOM Test Page</h1>
1919
<div>{% component "test_app.components.UseWebsocket" %}</div>
2020
<div>{% component "test_app.components.UseScope" %}</div>
2121
<div>{% component "test_app.components.UseLocation" %}</div>
22+
<div>{% component "test_app.components.StaticCSS" %}</div>
23+
<div>{% component "test_app.components.StaticJS" %}</div>
2224
</body>
2325

2426
</html>

Diff for: tests/test_app/tests/test_components.py

+10
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ def test_use_location(self):
5959
element = self.driver.find_element_by_id("use-location")
6060
self.assertEqual(element.get_attribute("data-success"), "true")
6161

62+
def test_static_css(self):
63+
element = self.driver.find_element_by_css_selector("#static-css button")
64+
self.assertEqual(
65+
element.value_of_css_property("color"), "rgba(0, 0, 255, 1)"
66+
)
67+
68+
def test_static_js(self):
69+
element = self.driver.find_element_by_id("static-js")
70+
self.assertEqual(element.get_attribute("data-success"), "true")
71+
6272

6373
def make_driver(page_load_timeout, implicit_wait_timeout):
6474
options = webdriver.ChromeOptions()

0 commit comments

Comments
 (0)