docs: add documentation around component media (#877)

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 22:03:31 +01:00 committed by GitHub
parent 57f89e2dcf
commit 329a398c61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 610 additions and 74 deletions

View file

@ -2,13 +2,37 @@
## v0.124
#### Feat
- Instead of inlining the JS and CSS under `Component.js` and `Component.css`, you can move
them to their own files, and link the JS/CSS files with `Component.js_file` and `Component.css_file`.
Even when you specify the JS/CSS with `Component.js_file` or `Component.css_file`, then you can still
access the content under `Component.js/css` - behind the scenes, the content of the JS/CSS files
will be set to `Component.js/css` upon first access.
With this change, the role of `Component.js/css` and the JS/CSS in `Component.Media` has changed:
- The JS/CSS defined in `Component.js/css` or `Component.js/css_file` is the "main" JS/CSS
- The JS/CSS defined in `Component.Media.js/css` are secondary or additional
See the updated ["Getting Started" tutorial](https://EmilStenstrom.github.io/django-components/0.124/concepts/getting_started/adding_js_and_css/)
#### Refactor
- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes:
- While `component_id` was unique every time you instantiated `Component`, The new `id` is unique
- While `component_id` was unique every time you instantiated `Component`, the new `id` is unique
every time you render the component (e.g. with `Component.render()`)
- The new `id` is available only during render, so e.g. from within `get_context_data()`
- The new `id` is available only during render, so e.g. from within `get_context_data()`
- Component's HTML / CSS / JS are now resolved and loaded lazily. That is, if you specify `template_name`,
`js_file`, `css_file`, or `Media.js/css`, the file paths will be resolved only once you:
1. Try to access component's HTML / CSS / JS, or
2. Render the component.
Read more on [Accessing component's HTML / JS / CSS](https://EmilStenstrom.github.io/django-components/0.124/concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags).
## v0.123

View file

@ -3,19 +3,60 @@ title: Defining HTML / JS / CSS files
weight: 8
---
django_component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
As you could have seen in [the tutorial](../../concepts/getting_started/adding_js_and_css.md), there's multiple ways how you can associate
HTML / JS / CSS with a component:
To be familiar with how Django handles static files, we recommend reading also:
- You can set [`Component.template`](../../reference/api.md#django_components.Component.template),
[`Component.css`](../../reference/api.md#django_components.Component.css) and
[`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
as inlined code.
- You can set [`Component.template_name`](../../reference/api.md#django_components.Component.template_name),
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) and
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
for a component in separate files.
- You can link additional CSS / JS files using
[`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js)
and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
!!! warning
## Defining file paths relative to component or static dirs
You **cannot** use both inlined code **and** separate file for a single language type:
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`, `js_file` and `css_file` respectively:
- You can only either set `Component.template` or `Component.template_name`
- You can only either set `Component.css` or `Component.css_file`
- You can only either set `Component.js` or `Component.js_file`
```py
# In a file [project root]/components/calendar/calendar.py
However, you can freely mix these for different languages:
```py
class MyTable(Component):
template: types.django_html = """
<div class="welcome">
Hi there!
</div>
"""
js_file = "my_table.js"
css_file = "my_table.css"
```
!!! note
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:
- [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
As seen in the [getting started example](../getting_started/your_first_component.md), to associate HTML / JS / CSS
files with a component, you can set them as
[`Component.template_name`](../../reference/api.md#django_components.Component.template_name),
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
and
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) respectively:
```py title="[project root]/components/calendar/calendar.py"
from django_components import Component, register
@register("calendar")
@ -25,14 +66,21 @@ class Calendar(Component):
js_file = "script.js"
```
In the example above, the files are defined relative to the directory where `component.py` is.
In the example above, we defined the files relative to the directory where the component file is defined.
Alternatively, you can specify the file paths relative to the directories set in `COMPONENTS.dirs` or `COMPONENTS.app_dirs`.
Alternatively, you can specify the file paths relative to the directories set in
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
Assuming that `COMPONENTS.dirs` contains path `[project root]/components`, we can rewrite the example as:
If you specify the paths relative to component's directory, django-componenents does the conversion automatically
for you.
```py
# In a file [project root]/components/calendar/calendar.py
Thus, assuming that
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
contains path `[project root]/components`, the example above is the same as writing:
```py title="[project root]/components/calendar/calendar.py"
from django_components import Component, register
@register("calendar")
@ -42,26 +90,78 @@ class Calendar(Component):
js_file = "calendar/script.js"
```
NOTE: In case of conflict, the preference goes to resolving the files relative to the component's directory.
!!! important
## Defining multiple paths
**File path resolution in-depth**
At component class creation, django-components checks all file paths defined on the component (e.g. `Component.template_name`).
For each file path, it checks if the file path is relative to the component's directory.
And such file exists, the component's file path is re-written to be defined relative to a first matching directory
in [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
**Example:**
```py title="[root]/components/mytable/mytable.py"
class MyTable(Component):
template_name = "mytable.html"
```
1. Component `MyTable` is defined in file `[root]/components/mytable/mytable.py`.
2. The component's directory is thus `[root]/components/mytable/`.
3. Because `MyTable.template_name` is `mytable.html`, django-components tries to
resolve it as `[root]/components/mytable/mytable.html`.
4. django-components checks the filesystem. If there's no such file, nothing happens.
5. If there IS such file, django-components tries to rewrite the path.
6. django-components searches `COMPONENTS.dirs` and `COMPONENTS.app_dirs` for a first
directory that contains `[root]/components/mytable/mytable.html`.
7. It comes across `[root]/components/`, which DOES contain the path to `mytable.html`.
8. Thus, it rewrites `template_name` from `mytable.html` to `mytable/mytable.html`.
NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
## Defining additional JS and CSS files
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).
using the nested [`Component.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:
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):
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)
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
`media_class.render_js()` or `media_class.render_css()`.
- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
However, there's a few differences from Django's Media class:
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 [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
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):
class MyTable(Component):
class Media:
js = ["path/to/script1.js", "path/to/script2.js"]
css = ["path/to/style1.css", "path/to/style2.css"]
js = [
"path/to/script.js",
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
]
css = {
"all": [
"path/to/style.css",
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
],
"print": ["path/to/style2.css"],
}
```
## Configuring CSS Media Types
@ -78,20 +178,29 @@ class MyComponent(Component):
class Media:
css = {
"all": "path/to/style1.css",
"print": "path/to/style2.css",
"print": ["path/to/style2.css", "path/to/style3.css"],
}
```
```py
class MyComponent(Component):
class Media:
css = {
"all": ["path/to/style1.css", "path/to/style2.css"],
"print": ["path/to/style3.css", "path/to/style4.css"],
}
```
!!! note
NOTE: When you define CSS as a string or a list, the `all` media type is implied.
When you define CSS as a string or a list, the `all` media type is implied.
So these two examples are the same:
```py
class MyComponent(Component):
class Media:
css = "path/to/style1.css"
```
```py
class MyComponent(Component):
class Media:
css = {
"all": ["path/to/style1.css"],
}
```
## Supported types for file paths
@ -103,6 +212,8 @@ File paths can be any of:
- `SafeData` (`__html__` method)
- `Callable` that returns any of the above, evaluated at class creation (`__new__`)
See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
```py
from pathlib import Path
@ -126,18 +237,26 @@ class SimpleComponent(Component):
]
```
## Path as objects
## Paths 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.
In the example [above](#supported-types-for-file-paths), you can see that when we used Django's
[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.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.
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 `COMPONENTS.dirs`.
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory,
nor relative to [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs).
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
In the example below, we make use of "safe" strings to add `type="module"` to the script tag that will fetch `calendar/script2.js`.
In this case, we implemented a "safe" string by defining a `__html__` method.
```py
class LazyJsPath:
class ModuleJsPath:
def __init__(self, static_path: str) -> None:
self.static_path = static_path
@ -162,11 +281,11 @@ class Calendar(Component):
# <script> tag constructed by Media class
"calendar/script1.js",
# Custom <script> tag
LazyJsPath("calendar/script2.js"),
ModuleJsPath("calendar/script2.js"),
]
```
## Customize how paths are rendered into HTML tags with `media_class`
## Customize how paths are rendered into HTML tags
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.
@ -207,4 +326,98 @@ class Calendar(Component):
media_class = MyMedia
```
NOTE: The instance of the `Media` class (or it's subclass) is available under `Component.media` after the class creation (`__new__`).
## Accessing component's HTML / JS / CSS
Component's HTML / CSS / JS is resolved and loaded lazily.
This means that, when you specify any of
[`template_name`](../../reference/api.md#django_components.Component.template_name),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file),
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
these file paths will be resolved only once you either:
1. Access any of the following attributes on the component:
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_name`](../../reference/api.md#django_components.Component.template_name),
[`js`](../../reference/api.md#django_components.Component.js),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
2. Render the component.
Once the component's media files have been loaded once, they will remain in-memory
on the Component class:
- HTML from [`Component.template_name`](../../reference/api.md#django_components.Component.template_name)
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
Thus, whether you define HTML via
[`Component.template_name`](../../reference/api.md#django_components.Component.template_name)
or [`Component.template`](../../reference/api.md#django_components.Component.template),
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
And the same applies for JS and CSS.
**Example:**
```py
# When we create Calendar component, the files like `calendar/template.html`
# are not yet loaded!
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
class Media:
css = "calendar/style1.css"
js = "calendar/script2.js"
# It's only at this moment that django-components reads the files like `calendar/template.html`
print(Calendar.css)
# Output:
# .calendar {
# width: 200px;
# background: pink;
# }
```
!!! warning
**Do NOT modify HTML / CSS / JS after it has been loaded**
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
If you need to dynamically change these media files, consider instead defining multiple Components.
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior.
## Accessing component's Media files
To access the files defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
you can access [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
This is consistent behavior with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
```py
class MyComponent(Component):
class Media:
js = "path/to/script.js"
css = "path/to/style.css"
print(MyComponent.media)
# Output:
# <script src="/static/path/to/script.js"></script>
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
```
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
([See example](#customize-how-paths-are-rendered-into-html-tags)).

View file

@ -11,6 +11,19 @@ h6 {
padding-top: 40px;
}
.md-typeset h3 {
/* Original styling */
font-size: 1.25em;
font-weight: 400;
letter-spacing: -.01em;
line-height: 1.5;
margin: 1.6em 0 0.8em;
/* Custom */
border-top: 0.5px solid var(--md-typeset-color);
padding-top: 40px;
}
.md-nav__item--section {
margin-top: 32px;
}

View file

@ -7,6 +7,7 @@
from django_components.app_settings import ContextBehavior, ComponentsSettings
from django_components.autodiscovery import autodiscover, import_libraries
from django_components.component import Component, ComponentVars, ComponentView
from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath
from django_components.component_registry import (
AlreadyRegistered,
ComponentRegistry,
@ -44,6 +45,8 @@ __all__ = [
"Component",
"ComponentFileEntry",
"ComponentFormatter",
"ComponentMediaInput",
"ComponentMediaInputPath",
"ComponentRegistry",
"ComponentVars",
"ComponentView",

View file

@ -212,7 +212,20 @@ class Component(
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
Only one of [`template_name`](../api#django_components.Component.template_name),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
**Example:**
```py
class MyComponent(Component):
template_name = "path/to/template.html"
def get_context_data(self):
return {"name": "World"}
```
"""
def get_template_name(self, context: Context) -> Optional[str]:
@ -222,7 +235,10 @@ class Component(
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
Only one of [`template_name`](../api#django_components.Component.template_name),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
"""
return None
@ -230,14 +246,30 @@ class Component(
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
Only one of [`template_name`](../api#django_components.Component.template_name),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
**Example:**
```py
class MyComponent(Component):
template = "Hello, {{ name }}!"
def get_context_data(self):
return {"name": "World"}
```
"""
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
Only one of [`template_name`](../api#django_components.Component.template_name),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
"""
return None
@ -245,50 +277,167 @@ class Component(
return cast(DataType, {})
js: Optional[str] = None
"""Main JS associated with this component inlined as string."""
"""
Main JS associated with this component inlined as string.
Only one of [`js`](../api#django_components.Component.js) or
[`js_file`](../api#django_components.Component.js_file) must be defined.
**Example:**
```py
class MyComponent(Component):
js = "console.log('Hello, World!');"
```
"""
js_file: Optional[str] = None
"""
Main JS associated with this component as file path.
When you create a Component subclass, these will happen:
When you create a Component class with `js_file`, these will happen:
1. The filepath is resolved, in case it is relative to the component Python file.
2. The file is read and its contents will be available under `MyComponent.js`.
1. If the file path is relative to the directory where the component's Python file is,
the path is resolved.
2. The file is read and its contents is set to [`Component.js`](../api#django_components.Component.js).
Only one of [`js`](../api#django_components.Component.js) or
[`js_file`](../api#django_components.Component.js_file) must be defined.
**Example:**
```js title="path/to/script.js"
console.log('Hello, World!');
```
```py title="path/to/component.py"
class MyComponent(Component):
js_file = "path/to/script.js"
print(MyComponent.js)
# Output: console.log('Hello, World!');
```
"""
css: Optional[str] = None
"""Main CSS associated with this component inlined as string."""
"""
Main CSS associated with this component inlined as string.
Only one of [`css`](../api#django_components.Component.css) or
[`css_file`](../api#django_components.Component.css_file) must be defined.
**Example:**
```py
class MyComponent(Component):
css = \"\"\"
.my-class {
color: red;
}
\"\"\"
```
"""
css_file: Optional[str] = None
"""
Main CSS associated with this component as file path.
When you create a Component subclass, these will happen:
When you create a Component class with `css_file`, these will happen:
1. The filepath is resolved, in case it is relative to the component Python file.
2. The file is read and its contents will be available under `MyComponent.css`.
1. If the file path is relative to the directory where the component's Python file is,
the path is resolved.
2. The file is read and its contents is set to [`Component.css`](../api#django_components.Component.css).
Only one of [`css`](../api#django_components.Component.css) or
[`css_file`](../api#django_components.Component.css_file) must be defined.
**Example:**
```css title="path/to/style.css"
.my-class {
color: red;
}
```
```py title="path/to/component.py"
class MyComponent(Component):
css_file = "path/to/style.css"
print(MyComponent.css)
# Output:
# .my-class {
# color: red;
# };
```
"""
media: Optional[MediaCls] = None
"""
Normalized definition of JS and CSS media files associated with this component.
`None` if `Media` is not defined.
`None` if [`Component.Media`](../api#django_components.Component.Media) is not defined.
This field is generated from [`Component.media_class`](../api#django_components.Component.media_class).
Read more on [Accessing component's HTML / JS / CSS](../../concepts/fundamentals/defining_js_css_html_files/#accessing-components-media-files).
**Example:**
```py
class MyComponent(Component):
class Media:
js = "path/to/script.js"
css = "path/to/style.css"
print(MyComponent.media)
# Output:
# <script src="/static/path/to/script.js"></script>
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
```
""" # noqa: E501
NOTE: This field is generated from `Component.media_class`.
"""
media_class: Type[MediaCls] = MediaCls
"""
Set the [Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition)
that will be instantiated with the JS and CSS media files from
[`Component.Media`](../api#django_components.Component.Media).
This is useful when you want to customize the behavior of the media files, like
customizing how the JS or CSS files are rendered into `<script>` or `<link>` HTML tags.
Read more in [Defining HTML / JS / CSS files](../../concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags-with-media_class).
**Example:**
```py
class MyTable(Component):
class Media:
js = "path/to/script.js"
css = "path/to/style.css"
media_class = MyMediaClass
```
""" # noqa: E501
Media: Optional[Type[ComponentMediaInput]] = None
"""
Defines JS and CSS media files associated with this component.
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:
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
`media_class.render_js()` or `media_class.render_css()`.
- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
- A `SafeString` (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
However, there's a few differences from Django's Media class:
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)
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function.
[`SafeString`](https://dev.to/doridoro/django-safestring-afj), or a function
(See [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
3. Our Media class does NOT support
[Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
@ -384,13 +533,31 @@ class Component(
@property
def id(self) -> str:
"""
Render ID - This ID is unique for every time a `Component.render()` (or equivalent) is called.
This ID is unique for every time a [`Component.render()`](../api#django_components.Component.render)
(or equivalent) is called (AKA "render ID").
This is useful for logging or debugging.
Render IDs have the chance of collision 1 in 3.3M.
Raises `RuntimeError` if accessed outside of rendering execution.
Raises RuntimeError if called outside of rendering execution.
A single render ID has a chance of collision 1 in 3.3M. However, due to birthday paradox, the chance of
collision increases when approaching ~1,000 render IDs.
**Thus, there is a soft-cap of 1,000 components rendered on a single page.**
If you need to more than that, please open an issue on GitHub.
**Example:**
```py
class MyComponent(Component):
def get_context_data(self):
print(f"Rendering '{self.id}'")
return {}
MyComponent.render()
# Rendering 'ab3c4d'
```
"""
return self.input.id

View file

@ -21,6 +21,40 @@ if TYPE_CHECKING:
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_name", "js", "js_file", "css", "css_file")
ComponentMediaInputPath = Union[
str,
bytes,
SafeData,
Path,
os.PathLike,
Callable[[], Union[str, bytes, SafeData, Path, os.PathLike]],
]
"""
A type representing an entry in [Media.js](../api#django_components.ComponentMediaInput.js)
or [Media.css](../api#django_components.ComponentMediaInput.css).
If an entry is a [SafeString](https://dev.to/doridoro/django-safestring-afj) (or has `__html__` method),
then entry is assumed to be a formatted HTML tag. Otherwise, it's assumed to be a path to a file.
**Example:**
```py
class MyComponent
class Media:
js = [
"path/to/script.js",
b"script.js",
SafeString("<script src='path/to/script.js'></script>"),
]
css = [
Path("path/to/style.css"),
lambda: "path/to/style.css",
lambda: Path("path/to/style.css"),
]
```
"""
# This is the interface of the class that user is expected to define on the component class, e.g.:
# ```py
# class MyComponent(Component):
@ -30,7 +64,7 @@ COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_name", "js", "js_file",
# ```
class ComponentMediaInput(Protocol):
"""
Defines JS and CSS media files associated with this component.
Defines JS and CSS media files associated with a [`Component`](../api#django_components.Component).
```py
class MyTable(Component):
@ -49,8 +83,90 @@ class ComponentMediaInput(Protocol):
```
"""
css: Optional[Union[str, List[str], Dict[str, str], Dict[str, List[str]]]] = None
js: Optional[Union[str, List[str]]] = None
css: Optional[
Union[
ComponentMediaInputPath,
List[ComponentMediaInputPath],
Dict[str, ComponentMediaInputPath],
Dict[str, List[ComponentMediaInputPath]],
]
] = None
"""
CSS files associated with a [`Component`](../api#django_components.Component).
- If a string, it's assumed to be a path to a CSS file.
- If a list, each entry is assumed to be a path to a CSS file.
- If a dict, the keys are media types (e.g. "all", "print", "screen", etc.), and the values are either:
- A string, assumed to be a path to a CSS file.
- A list, each entry is assumed to be a path to a CSS file.
Each entry can be a string, bytes, SafeString, PathLike, or a callable that returns one of the former
(see [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
Examples:
```py
class MyComponent(Component):
class Media:
css = "path/to/style.css"
```
```py
class MyComponent(Component):
class Media:
css = ["path/to/style1.css", "path/to/style2.css"]
```
```py
class MyComponent(Component):
class Media:
css = {
"all": "path/to/style.css",
"print": "path/to/print.css",
}
```
```py
class MyComponent(Component):
class Media:
css = {
"all": ["path/to/style1.css", "path/to/style2.css"],
"print": "path/to/print.css",
}
```
"""
js: Optional[Union[ComponentMediaInputPath, List[ComponentMediaInputPath]]] = None
"""
JS files associated with a [`Component`](../api#django_components.Component).
- If a string, it's assumed to be a path to a JS file.
- If a list, each entry is assumed to be a path to a JS file.
Each entry can be a string, bytes, SafeString, PathLike, or a callable that returns one of the former
(see [`ComponentMediaInputPath`](../api#django_components.ComponentMediaInputPath)).
Examples:
```py
class MyComponent(Component):
class Media:
js = "path/to/script.js"
```
```py
class MyComponent(Component):
class Media:
js = ["path/to/script1.js", "path/to/script2.js"]
```
```py
class MyComponent(Component):
class Media:
js = lambda: ["path/to/script1.js", "path/to/script2.js"]
```
"""
@dataclass
@ -96,7 +212,7 @@ class ComponentMediaMeta(type):
def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict[str, Any]) -> Type:
# Normalize the various forms of Media inputs we allow
if "Media" in attrs:
normalize_media(attrs["Media"])
_normalize_media(attrs["Media"])
cls = super().__new__(mcs, name, bases, attrs)
comp_cls = cast(Type["Component"], cls)
@ -155,7 +271,7 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
if comp_media is None:
continue
if not comp_media.resolved:
resolve_media(base, comp_media)
_resolve_media(base, comp_media)
value = getattr(comp_media, attr, None)
# For each of the pairs of inlined_content + file (e.g. `js` + `js_file`), if at least one of the two
@ -204,7 +320,7 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
setattr(comp_cls, attr, InterceptDescriptor(attr))
def resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> None:
def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> None:
"""
Resolve the media files associated with the component.
@ -272,7 +388,7 @@ def resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> No
comp_media.resolved = True
def normalize_media(media: Type[ComponentMediaInput]) -> None:
def _normalize_media(media: Type[ComponentMediaInput]) -> None:
"""
Resolve the `Media` class associated with the component.
@ -394,7 +510,7 @@ def _is_media_filepath(filepath: Any) -> bool:
return False
def _normalize_media_filepath(filepath: Any) -> Union[str, SafeData]:
def _normalize_media_filepath(filepath: ComponentMediaInputPath) -> Union[str, SafeData]:
if callable(filepath):
filepath = filepath()