feat: allow to set main JS and CSS from files + lazy-load component m… (#870)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-12-30 18:00:46 +01:00 committed by GitHub
parent 8fcb84c002
commit 715bf7d447
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1014 additions and 248 deletions

View file

@ -3,7 +3,7 @@ title: Defining HTML / JS / CSS files
weight: 8
---
django_component's management of files builds on top of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
django_component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
To be familiar with how Django handles static files, we recommend reading also:
@ -12,7 +12,7 @@ To be familiar with how Django handles static files, we recommend reading also:
## Defining file paths relative to component or static dirs
As seen in the [getting started example](#create-your-first-component), to associate HTML/JS/CSS
files with a component, you set them as `template_name`, `Media.js` and `Media.css` respectively:
files with a component, you set them as `template_name`, `js_file` and `css_file` respectively:
```py
# In a file [project root]/components/calendar/calendar.py
@ -21,10 +21,8 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "template.html"
class Media:
css = "style.css"
js = "script.js"
css_file = "style.css"
js_file = "script.js"
```
In the example above, the files are defined relative to the directory where `component.py` is.
@ -40,17 +38,24 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
class Media:
css = "calendar/style.css"
js = "calendar/script.js"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
```
NOTE: In case of conflict, the preference goes to resolving the files relative to the component's directory.
## Defining multiple paths
Each component can have only a single template. However, you can define as many JS or CSS files as you want using a list.
Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
using the nested [`Media` class](../../../reference/api#django_components.Component.Media).
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition),
with a few differences:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (See below)
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
3. Our Media class does NOT support [Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
```py
class MyComponent(Component):
@ -106,14 +111,14 @@ from django.utils.safestring import mark_safe
class SimpleComponent(Component):
class Media:
css = [
mark_safe('<link href="/static/calendar/style.css" rel="stylesheet" />'),
mark_safe('<link href="/static/calendar/style1.css" rel="stylesheet" />'),
Path("calendar/style1.css"),
"calendar/style2.css",
b"calendar/style3.css",
lambda: "calendar/style4.css",
]
js = [
mark_safe('<script src="/static/calendar/script.js"></script>'),
mark_safe('<script src="/static/calendar/script1.js"></script>'),
Path("calendar/script1.js"),
"calendar/script2.js",
b"calendar/script3.js",
@ -152,7 +157,7 @@ class Calendar(Component):
}
class Media:
css = "calendar/style.css"
css = "calendar/style1.css"
js = [
# <script> tag constructed by Media class
"calendar/script1.js",
@ -191,10 +196,12 @@ class MyMedia(Media):
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
class Media:
css = "calendar/style.css"
js = "calendar/script.js"
css = "calendar/style1.css"
js = "calendar/script2.js"
# Override the behavior of Media class
media_class = MyMedia

View file

@ -3,7 +3,7 @@ title: Single-file components
weight: 1
---
Components can also be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
Components can be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
```python title="[project root]/components/calendar.py"
# In a file called [project root]/components/calendar.py
@ -35,3 +35,5 @@ class Calendar(Component):
```
This makes it easy to create small components without having to create a separate template, CSS, and JS file.
To add syntax highlighting to these snippets, head over to [Syntax highlighting](../../guides/setup/syntax_highlight.md).

View file

@ -177,18 +177,16 @@ So in your HTML, you may see something like this:
Finally, we return to our Python component in `calendar.py` to tie this together.
To link JS and CSS defined in other files, use the `Media` nested class
([Learn more about using Media](../fundamentals/defining_js_css_html_files.md)).
To link JS and CSS defined in other files, use [`js_file`](../../../reference/api#django_components.Component.js_file)
and [`css_file`](../../../reference/api#django_components.Component.css_file) attributes:
```python title="[project root]/components/calendar/calendar.py"
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
class Media: # <--- new
js = "calendar.js"
css = "calendar.css"
js_file = "calendar.js" # <--- new
css_file = "calendar.css" # <--- new
def get_context_data(self):
return {
@ -196,6 +194,86 @@ class Calendar(Component):
}
```
And that's it! If you were to embed this component in an HTML, django-components will
automatically embed the associated JS and CSS.
!!! note
Similarly to the template file, the JS and CSS file paths can be either:
1. Relative to the Python component file (as seen above),
2. Relative to any of the component directories as defined by
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
and/or [`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `[your apps]/components` dir and `[project root]/components`)
3. Relative to any of the directories defined by `STATICFILES_DIRS`.
<!-- TODO: UPDATE AFTER AT LEAST ONE IMPLEMENTED
!!! info
Special role of `css` and `js`:
The "primary" JS and CSS you that specify via `js/css` and `js_file/css_file` have special role in many of django-components'
features:
- CSS scoping [a la Vue](https://vuejs.org/api/sfc-css-features.html#scoped-css)
- CSS variables from Python are available
- JS variables from Python are available
- JS can pass a callback to special JS method `$onLoad()`, which will be called every time
a component is rendered on the page.
This is not true for JS and CSS defined in `Media.js/css`, where the linked JS / CSS are rendered as is.
-->
### 5. Link additional JS and CSS to a component
Your components may depend on third-party packages or styling, or other shared logic.
To load these additional dependencies, you can use a nested [`Media` class](../../../reference/api#django_components.Component.Media).
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition),
with a few differences:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, [`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
3. Our Media class does NOT support [Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
[Learn more](../fundamentals/defining_js_css_html_files.md) about using Media.
```python title="[project root]/components/calendar/calendar.py"
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
class Media: # <--- new
js = [
"path/to/shared.js",
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
]
css = [
"path/to/shared.css",
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # Tailwind
]
def get_context_data(self):
return {
"date": "1970-01-01",
}
```
!!! note
Same as with the "primary" JS and CSS, the file paths files can be either:
1. Relative to the Python component file (as seen above),
2. Relative to any of the component directories as defined by
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
and/or [`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `[your apps]/components` dir and `[project root]/components`)
!!! info
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/).
@ -223,18 +301,57 @@ class Calendar(Component):
If you define a list of JS files, they will be executed one-by-one, left-to-right.
!!! note
#### Rules of execution of scripts in `Media.js`
Same as with the template file, the file paths for the JS and CSS files can be either:
The scripts defined in `Media.js` still follow the rules outlined above:
1. Relative to the Python component file (as seen above),
2. Relative to any of the component directories as defined by
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
and/or [`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `[your apps]/components` dir and `[project root]/components`)
1. JS is executed in the order in which the components are found in the HTML.
2. JS will be executed only once, even if there is multiple instances of the same component.
Additionally to `Media.js` applies that:
And that's it! If you were to embed this component in an HTML, django-components will
automatically embed the associated JS and CSS.
1. JS in `Media.js` is executed **before** the component's primary JS.
2. JS in `Media.js` is executed **in the same order** as it was defined.
3. If there is multiple components that specify the same JS path or URL in `Media.js`,
this JS will be still loaded and executed only once.
Putting all of this together, our `Calendar` component above would render HTML like so:
```html
<html>
<head>
...
<!-- CSS from Media.css -->
<link href="/static/path/to/shared.css" media="all" rel="stylesheet">
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" media="all" rel="stylesheet">
<!-- CSS from Component.css_file -->
<style>
.calendar {
width: 200px;
background: pink;
}
.calendar span {
font-weight: bold;
}
</style>
</head>
<body>
...
<!-- JS from Media.js -->
<script src="/static/path/to/shared.js"></script>
<script src="https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js"></script>
<!-- JS from Component.js_file -->
<script>
(function () {
document.querySelector(".calendar").onclick = () => {
alert("Clicked calendar!");
};
})();
</script>
</body>
</html>
```
---
Now that we have a fully-defined component, [next let's use it in a Django template ➡️](./components_in_templates.md).

View file

@ -32,10 +32,8 @@ from django_components import Component, register # <--- new
@register("calendar") # <--- new
class Calendar(Component):
template_name = "calendar.html"
class Media:
js = "calendar.js"
css = "calendar.css"
js_file = "calendar.js"
css_file = "calendar.css"
def get_context_data(self):
return {
@ -48,7 +46,7 @@ by calling `{% load component_tags %}` inside the template.
!!! info
Why do we have to register components?
**Why do we have to register components?**
We want to use our component as a template tag (`{% ... %}`) in Django template.
@ -170,7 +168,7 @@ and keeping your CSS and Javascript in the static directory.
!!! info
How does django-components pick up registered components?
**How does django-components pick up registered components?**
Notice that it was enough to add [`@register`](../../../reference/api#django_components.register) to the component.
We didn't need to import the component file anywhere to execute it.
@ -191,6 +189,9 @@ and keeping your CSS and Javascript in the static directory.
...
```
You can now render the components! But our component will render the same content now matter where
and how many times we use it. [Let's parametrise some of its state, so that our Calendar component
You can now render the components in templates!
---
Currently our component always renders the same content. [Let's parametrise it, so that our Calendar component
is configurable from within the template ➡️](./parametrising_components.md)

View file

@ -222,4 +222,6 @@ the parametrized version of the component:
</div>
```
---
Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)

View file

@ -5,19 +5,51 @@ weight: 1
A component in django-components can be as simple as a Django template and Python code to declare the component:
```py
```htmldjango title="calendar.html"
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```py title="calendar.py"
from django_components import Component
class Calendar(Component):
template = """
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
"""
template_name = "calendar.html"
```
Or a combination of Django template, Python, CSS, and Javascript:
```htmldjango title="calendar.html"
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```css title="calendar.css"
.calendar {
width: 200px;
background: pink;
}
```
```js title="calendar.js"
document.querySelector(".calendar").onclick = function () {
alert("Clicked calendar!");
};
```
```py title="calendar.py"
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
```
Alternatively, you can "inline" HTML, JS, and CSS right into the component class:
```py
from django_components import Component
@ -44,14 +76,9 @@ class Calendar(Component):
!!! note
With django-components, you can "inline" the HTML, JS and CSS code into the Python class,
as seen above.
You can set up [syntax highlighting](../../guides/setup/syntax_highlight.md),
but autocompletion / intellisense does not yet work.
So, in the example below we define the Django template in a separate file, `calendar.html`,
to allow our IDEs to interpret the file as HTML / Django file.
If you "inline" the HTML, JS and CSS code into the Python class, you can set up
[syntax highlighting](../../guides/setup/syntax_highlight.md) for better experience.
However, autocompletion / intellisense does not work with syntax highlighting.
We'll start by creating a component that defines only a Django template:
@ -154,4 +181,6 @@ It will output
And voilá!! We've created our first component.
---
Next, [let's add JS and CSS to this component ➡️](./adding_js_and_css.md).