Skip to content

Commit

Permalink
Deferred static files (#77)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
Archmonger authored Jul 1, 2022
1 parent b9ca296 commit 18a419f
Show file tree
Hide file tree
Showing 18 changed files with 273 additions and 19 deletions.
7 changes: 0 additions & 7 deletions .github/ISSUE_TEMPLATE/issue-form.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,3 @@ body:
description: Describe what ought to be done, and why that will address the reasons for action mentioned above.
validations:
required: false
- type: textarea
attributes:
label: Work Items
description: |
An itemized list or detailed description of the work to be done to based on the proposed actions above.
validations:
required: false
11 changes: 11 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Description

A summary of the changes.

# Checklist:

Please update this checklist as you complete each item:

- [ ] Tests have been included for all bug fixes or added functionality.
- [ ] The `changelog.rst` has been updated with any significant changes, if necessary.
- [ ] GitHub Issues which may be closed by this PR have been linked.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ Types of changes are to be listed in this order

- Nothing (yet)

## [1.1.0] - 2022-06-25

### Added

- `django_css` and `django_js` components to defer loading CSS & JS files until needed.

### Changed

- Bumped the minimum IDOM version to 0.39.0

## [1.0.0] - 2022-05-22

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

[unreleased]: https://github.com/idom-team/django-idom/compare/1.0.0...HEAD
[1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0
[1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0
[0.0.5]: https://github.com/idom-team/django-idom/compare/0.0.4...0.0.5
[0.0.4]: https://github.com/idom-team/django-idom/compare/0.0.3...0.0.4
Expand Down
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ Any Python web framework with Websockets can support IDOM. See below for what fr

<!--intro-end-->

---

# At a Glance

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

<!--html-header-start-->

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

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.

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

<!--html-code-end-->

---

# Resources

<!--resources-start-->
Expand Down
121 changes: 121 additions & 0 deletions docs/features/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
## Django CSS

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/).

```python title="components.py"
from idom import component, html
from django_idom.components import django_css

@component
def MyComponent():
return html.div(
django_css("css/buttons.css"),
html.button("My Button!"),
)
```

??? question "Should I put `django_css` at the top of my component?"

Yes, if the stylesheet contains styling for your component.

??? question "Can I load static CSS using `html.link` instead?"

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.

Here's an example on what you should avoid doing for Django static files:

```python
from idom import component, html
from django.templatetags.static import static

@component
def MyComponent():
return html.div(
html.link({"rel": "stylesheet", "href": static("css/buttons.css")}),
html.button("My Button!"),
)
```

??? question "How do I load external CSS?"

`django_css` can only be used with local static files.

For external CSS, substitute `django_css` with `html.link`.

```python
from idom import component, html

@component
def MyComponent():
return html.div(
html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}),
html.button("My Button!"),
)
```

??? question "Why not load my CSS in `#!html <head>`?"

Traditionally, stylesheets are loaded in your `#!html <head>` using the `#!jinja {% load static %}` template tag.

To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed.

## Django JS

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/).

```python title="components.py"
from idom import component, html
from django_idom.components import django_js

@component
def MyComponent():
return html.div(
html.button("My Button!"),
django_js("js/scripts.js"),
)
```

??? question "Should I put `django_js` at the bottom of my component?"

Yes, if your scripts are reliant on the contents of the component.

??? question "Can I load static JavaScript using `html.script` instead?"

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.

Here's an example on what you should avoid doing for Django static files:

```python
from idom import component, html
from django.templatetags.static import static

@component
def MyComponent():
return html.div(
html.script({"src": static("js/scripts.js")}),
html.button("My Button!"),
)
```

??? question "How do I load external JS?"

`django_js` can only be used with local static files.

For external JavaScript, substitute `django_js` with `html.script`.

```python
from idom import component, html

@component
def MyComponent():
return html.div(
html.script({"src": static("https://example.com/external-scripts.js")}),
html.button("My Button!"),
)
```

??? question "Why not load my JS in `#!html <head>`?"

Traditionally, JavaScript is loaded in your `#!html <head>` using the `#!jinja {% load static %}` template tag.

To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed.
3 changes: 1 addition & 2 deletions docs/features/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def MyComponent():
return html.div(my_websocket)
```

---


## Use Scope

Expand All @@ -34,7 +34,6 @@ def MyComponent():
return html.div(my_scope)
```

---

## Use Location

Expand Down
4 changes: 4 additions & 0 deletions docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
.md-header {
background-color: var(--md-footer-bg-color--dark);
}

.md-typeset :is(.admonition, details) {
margin: 1em 0;
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav:
- 4. Render Your View: getting-started/render-view.md
- 5. Learn More: getting-started/learn-more.md
- Exclusive Features:
- Components: features/components.md
- Hooks: features/hooks.md
- Template Tag: features/templatetag.md
- Contribute:
Expand Down
2 changes: 1 addition & 1 deletion requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
channels >=3.0.0
idom >=0.38.0, <0.39.0
idom >=0.39.0, <0.40.0
aiofile >=3.0
2 changes: 1 addition & 1 deletion requirements/test-env.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
django
selenium
selenium <= 4.2.0
twisted
6 changes: 3 additions & 3 deletions src/django_idom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from . import hooks
from . import components, hooks
from .websocket.consumer import IdomWebsocket
from .websocket.paths import IDOM_WEBSOCKET_PATH


__version__ = "1.0.0"
__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks"]
__version__ = "1.1.0"
__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components"]
61 changes: 61 additions & 0 deletions src/django_idom/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os

from django.contrib.staticfiles.finders import find
from idom import component, html

from django_idom.config import IDOM_CACHE


@component
def django_css(static_path: str):
"""Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.
Args:
static_path: The path to the static file. This path is identical to what you would
use on a `static` template tag.
"""
return html._(
html.script(
"""
let parentTag = document.currentScript;
console.log(parentTag);
//parentTag.attachShadow({ mode: 'open' });
"""
),
html.style(_cached_static_contents(static_path)),
)


@component
def django_js(static_path: str):
"""Fetches a JS static file for use within IDOM. This allows for deferred JS loading.
Args:
static_path: The path to the static file. This path is identical to what you would
use on a `static` template tag.
"""
return html.script(_cached_static_contents(static_path))


def _cached_static_contents(static_path: str):
# Try to find the file within Django's static files
abs_path = find(static_path)
if not abs_path:
raise FileNotFoundError(
f"Could not find static file {static_path} within Django's static files."
)

# Fetch the file from cache, if available
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
last_modified_time = os.stat(abs_path).st_mtime
cache_key = f"django_idom:static_contents:{static_path}"
file_contents = IDOM_CACHE.get(cache_key, version=last_modified_time)
if file_contents is None:
with open(abs_path, encoding="utf-8") as static_file:
file_contents = static_file.read()
IDOM_CACHE.delete(cache_key)
IDOM_CACHE.set(
cache_key, file_contents, timeout=None, version=last_modified_time
)

return file_contents
15 changes: 15 additions & 0 deletions src/django_idom/templatetags/idom.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@

@register.inclusion_tag("idom/component.html")
def component(_component_id_, **kwargs):
"""
This tag is used to embed an existing IDOM component into your HTML template.
The first argument within this tag is your dotted path to the component function.
Subsequent values are keyworded arguments are passed into your component::
{% load idom %}
<!DOCTYPE html>
<html>
<body>
{% component "example_project.my_app.components.HelloWorld" recipient="World" %}
</body>
</html>
"""
_register_component(_component_id_)

class_ = kwargs.pop("class", "")
Expand Down
24 changes: 24 additions & 0 deletions tests/test_app/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,27 @@ def UseLocation():
f"UseLocation: {location}",
idom.html.hr(),
)


@idom.component
def StaticCSS():
return idom.html.div(
{"id": "static-css"},
django_idom.components.django_css("static-css-test.css"),
idom.html.div({"style": {"display": "inline"}}, "StaticCSS: "),
idom.html.button("This text should be blue."),
idom.html.hr(),
)


@idom.component
def StaticJS():
success = False
return idom.html._(
idom.html.div(
{"id": "static-js", "data-success": success},
f"StaticJS: {success}",
django_idom.components.django_js("static-js-test.js"),
),
idom.html.hr(),
)
3 changes: 3 additions & 0 deletions tests/test_app/static/static-css-test.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#static-css button {
color: rgba(0, 0, 255, 1);
}
3 changes: 3 additions & 0 deletions tests/test_app/static/static-js-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let el = document.body.querySelector("#static-js");
el.textContent = "StaticJS: True";
el.dataset.success = "true";
2 changes: 2 additions & 0 deletions tests/test_app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ <h1>IDOM Test Page</h1>
<div>{% component "test_app.components.UseWebsocket" %}</div>
<div>{% component "test_app.components.UseScope" %}</div>
<div>{% component "test_app.components.UseLocation" %}</div>
<div>{% component "test_app.components.StaticCSS" %}</div>
<div>{% component "test_app.components.StaticJS" %}</div>
</body>

</html>
10 changes: 10 additions & 0 deletions tests/test_app/tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ def test_use_location(self):
element = self.driver.find_element_by_id("use-location")
self.assertEqual(element.get_attribute("data-success"), "true")

def test_static_css(self):
element = self.driver.find_element_by_css_selector("#static-css button")
self.assertEqual(
element.value_of_css_property("color"), "rgba(0, 0, 255, 1)"
)

def test_static_js(self):
element = self.driver.find_element_by_id("static-js")
self.assertEqual(element.get_attribute("data-success"), "true")


def make_driver(page_load_timeout, implicit_wait_timeout):
options = webdriver.ChromeOptions()
Expand Down

0 comments on commit 18a419f

Please sign in to comment.