mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
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:
parent
57f89e2dcf
commit
329a398c61
6 changed files with 610 additions and 74 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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)).
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue