feat: add Media.extend (#890)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2025-01-07 22:55:17 +01:00 committed by GitHub
parent 5e8770c720
commit ab037f24b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 727 additions and 61 deletions

View file

@ -22,11 +22,10 @@
#### Refactor #### Refactor
- The canonical way to define a template file was changed from `template_name` to `template_file`, - The canonical way to define a template file was changed from `template_name` to `template_file`, to align with the rest of the API.
to align with the rest of the API.
`template_name` remains for backwards compatibility. When you get / set `template_name`, `template_name` remains for backwards compatibility. When you get / set `template_name`,
internally this is proxied to `template_file`. internally this is proxied to `template_file`.
- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes: - The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes:
@ -42,6 +41,12 @@
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). 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).
- Component inheritance:
- When you subclass a component, the JS and CSS defined on parent's `Media` class is now inherited by the child component.
- You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
- When child component defines either `template` or `template_file`, both of parent's `template` and `template_file` are ignored. The same applies to `js_file` and `css_file`.
- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal. - The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
## v0.123 ## v0.123

View file

@ -137,6 +137,9 @@ This `Media` class behaves similarly to
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), - 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 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()`. resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See
[Controlling Media Inheritance](../../fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
However, there's a few differences from Django's Media class: However, there's a few differences from Django's Media class:
@ -145,8 +148,6 @@ However, there's a few differences from Django's Media class:
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, 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 [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)). (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 ```py
class MyTable(Component): class MyTable(Component):
@ -402,8 +403,8 @@ print(Calendar.css)
## Accessing component's Media files ## Accessing component's Media files
To access the files defined under [`Component.Media`](../../../reference/api#django_components.Component.Media), To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
you can access [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase). use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
This is consistent behavior with This is consistent behavior with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition). [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
@ -419,6 +420,95 @@ print(MyComponent.media)
# <link href="/static/path/to/style.css" media="all" rel="stylesheet"> # <link href="/static/path/to/style.css" media="all" rel="stylesheet">
``` ```
### `Component.Media` vs `Component.media`
When working with component media files, there are a few important concepts to understand:
- `Component.Media`
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
- This class is NOT instantiated, it merely holds the JS / CSS files.
- `Component.media`
- Returns all resolved media files, **including** those inherited from parent components
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class ChildComponent(ParentComponent):
class Media:
js = ["child.js"]
# Access only this component's media
print(ChildComponent.Media.js) # ["child.js"]
# Access all inherited media
print(ChildComponent.media._js) # ["parent.js", "child.js"]
```
!!! note
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media), 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) you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
([See example](#customize-how-paths-are-rendered-into-html-tags)). ([See example](#customize-how-paths-are-rendered-into-html-tags)).
## Controlling Media Inheritance
By default, the media files are inherited from the parent component.
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
js = ["script.js"]
print(MyComponent.media._js) # ["parent.js", "script.js"]
```
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
extend = False # Don't inherit parent media
js = ["script.js"]
print(MyComponent.media._js) # ["script.js"]
```
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
# Only inherit from these, ignoring the files from the parent
extend = [OtherComponent1, OtherComponent2]
js = ["script.js"]
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
```
!!! info
The `extend` behaves consistently with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
with one exception:
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).

View file

@ -0,0 +1,132 @@
---
title: Subclassing components
weight: 11
---
In larger projects, you might need to write multiple components with similar behavior.
In such cases, you can extract shared behavior into a standalone component class to keep things
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
When subclassing a component, there's a couple of things to keep in mind:
### Template, JS, and CSS Inheritance
When it comes to the pairs:
- [`Component.template`](../../reference/api.md#django_components.Component.template)/[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
- [`Component.js`](../../reference/api.md#django_components.Component.js)/[`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
- [`Component.css`](../../reference/api.md#django_components.Component.css)/[`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
inheritance follows these rules:
- If a child component class defines either member of a pair (e.g., either [`template`](../../reference/api.md#django_components.Component.template) or [`template_file`](../../reference/api.md#django_components.Component.template_file)), it takes precedence and the parent's definition is ignored completely.
- For example, if a child component defines [`template_file`](../../reference/api.md#django_components.Component.template_file), the parent's [`template`](../../reference/api.md#django_components.Component.template) or [`template_file`](../../reference/api.md#django_components.Component.template_file) will be ignored.
- This applies independently to each pair - you can inherit the JS while overriding the template, for instance.
For example:
```python
class BaseCard(Component):
template = """
<div class="card">
<div class="card-content">{{ content }}</div>
</div>
"""
css = """
.card {
border: 1px solid gray;
}
"""
js = "console.log('Base card loaded');"
# This class overrides parent's template, but inherits CSS and JS
class SpecialCard(BaseCard):
template = """
<div class="card special">
<div class="card-content">✨ {{ content }} ✨</div>
</div>
"""
# This class overrides parent's template and CSS, but inherits JS
class CustomCard(BaseCard):
template_file = "custom_card.html"
css = """
.card {
border: 2px solid gold;
}
"""
```
### Media Class Inheritance
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
- If both parent and child define a `Media` class, the child's media will automatically include both its own and the parent's JS and CSS files.
- This behavior can be configured using the [`extend`](../../reference/api.md#django_components.Component.Media.extend) attribute in the Media class, similar to Django's forms.
Read more on this in [Controlling Media Inheritance](./defining_js_css_html_files.md#controlling-media-inheritance).
For example:
```python
class BaseModal(Component):
template = "<div>Modal content</div>"
class Media:
css = ["base_modal.css"]
js = ["base_modal.js"] # Contains core modal functionality
class FancyModal(BaseModal):
class Media:
# Will include both base_modal.css/js AND fancy_modal.css/js
css = ["fancy_modal.css"] # Additional styling
js = ["fancy_modal.js"] # Additional animations
class SimpleModal(BaseModal):
class Media:
extend = False # Don't inherit parent's media
css = ["simple_modal.css"] # Only this CSS will be included
js = ["simple_modal.js"] # Only this JS will be included
```
### Regular Python Inheritance
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.
For example:
```python
class BaseForm(Component):
template = """
<form>
{{ form_content }}
<button type="submit">
{{ submit_text }}
</button>
</form>
"""
def get_context_data(self, **kwargs):
return {
"form_content": self.get_form_content(),
"submit_text": "Submit"
}
def get_form_content(self):
return "<input type='text' name='data'>"
class ContactForm(BaseForm):
# Extend parent's "context"
# but override "submit_text"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["submit_text"] = "Send Message"
return context
# Completely override parent's get_form_content
def get_form_content(self):
return """
<input type='text' name='name' placeholder='Your Name'>
<input type='email' name='email' placeholder='Your Email'>
<textarea name='message' placeholder='Your Message'></textarea>
"""
```

View file

@ -235,7 +235,7 @@ 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). 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. 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). 3. If you set `Media.extend` to a list, it should be a list of `Component` classes.
[Learn more](../fundamentals/defining_js_css_html_files.md) about using Media. [Learn more](../fundamentals/defining_js_css_html_files.md) about using Media.

View file

@ -490,6 +490,9 @@ class Component(
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`. 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 - 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()`. resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- You can set [`extend`](../api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See
[Controlling Media Inheritance](../../concepts/fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
However, there's a few differences from Django's Media class: However, there's a few differences from Django's Media class:
@ -498,8 +501,6 @@ class Component(
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`, 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)). (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)
**Example:** **Example:**
@ -518,7 +519,7 @@ class Component(
"print": ["path/to/style2.css"], "print": ["path/to/style2.css"],
} }
``` ```
""" """ # noqa: E501
response_class = HttpResponse response_class = HttpResponse
"""This allows to configure what class is used to generate response from `render_to_response`""" """This allows to configure what class is used to generate response from `render_to_response`"""

View file

@ -1,5 +1,6 @@
import os import os
import sys import sys
from collections import deque
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
@ -170,13 +171,57 @@ class ComponentMediaInput(Protocol):
``` ```
""" """
extend: Union[bool, List[Type["Component"]]] = True
"""
Configures whether the component should inherit the media files from the parent component.
- If `True`, the component inherits the media files from the parent component.
- If `False`, the component does not inherit the media files from the parent component.
- If a list of components classes, the component inherits the media files ONLY from these specified components.
Read more in [Controlling Media Inheritance](../concepts/fundamentals/defining_js_css_html_files.md#controlling-media-inheritance) section.
**Example:**
Disable media inheritance:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
extend = False # Don't inherit parent media
js = ["script.js"]
print(MyComponent.media._js) # ["script.js"]
```
Specify which components to inherit from. In this case, the media files are inherited ONLY
from the specified components, and NOT from the original parent components:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
# Only inherit from these, ignoring the files from the parent
extend = [OtherComponent1, OtherComponent2]
js = ["script.js"]
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
```
""" # noqa: E501
@dataclass @dataclass
class ComponentMedia: class ComponentMedia:
resolved: bool = False resolved: bool = False
Media: Optional[Type[ComponentMediaInput]] = None Media: Optional[Type[ComponentMediaInput]] = None
media: Optional[MediaCls] = None
media_class: Type[MediaCls] = MediaCls
template: Optional[str] = None template: Optional[str] = None
template_file: Optional[str] = None template_file: Optional[str] = None
js: Optional[str] = None js: Optional[str] = None
@ -260,8 +305,6 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
# NOTE: We take the values from `attrs` so we consider only the values that were set on THIS class, # NOTE: We take the values from `attrs` so we consider only the values that were set on THIS class,
# and not the values that were inherited from the parent classes. # and not the values that were inherited from the parent classes.
Media=attrs.get("Media", None), Media=attrs.get("Media", None),
media=attrs.get("media", None),
media_class=attrs.get("media_class", None),
template=attrs.get("template", None), template=attrs.get("template", None),
template_file=attrs.get("template_file", None), template_file=attrs.get("template_file", None),
js=attrs.get("js", None), js=attrs.get("js", None),
@ -270,47 +313,11 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
css_file=attrs.get("css_file", None), css_file=attrs.get("css_file", None),
) )
# Because the media values are not defined directly on the instance, but held in `_component_media`,
# then simply accessing `_component_media.js` will NOT get the values from parent classes.
#
# So this function is like `getattr`, but for searching for values inside `_component_media`.
def get_comp_media_attr(attr: str) -> Any: def get_comp_media_attr(attr: str) -> Any:
for base in comp_cls.mro(): if attr == "media":
comp_media: Optional[ComponentMedia] = getattr(base, "_component_media", None) return _get_comp_cls_media(comp_cls)
if comp_media is None: else:
continue return _get_comp_cls_attr(comp_cls, attr)
if not comp_media.resolved:
_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
# is defined, we interpret it such that this (sub)class has overriden what was set by the parent class(es),
# and we won't search further up the MRO.
def check_pair_empty(inline_attr: str, file_attr: str) -> bool:
inline_attr_empty = getattr(comp_media, inline_attr, None) is None
file_attr_empty = getattr(comp_media, file_attr, None) is None
return inline_attr_empty and file_attr_empty
if attr in ("js", "js_file"):
if check_pair_empty("js", "js_file"):
continue
else:
return value
if attr in ("css", "css_file"):
if check_pair_empty("css", "css_file"):
continue
else:
return value
if attr in ("template", "template_file"):
if check_pair_empty("template", "template_file"):
continue
else:
return value
# For the other attributes, simply search for the closest non-null
if value is not None:
return value
return None
# Because of the lazy resolution, we want to know when the user tries to access the media attributes. # Because of the lazy resolution, we want to know when the user tries to access the media attributes.
# And because these fields are class attributes, we can't use `@property` decorator. # And because these fields are class attributes, we can't use `@property` decorator.
@ -329,6 +336,134 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
setattr(comp_cls, attr, InterceptDescriptor(attr)) setattr(comp_cls, attr, InterceptDescriptor(attr))
# Because the media values are not defined directly on the instance, but held in `_component_media`,
# then simply accessing `_component_media.js` will NOT get the values from parent classes.
#
# So this function is like `getattr`, but for searching for values inside `_component_media`.
def _get_comp_cls_attr(comp_cls: Type["Component"], attr: str) -> Any:
for base in comp_cls.mro():
comp_media: Optional[ComponentMedia] = getattr(base, "_component_media", None)
if comp_media is None:
continue
if not comp_media.resolved:
_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
# is defined, we interpret it such that this (sub)class has overriden what was set by the parent class(es),
# and we won't search further up the MRO.
def check_pair_empty(inline_attr: str, file_attr: str) -> bool:
inline_attr_empty = getattr(comp_media, inline_attr, None) is None
file_attr_empty = getattr(comp_media, file_attr, None) is None
return inline_attr_empty and file_attr_empty
if attr in ("js", "js_file"):
if check_pair_empty("js", "js_file"):
continue
else:
return value
if attr in ("css", "css_file"):
if check_pair_empty("css", "css_file"):
continue
else:
return value
if attr in ("template", "template_file"):
if check_pair_empty("template", "template_file"):
continue
else:
return value
# For the other attributes, simply search for the closest non-null
if value is not None:
return value
return None
media_cache: Dict[Type["Component"], MediaCls] = {}
def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
# Component's `media` attribute is a special case, because it should inherit all the JS/CSS files
# from the parent classes. So we need to walk up the MRO and merge all the Media classes.
#
# But before we do that, we need to ensure that all parent classes have resolved their `media` attributes.
# Because to be able to construct `media` for a Component class, all its parent classes must have resolved
# their `media`.
#
# So we will:
# 0. Cache the resolved media, so we don't have to resolve it again, and so we can store it even for classes
# that don't have `Media` attribute.
# 1. If the current class HAS `media` in the cache, we used that
# 2. Otherwise, we check if its parent bases have `media` in the cache,
# 3. If ALL parent bases have `media` in the cache, we can resolve the child class's `media`,
# and put it in the cache.
# 4. If ANY of the parent bases DOESN'T, then we add those parent bases to the stack (so they are processed
# right after this. And we add the child class right after that.
#
# E.g. `stack = [*cls.__bases__, cls, *stack]`
#
# That way, we go up one level of the bases, and then we eventually come back down to the
# class that we tried to resolve. But the second time, we will have `media` resolved for all its parent bases.
bases_stack = deque([comp_cls])
while bases_stack:
curr_cls = bases_stack.popleft()
if curr_cls in media_cache:
continue
# Prepare base classes
media_input = getattr(curr_cls, "Media", None)
media_extend = getattr(media_input, "extend", True)
# This ensures the same behavior as Django's Media class, where:
# - If `Media.extend == True`, then the media files are inherited from the parent classes.
# - If `Media.extend == False`, then the media files are NOT inherited from the parent classes.
# - If `Media.extend == [Component1, Component2, ...]`, then the media files are inherited only
# from the specified classes.
if media_extend is True:
bases = curr_cls.__bases__
elif media_extend is False:
bases = tuple()
else:
bases = media_extend
unresolved_bases = [base for base in bases if base not in media_cache]
if unresolved_bases:
# Put the current class's bases at the FRONT of the queue, and put the current class back right after that.
# E.g. `[parentCls1, parentCls2, currCls, ...]`
# That way, we first resolve the parent classes, and then the current class.
bases_stack.extendleft(reversed([*unresolved_bases, curr_cls]))
continue
# Now, if we got here, then either all the bases of the current class have had their `media` resolved,
# or the current class has NO bases. So now we construct the `media` for the current class.
media_cls = getattr(curr_cls, "media_class", MediaCls)
# NOTE: If the class is a component and and it was not yet resolved, accessing `Media` should resolve it.
media_js = getattr(media_input, "js", [])
media_css = getattr(media_input, "css", {})
media: MediaCls = media_cls(js=media_js, css=media_css)
# We have the current class's `media`, now we add the JS and CSS from the parent classes.
# NOTE: Django's implementation of `Media` should ensure that duplicate files are not added.
for base in bases:
base_media = media_cache.get(base, None)
if base_media is None:
continue
# Add JS / CSS from the base class's Media to the current class's Media.
# We make use of the fact that Django's Media class does this with `__add__` method.
#
# However, the `__add__` converts our `media_cls` to Django's Media class.
# So we also have to convert it back to `media_cls`.
merged_media = media + base_media
media = media_cls(js=merged_media._js, css=merged_media._css)
# Lastly, cache the merged-up Media, so we don't have to search further up the MRO the next time
media_cache[curr_cls] = media
return media_cache[comp_cls]
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. Resolve the media files associated with the component.
@ -399,11 +534,6 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static" comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
) )
media_cls = comp_media.media_class or MediaCls
media_js = getattr(comp_media.Media, "js", [])
media_css = getattr(comp_media.Media, "css", {})
comp_media.media = media_cls(js=media_js, css=media_css) if comp_media.Media is not None else None
comp_media.resolved = True comp_media.resolved = True

View file

@ -993,3 +993,311 @@ class MediaRelativePathTests(BaseTestCase):
self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered) self.assertInHTML('<link href="relative_file_pathobj.css" rel="stylesheet">', rendered)
self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered) self.assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
class SubclassingMediaTests(BaseTestCase):
def test_media_in_child_and_parent(self):
class ParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "parent.css"
js = "parent.js"
class ChildComponent(ParentComponent):
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="parent.js"></script>', rendered)
def test_media_in_child_and_grandparent(self):
class GrandParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "grandparent.css"
js = "grandparent.js"
class ParentComponent(GrandParentComponent):
Media = None
class ChildComponent(ParentComponent):
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="grandparent.js"></script>', rendered)
def test_media_in_parent_and_grandparent(self):
class GrandParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "grandparent.css"
js = "grandparent.js"
class ParentComponent(GrandParentComponent):
class Media:
css = "parent.css"
js = "parent.js"
class ChildComponent(ParentComponent):
pass
rendered = ChildComponent.render()
self.assertInHTML('<link href="parent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="parent.js"></script>', rendered)
self.assertInHTML('<script src="grandparent.js"></script>', rendered)
def test_media_in_multiple_bases(self):
class GrandParent1Component(Component):
class Media:
css = "grandparent1.css"
js = "grandparent1.js"
class GrandParent2Component(Component):
pass
# NOTE: The bases don't even have to be Component classes,
# as long as they have the nested `Media` class.
class GrandParent3Component:
# NOTE: When we don't subclass `Component`, we have to correctly format the `Media` class
class Media:
css = {"all": ["grandparent3.css"]}
js = ["grandparent3.js"]
class GrandParent4Component:
pass
class Parent1Component(GrandParent1Component, GrandParent2Component):
class Media:
css = "parent1.css"
js = "parent1.js"
class Parent2Component(GrandParent3Component, GrandParent4Component):
Media = None
class ChildComponent(Parent1Component, Parent2Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="grandparent3.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
self.assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="grandparent1.js"></script>', rendered)
self.assertInHTML('<script src="grandparent3.js"></script>', rendered)
def test_extend_false_in_child(self):
class Parent1Component(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "parent1.css"
js = "parent1.js"
class Parent2Component(Component):
class Media:
css = "parent2.css"
js = "parent2.js"
class ChildComponent(Parent1Component, Parent2Component):
class Media:
extend = False
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertNotIn("parent1.css", rendered)
self.assertNotIn("parent2.css", rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("parent1.js", rendered)
self.assertNotIn("parent2.js", rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_false_in_parent(self):
class GrandParentComponent(Component):
class Media:
css = "grandparent.css"
js = "grandparent.js"
class Parent1Component(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "parent1.css"
js = "parent1.js"
class Parent2Component(GrandParentComponent):
class Media:
extend = False
css = "parent2.css"
js = "parent2.js"
class ChildComponent(Parent1Component, Parent2Component):
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertNotIn("grandparent.css", rendered)
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("grandparent.js", rendered)
self.assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="parent2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_list_in_child(self):
class Parent1Component(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "parent1.css"
js = "parent1.js"
class Parent2Component(Component):
class Media:
css = "parent2.css"
js = "parent2.js"
class Other1Component(Component):
class Media:
css = "other1.css"
js = "other1.js"
class Other2Component:
class Media:
css = {"all": ["other2.css"]}
js = ["other2.js"]
class ChildComponent(Parent1Component, Parent2Component):
class Media:
extend = [Other1Component, Other2Component]
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertNotIn("parent1.css", rendered)
self.assertNotIn("parent2.css", rendered)
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("parent1.js", rendered)
self.assertNotIn("parent2.js", rendered)
self.assertInHTML('<script src="other1.js"></script>', rendered)
self.assertInHTML('<script src="other2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)
def test_extend_list_in_parent(self):
class Other1Component(Component):
class Media:
css = "other1.css"
js = "other1.js"
class Other2Component:
class Media:
css = {"all": ["other2.css"]}
js = ["other2.js"]
class GrandParentComponent(Component):
class Media:
css = "grandparent.css"
js = "grandparent.js"
class Parent1Component(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "parent1.css"
js = "parent1.js"
class Parent2Component(GrandParentComponent):
class Media:
extend = [Other1Component, Other2Component]
css = "parent2.css"
js = "parent2.js"
class ChildComponent(Parent1Component, Parent2Component):
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
self.assertNotIn("grandparent.css", rendered)
self.assertInHTML('<link href="other1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="other2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="parent2.css" media="all" rel="stylesheet">', rendered)
self.assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
self.assertNotIn("grandparent.js", rendered)
self.assertInHTML('<script src="other1.js"></script>', rendered)
self.assertInHTML('<script src="other2.js"></script>', rendered)
self.assertInHTML('<script src="parent1.js"></script>', rendered)
self.assertInHTML('<script src="parent2.js"></script>', rendered)
self.assertInHTML('<script src="child.js"></script>', rendered)