feat: paths as objects + user-provided Media cls + handle static (#526)

Co-authored-by: Emil Stenström <emil@emilstenstrom.se>
This commit is contained in:
Juro Oravec 2024-06-21 19:36:53 +02:00 committed by GitHub
parent 1d0d960211
commit 3c5a7ad823
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1106 additions and 146 deletions

219
README.md
View file

@ -35,7 +35,8 @@ Read on to learn about the details!
- [Rendering HTML attributes](#rendering-html-attributes)
- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject)
- [Component context and scope](#component-context-and-scope)
- [Rendering JS and CSS dependencies](#rendering-js-and-css-dependencies)
- [Defining HTML/JS/CSS files](#defining-htmljscss-files)
- [Rendering JS/CSS dependencies](#rendering-jscss-dependencies)
- [Available settings](#available-settings)
- [Logging and debugging](#logging-and-debugging)
- [Management Command](#management-command)
@ -262,9 +263,12 @@ from django_components import component
@component.register("calendar")
class Calendar(component.Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir will be automatically found. To customize which template to use based on context
# you can override def get_template_name() instead of specifying the below variable.
template_name = "calendar/template.html"
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found. To customize which template to use based on context
# you can override method `get_template_name` instead of specifying `template_name`.
#
# `template_name` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
template_name = "template.html"
# This component takes one parameter, a date string to show in the template
def get_context_data(self, date):
@ -272,9 +276,10 @@ class Calendar(component.Component):
"date": date,
}
# Both `css` and `js` can be relative to dir where `calendar.py` is, or relative to STATICFILES_DIRS
class Media:
css = "calendar/style.css"
js = "calendar/script.js"
css = "style.css"
js = "script.js"
```
And voilá!! We've created our first component.
@ -1645,7 +1650,207 @@ If you find yourself using the `only` modifier often, you can set the [context_b
Components can also access the outer context in their context methods like `get_context_data` by accessing the property `self.outer_context`.
## Rendering JS and CSS dependencies
## Defining HTML/JS/CSS files
django_component's management of files builds on top of [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:
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
### 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:
```py
# In a file [project root]/components/calendar/calendar.py
from django_components import component
@component.register("calendar")
class Calendar(component.Component):
template_name = "template.html"
class Media:
css = "style.css"
js = "script.js"
```
In the example above, the files are defined relative to the directory where `component.py` is.
Alternatively, you can specify the file paths relative to the directories set in `STATICFILES_DIRS`.
Assuming that `STATICFILES_DIRS` contains path `[project root]/components`, we can rewrite the example as:
```py
# In a file [project root]/components/calendar/calendar.py
from django_components import component
@component.register("calendar")
class Calendar(component.Component):
template_name = "calendar/template.html"
class Media:
css = "calendar/style.css"
js = "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.
```py
class MyComponent(component.Component):
class Media:
js = ["path/to/script1.js", "path/to/script2.js"]
css = ["path/to/style1.css", "path/to/style2.css"]
```
### Configuring CSS Media Types
You can define which stylesheets will be associated with which
[CSS Media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
Again, you can set either a single file or a list of files per media type:
```py
class MyComponent(component.Component):
class Media:
css = {
"all": "path/to/style1.css",
"print": "path/to/style2.css",
}
```
```py
class MyComponent(component.Component):
class Media:
css = {
"all": ["path/to/style1.css", "path/to/style2.css"],
"print": ["path/to/style3.css", "path/to/style4.css"],
}
```
NOTE: When you define CSS as a string or a list, the `all` media type is implied.
### Supported types for file paths
File paths can be any of:
- `str`
- `bytes`
- `PathLike` (`__fspath__` method)
- `SafeData` (`__html__` method)
- `Callable` that returns any of the above, evaluated at class creation (`__new__`)
```py
from pathlib import Path
from django.utils.safestring import mark_safe
class SimpleComponent(component.Component):
class Media:
css = [
mark_safe('<link href="/static/calendar/style.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>'),
Path("calendar/script1.js"),
"calendar/script2.js",
b"calendar/script3.js",
lambda: "calendar/script4.js",
]
```
### Path as objects
In the example [above](#supported-types-for-file-paths), you could see that when we used `mark_safe` to mark a string as a `SafeString`, we had to define the full `<script>`/`<link>` tag.
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects) feature, where "safe" strings are taken as is, and accessed only at render time.
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory, nor relative to `STATICFILES_DIRS`.
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
```py
class LazyJsPath:
def __init__(self, static_path: str) -> None:
self.static_path = static_path
def __html__(self):
full_path = static(self.static_path)
return format_html(
f'<script type="module" src="{full_path}"></script>'
)
@component.register("calendar")
class Calendar(component.Component):
template_name = "calendar/template.html"
def get_context_data(self, date):
return {
"date": date,
}
class Media:
css = "calendar/style.css"
js = [
# <script> tag constructed by Media class
"calendar/script1.js",
# Custom <script> tag
LazyJsPath("calendar/script2.js"),
]
```
### Customize how paths are rendered into HTML tags with `media_class`
Sometimes you may need to change how all CSS `<link>` or JS `<script>` tags are rendered for a given component. You can achieve this by providing your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to component's `media_class` attribute.
Normally, the JS and CSS paths are passed to `Media` class, which decides how the paths are resolved and how the `<link>` and `<script>` tags are constructed.
To change how the tags are constructed, you can override the [`Media.render_js` and `Media.render_css` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
```py
from django.forms.widgets import Media
from django_components import component
class MyMedia(Media):
# Same as original Media.render_js, except
# the `<script>` tag has also `type="module"`
def render_js(self):
tags = []
for path in self._js:
if hasattr(path, "__html__"):
tag = path.__html__()
else:
tag = format_html(
'<script type="module" src="{}"></script>',
self.absolute_path(path)
)
return tags
@component.register("calendar")
class Calendar(component.Component):
template_name = "calendar/template.html"
class Media:
css = "calendar/style.css"
js = "calendar/script.js"
# Override the behavior of Media class
media_class = MyMedia
```
NOTE: The instance of the `Media` class (or it's subclass) is available under `Component.media` after the class creation (`__new__`).
## Rendering JS/CSS dependencies
The JS and CSS files included in components are not automatically rendered.
Instead, use the following tags to specify where to render the dependencies: