feat: Scoped slots + Updated docs (#495)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-05-23 07:08:10 +02:00 committed by GitHub
parent bdeb9c4e32
commit b1b66fd751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 804 additions and 254 deletions

518
README.md
View file

@ -18,6 +18,30 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
Read on to learn about the details!
## Table of Contents
- [Release notes](#release-notes)
- [Security notes 🚨](#security-notes-)
- [Installation](#installation)
- [Compatiblity](#compatiblity)
- [Create your first component](#create-your-first-component)
- [Using single-file components](#using-single-file-components)
- [Use the component in a template](#use-the-component-in-a-template)
- [Use components as views](#use-components-as-views)
- [Registering components](#registering-components)
- [Autodiscovery](#autodiscovery)
- [Using slots in templates](#using-slots-in-templates)
- [Passing data to components](#passing-data-to-components)
- [Rendering HTML attributes](#rendering-html-attributes)
- [Component context and scope](#component-context-and-scope)
- [Rendering JS and CSS dependencies](#rendering-js-and-css-dependencies)
- [Available settings](#available-settings)
- [Logging and debugging](#logging-and-debugging)
- [Management Command](#management-command)
- [Community examples](#community-examples)
- [Running django-components project locally](#running-django-components-project-locally)
- [Development guides](#development-guides)
## Release notes
**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components.
@ -33,7 +57,7 @@ Read on to learn about the details!
This change is done to simplify the API in anticipation of a 1.0 release of django_components. After 1.0 we intend to be stricter with big changes like this in point releases.
**Version 0.34** adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#components-as-views) for more details.
**Version 0.34** adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#use-components-as-views) for more details.
**Version 0.28** introduces 'implicit' slot filling and the `default` option for `slot` tags.
@ -237,19 +261,43 @@ class Calendar(component.Component):
And voilá!! We've created our first component.
## Autodiscovery
## Using single-file components
By default, the Python files in the `components` app are auto-imported in order to auto-register the components (e.g. `components/button/button.py`).
Components can also be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
```python
# In a file called [project root]/components/calendar.py
from django_components import component
from django_components import types as t
If you are using autodiscovery, keep a few points in mind:
@component.register("calendar")
class Calendar(component.Component):
def get_context_data(self, date):
return {
"date": date,
}
- Avoid defining any logic on the module-level inside the `components` dir, that you would not want to run anyway.
- Components inside the auto-imported files still need to be registered with `@component.register()`
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
template: t.django_html = """
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
"""
Autodiscovery can be disabled via in the [settings](#disable-autodiscovery).
css: t.css = """
.calendar-component { width: 200px; background: pink; }
.calendar-component span { font-weight: bold; }
"""
js: t.js = """
(function(){
if (document.querySelector(".calendar-component")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
}
})()
"""
```
This makes it easy to create small components without having to create a separate template, CSS, and JS file.
Note that the `t.django_html`, `t.css`, and `t.js` types are used to specify the type of the template, CSS, and JS files, respectively. This is not necessary, but if you're using VSCode with the [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS.
## Use the component in a template
@ -296,43 +344,86 @@ The output from the above template will be:
This makes it possible to organize your front-end around reusable components. Instead of relying on template tags and keeping your CSS and Javascript in the static directory.
## Using single-file components
## Use components as views
Components can also be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
_New in version 0.34_
Components can now be used as views. To do this, `Component` subclasses Django's `View` class. This means that you can use all of the [methods](https://docs.djangoproject.com/en/5.0/ref/class-based-views/base/#view) of `View` in your component. For example, you can override `get` and `post` to handle GET and POST requests, respectively.
In addition, `Component` now has a `render_to_response` method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
Here's an example of a calendar component defined as a view:
```python
# In a file called [project root]/components/calendar.py
from django_components import component
from django_components import types as t
@component.register("calendar")
class Calendar(component.Component):
def get_context_data(self, date):
return {
"date": date,
template = """
<div class="calendar-component">
<div class="header">
{% slot "header" %}{% endslot %}
</div>
<div class="body">
Today's date is <span>{{ date }}</span>
</div>
</div>
"""
def get(self, request, *args, **kwargs):
context = {
"date": request.GET.get("date", "2020-06-06"),
}
template: t.django_html = """
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
"""
css: t.css = """
.calendar-component { width: 200px; background: pink; }
.calendar-component span { font-weight: bold; }
"""
js: t.js = """
(function(){
if (document.querySelector(".calendar-component")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
}
})()
"""
slots = {
"header": "Calendar header",
}
return self.render_to_response(context, slots)
```
This makes it easy to create small components without having to create a separate template, CSS, and JS file.
Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
Note that the `t.django_html`, `t.css`, and `t.js` types are used to specify the type of the template, CSS, and JS files, respectively. This is not necessary, but if you're using VSCode with the [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS.
```python
# In a file called [project root]/components/urls.py
from django.urls import path
from components.calendar.calendar import Calendar
urlpatterns = [
path("calendar/", Calendar.as_view()),
]
```
Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
Finally, include the component's urls in your project's `urls.py` file:
```python
# In a file called [project root]/urls.py
from django.urls import include, path
urlpatterns = [
path("components/", include("components.urls")),
]
```
Note: slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
## Autodiscovery
By default, the Python files in the `components` app are auto-imported in order to auto-register the components (e.g. `components/button/button.py`).
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
If you are using autodiscovery, keep a few points in mind:
- Avoid defining any logic on the module-level inside the `components` dir, that you would not want to run anyway.
- Components inside the auto-imported files still need to be registered with `@component.register()`
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
Autodiscovery can be disabled via in the [settings](#disable-autodiscovery).
## Using slots in templates
@ -348,6 +439,7 @@ _New in version 0.26_:
Components support something called 'slots'.
When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content.
This mechanism makes components more reusable and composable.
This behavior is similar to [slots in Vue](https://vuejs.org/guide/components/slots.html).
In the example below we introduce two block tags that work hand in hand to make this work. These are...
@ -375,7 +467,7 @@ When using the component, you specify which slots you want to fill and where you
{% endcomponent %}
```
Since the header block is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered:
Since the 'header' fill is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered:
```htmldjango
<div class="calendar-component">
@ -388,6 +480,10 @@ Since the header block is unspecified, it's taken from the base template. If you
</div>
```
### Default slot
_Added in version 0.28_
As you can see, component slots lets you write reusable containers that you fill in when you use a component. This makes for highly reusable components that can be used in different circumstances.
It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component` tag pair without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
@ -460,86 +556,88 @@ This is fine too:
{% endcomponent %}
```
### Components as views
### Render fill in multiple places
_New in version 0.34_
_Added in version 0.70_
Components can now be used as views. To do this, `Component` subclasses Django's `View` class. This means that you can use all of the [methods](https://docs.djangoproject.com/en/5.0/ref/class-based-views/base/#view) of `View` in your component. For example, you can override `get` and `post` to handle GET and POST requests, respectively.
You can render the same content in multiple places by defining multiple slots with
identical names:
In addition, `Component` now has a `render_to_response` method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
Here's an example of a calendar component defined as a view:
```python
# In a file called [project root]/components/calendar.py
from django_components import component
@component.register("calendar")
class Calendar(component.Component):
template = """
<div class="calendar-component">
<div class="header">
{% slot "header" %}{% endslot %}
</div>
<div class="body">
Today's date is <span>{{ date }}</span>
</div>
</div>
"""
def get(self, request, *args, **kwargs):
context = {
"date": request.GET.get("date", "2020-06-06"),
}
slots = {
"header": "Calendar header",
}
return self.render_to_response(context, slots)
```htmldjango
<div class="calendar-component">
<div class="header">
{% slot "image" %}Image here{% endslot %}
</div>
<div class="body">
{% slot "image" %}Image here{% endslot %}
</div>
</div>
```
Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
So if used like:
```python
# In a file called [project root]/components/urls.py
from django.urls import path
from components.calendar.calendar import Calendar
urlpatterns = [
path("calendar/", Calendar.as_view()),
]
```htmldjango
{% component "calendar" date="2020-06-06" %}
{% fill "image" %}
<img src="..." />
{% endfill %}
{% endcomponent %}
```
Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
This renders:
Finally, include the component's urls in your project's `urls.py` file:
```python
# In a file called [project root]/urls.py
from django.urls import include, path
urlpatterns = [
path("components/", include("components.urls")),
]
```htmldjango
<div class="calendar-component">
<div class="header">
<img src="..." />
</div>
<div class="body">
<img src="..." />
</div>
</div>
```
Note: slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
#### Default and required slots
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
If you use a slot multiple times, you can still mark the slot as `default` or `required`.
For that, you must mark ONLY ONE of the identical slots.
### Advanced
We recommend to mark the first occurence for consistency, e.g.:
#### Re-using content defined in the original slot
```htmldjango
<div class="calendar-component">
<div class="header">
{% slot "image" default required %}Image here{% endslot %}
</div>
<div class="body">
{% slot "image" %}Image here{% endslot %}
</div>
</div>
```
Which you can then use are regular default slot:
```htmldjango
{% component "calendar" date="2020-06-06" %}
<img src="..." />
{% endcomponent %}
```
### Accessing original content of slots
_Added in version 0.26_
Certain properties of a slot can be accessed from within a 'fill' context. They are provided as attributes on a user-defined alias of the targeted slot. For instance, let's say you're filling a slot called 'body'. To access properties of this slot, alias it using the 'as' keyword to a new name -- or keep the original name. With the new slot alias, you can call `<alias>.default` to insert the default content.
Notice the use of `as "body"` below:
```htmldjango
{% component "calendar" date="2020-06-06" %}
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
{% endcomponent %}
```
Produces:
This produces:
```htmldjango
<div class="calendar-component">
@ -552,7 +650,7 @@ Produces:
</div>
```
#### Conditional slots
### Conditional slots
_Added in version 0.26._
@ -638,7 +736,7 @@ To negate the meaning of `component_vars.is_filled`, simply treat it as boolean
{% endif %}
```
**Accessing slot names with special characters**
#### Accessing `is_filled` of slot names with special characters
To be able to access a slot name via `component_vars.is_filled`, the slot name needs to be composed of only alphanumeric characters and underscores (e.g. `this__isvalid_123`).
@ -646,27 +744,103 @@ However, you can still define slots with other special characters. In such case,
So a slot named `"my super-slot :)"` will be available as `component_vars.is_filled.my_super_slot___`.
### Setting Up `ComponentDependencyMiddleware`
### Scoped slots
`ComponentDependencyMiddleware` is a Django middleware designed to manage and inject CSS/JS dependencies for rendered components dynamically. It ensures that only the necessary stylesheets and scripts are loaded in your HTML responses, based on the components used in your Django templates.
_Added in version 0.76_:
To set it up, add the middleware to your `MIDDLEWARE` in settings.py:
Consider a component with slot(s). This component may do some processing on the inputs, and then use the processed variable in the slot's default template:
```python
MIDDLEWARE = [
# ... other middleware classes ...
'django_components.middleware.ComponentDependencyMiddleware'
# ... other middleware classes ...
]
```py
@component.register("my_comp")
class MyComp(component.Component):
template = """
<div>
{% slot "content" default %}
input: {{ input }}
{% endslot %}
</div>
"""
def get_context_data(self, input):
processed_input = do_something(input)
return {"input": processed_input}
```
Then, enable `RENDER_DEPENDENCIES` in setting.py:
You may want to design a component so that users of your component can still access the `input` variable, so they don't have to recompute it.
```python
COMPONENTS = {
"RENDER_DEPENDENCIES": True,
# ... other component settings ...
}
This behavior is called "scoped slots". This is inspired by [Vue scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots) and [scoped slots of django-web-components](https://github.com/Xzya/django-web-components/tree/master?tab=readme-ov-file#scoped-slots).
Using scoped slots consists of two steps:
1. Passing data to `slot` tag
2. Accessing data in `fill` tag
#### Passing data to slots
To pass the data to the `slot` tag, simply pass them as keyword attributes (`key=value`):
```py
@component.register("my_comp")
class MyComp(component.Component):
template = """
<div>
{% slot "content" default input=input %}
input: {{ input }}
{% endslot %}
</div>
"""
def get_context_data(self, input):
processed_input = do_something(input)
return {
"input": processed_input,
}
```
#### Accessing slot data in fill
Next, we head over to where we define a fill for this slot. Here, to access the slot data
we set the `data` attribute to the name of the variable through which we want to access
the slot data. In the example below, we set it to `data`:
```django
{% component "my_comp" %}
{% fill "content" data="data" %}
{{ data.input }}
{% endfill %}
{% endcomponent %}
```
To access slot data on a default slot, you have to explictly define the `{% fill %}` tags.
So this works:
```django
{% component "my_comp" %}
{% fill "content" data="data" %}
{{ data.input }}
{% endfill %}
{% endcomponent %}
```
While this does not:
```django
{% component "my_comp" data="data" %}
{{ data.input }}
{% endcomponent %}
```
Note: You cannot set the `data` attribute and
[slot alias (`as var` syntax)](#accessing-original-content-of-slots)
to the same name. This raises an error:
```django
{% component "my_comp" %}
{% fill "content" data="slot_var" as "slot_var" %}
{{ slot_var.input }}
{% endfill %}
{% endcomponent %}
```
## Passing data to components
@ -777,7 +951,7 @@ Sweet! Now all the relevant HTML is inside the template, and we can move it to a
```
> Note: It is NOT possible to define nested dictionaries, so
`attrs:my_key:two=2` would be interpreted as:
> `attrs:my_key:two=2` would be interpreted as:
>
> ```py
> {"attrs": {"my_key:two": 2}}
@ -907,6 +1081,7 @@ We can achieve this by adding extra kwargs. These values
will be appended, instead of overwriting the previous value.
So if we have a variable `attrs`:
```py
attrs = {
"class": "my-class pa-4",
@ -938,19 +1113,20 @@ To simplify merging of variables, you can supply the same key multiple times, an
Renders:
```html
<div data-value="my-class pa-4 some-class another-class class-from-var text-red">
</div>
<div
data-value="my-class pa-4 some-class another-class class-from-var text-red"
></div>
```
### Rules for `html_attrs`
1. Both `attrs` and `defaults` can be passed as positional args
`{% html_attrs attrs defaults key=val %}`
or as kwargs
`{% html_attrs key=val defaults=defaults attrs=attrs %}`
`{% html_attrs attrs defaults key=val %}`
or as kwargs
`{% html_attrs key=val defaults=defaults attrs=attrs %}`
2. Both `attrs` and `defaults` are optional (can be omitted)
@ -979,64 +1155,64 @@ defaults = {
Then:
- Empty tag <br/>
`{% html_attr %}`
`{% html_attr %}`
renders (empty string): <br/>
` `
renders (empty string): <br/>
` `
- Only kwargs <br/>
`{% html_attr class="some-class" class=class_from_var data-id="123" %}`
`{% html_attr class="some-class" class=class_from_var data-id="123" %}`
renders: <br/>
`class="some-class from-var" data-id="123"`
renders: <br/>
`class="some-class from-var" data-id="123"`
- Only attrs <br/>
`{% html_attr attrs %}`
`{% html_attr attrs %}`
renders: <br/>
`class="from-attrs" type="submit"`
renders: <br/>
`class="from-attrs" type="submit"`
- Attrs as kwarg <br/>
`{% html_attr attrs=attrs %}`
`{% html_attr attrs=attrs %}`
renders: <br/>
`class="from-attrs" type="submit"`
renders: <br/>
`class="from-attrs" type="submit"`
- Only defaults (as kwarg) <br/>
`{% html_attr defaults=defaults %}`
`{% html_attr defaults=defaults %}`
renders: <br/>
`class="from-defaults" role="button"`
renders: <br/>
`class="from-defaults" role="button"`
- Attrs using the `prefix:key=value` construct <br/>
`{% html_attr attrs:class="from-attrs" attrs:type="submit" %}`
`{% html_attr attrs:class="from-attrs" attrs:type="submit" %}`
renders: <br/>
`class="from-attrs" type="submit"`
renders: <br/>
`class="from-attrs" type="submit"`
- Defaults using the `prefix:key=value` construct <br/>
`{% html_attr defaults:class="from-defaults" %}`
`{% html_attr defaults:class="from-defaults" %}`
renders: <br/>
`class="from-defaults" role="button"`
renders: <br/>
`class="from-defaults" role="button"`
- All together (1) - attrs and defaults as positional args: <br/>
`{% html_attrs attrs defaults class="added_class" class=class_from_var data-id=123 %}`
`{% html_attrs attrs defaults class="added_class" class=class_from_var data-id=123 %}`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
- All together (2) - attrs and defaults as kwargs args: <br/>
`{% html_attrs class="added_class" class=class_from_var data-id=123 attrs=attrs defaults=defaults %}`
`{% html_attrs class="added_class" class=class_from_var data-id=123 attrs=attrs defaults=defaults %}`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
- All together (3) - mixed: <br/>
`{% html_attrs attrs defaults:class="default-class" class="added_class" class=class_from_var data-id=123 %}`
`{% html_attrs attrs defaults:class="default-class" class="added_class" class=class_from_var data-id=123 %}`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" data-id=123`
renders: <br/>
`class="from-attrs added_class from-var" type="submit" data-id=123`
### Full example for `html_attrs`
@ -1132,14 +1308,16 @@ attrs = {
will be merged.
So in the end `MyComp` will render:
```html
<div
class="pa-0 border-solid my-comp-date extra-class"
data-id="123"
data-json='{"value": 456}'
@click="(e) => onClick(e, 'from_parent')"
class="pa-0 border-solid my-comp-date extra-class"
data-id="123"
data-json='{"value": 456}'
@click="(e) => onClick(e, 'from_parent')"
>
...
...
</div>
```
### Rendering HTML attributes outside of templates
@ -1175,6 +1353,40 @@ NOTE: `{% csrf_token %}` tags need access to the top-level context, and they wil
Components can also access the outer context in their context methods by accessing the property `outer_context`.
## Rendering JS and CSS dependencies
The JS and CSS files included in components are not automatically rendered.
Instead, use the following tags to specify where to render the dependencies:
- `component_dependencies` - Renders both JS and CSS
- `component_js_dependencies` - Renders only JS
- `component_css_dependencies` - Reneders only CSS
JS files are rendered as `<script>` tags.<br/>
CSS files are rendered as `<style>` tags.
### Setting Up `ComponentDependencyMiddleware`
`ComponentDependencyMiddleware` is a Django middleware designed to manage and inject CSS/JS dependencies for rendered components dynamically. It ensures that only the necessary stylesheets and scripts are loaded in your HTML responses, based on the components used in your Django templates.
To set it up, add the middleware to your `MIDDLEWARE` in settings.py:
```python
MIDDLEWARE = [
# ... other middleware classes ...
'django_components.middleware.ComponentDependencyMiddleware'
# ... other middleware classes ...
]
```
Then, enable `RENDER_DEPENDENCIES` in setting.py:
```python
COMPONENTS = {
"RENDER_DEPENDENCIES": True,
# ... other component settings ...
}
```
## Available settings
All library settings are handled from a global `COMPONENTS` variable that is read from `settings.py`. By default you don't need it set, there are resonable defaults.

View file

@ -31,6 +31,7 @@ from django_components.context import (
make_isolated_context_copy,
prepare_context,
)
from django_components.expression import safe_resolve_dict, safe_resolve_list
from django_components.logger import logger, trace_msg
from django_components.middleware import is_dependency_middleware_active
from django_components.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotName, resolve_slots
@ -358,6 +359,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
slot_name: FillContent(
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
alias=None,
scope=None,
)
for (slot_name, content) in slots_data.items()
}
@ -405,7 +407,9 @@ class ComponentNode(Node):
is_default_slot = len(self.fill_nodes) == 1 and self.fill_nodes[0].is_implicit
if is_default_slot:
fill_content: Dict[str, FillContent] = {DEFAULT_SLOT_KEY: FillContent(self.fill_nodes[0].nodelist, None)}
fill_content: Dict[str, FillContent] = {
DEFAULT_SLOT_KEY: FillContent(self.fill_nodes[0].nodelist, None, None),
}
else:
fill_content = {}
for fill_node in self.fill_nodes:
@ -419,7 +423,12 @@ class ComponentNode(Node):
)
resolved_fill_alias = fill_node.resolve_alias(context, resolved_component_name)
fill_content[resolved_name] = FillContent(fill_node.nodelist, resolved_fill_alias)
resolved_scope_var = fill_node.resolve_scope(context, resolved_component_name)
fill_content[resolved_name] = FillContent(
nodes=fill_node.nodelist,
alias=resolved_fill_alias,
scope=resolved_scope_var,
)
component: Component = component_cls(
registered_name=resolved_component_name,
@ -436,20 +445,3 @@ class ComponentNode(Node):
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
return output
def safe_resolve_list(args: List[FilterExpression], context: Context) -> List:
return [safe_resolve(arg, context) for arg in args]
def safe_resolve_dict(
kwargs: Union[Mapping[str, FilterExpression], Dict[str, FilterExpression]],
context: Context,
) -> Dict:
return {key: safe_resolve(kwarg, context) for key, kwarg in kwargs.items()}
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item

View file

@ -0,0 +1,46 @@
from typing import Any, Dict, List, Mapping, Optional, Union
from django.template import Context
from django.template.base import FilterExpression, Parser
def resolve_expression_as_identifier(
context: Context,
fexp: FilterExpression,
) -> str:
resolved = fexp.resolve(context)
if not isinstance(resolved, str):
raise ValueError(
f"FilterExpression '{fexp}' was expected to resolve to string, instead got '{type(resolved)}'"
)
if not resolved.isidentifier():
raise ValueError(
f"FilterExpression '{fexp}' was expected to resolve to valid identifier, instead got '{resolved}'"
)
return resolved
def safe_resolve_list(args: List[FilterExpression], context: Context) -> List:
return [safe_resolve(arg, context) for arg in args]
def safe_resolve_dict(
kwargs: Union[Mapping[str, FilterExpression], Dict[str, FilterExpression]],
context: Context,
) -> Dict:
return {key: safe_resolve(kwarg, context) for key, kwarg in kwargs.items()}
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
def resolve_string(
s: str,
parser: Optional[Parser] = None,
context: Optional[Mapping[str, Any]] = None,
) -> str:
parser = parser or Parser([])
context = context or {}
return parser.compile_filter(s).resolve(context)

View file

@ -12,8 +12,10 @@ from django.utils.safestring import SafeString, mark_safe
from django_components.app_settings import ContextBehavior, app_settings
from django_components.context import _FILLED_SLOTS_CONTENT_CONTEXT_KEY, _ROOT_CTX_CONTEXT_KEY
from django_components.expression import resolve_expression_as_identifier, safe_resolve_dict
from django_components.logger import trace_msg
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
from django_components.template_parser import process_aggregate_kwargs
from django_components.utils import gen_id
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
@ -23,6 +25,7 @@ DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
SlotId = str
SlotName = str
AliasName = str
ScopeName = str
class FillContent(NamedTuple):
@ -42,6 +45,7 @@ class FillContent(NamedTuple):
nodes: NodeList
alias: Optional[AliasName]
scope: Optional[ScopeName]
class Slot(NamedTuple):
@ -77,6 +81,7 @@ class SlotFill(NamedTuple):
nodelist: NodeList
context_data: Dict
alias: Optional[AliasName]
scope: Optional[ScopeName]
class UserSlotVar:
@ -107,12 +112,14 @@ class SlotNode(Node):
is_required: bool = False,
is_default: bool = False,
node_id: Optional[str] = None,
slot_kwargs: Optional[Dict[str, FilterExpression]] = None,
):
self.name = name
self.nodelist = nodelist
self.is_required = is_required
self.is_default = is_default
self.node_id = node_id or gen_id()
self.slot_kwargs = slot_kwargs or {}
@property
def active_flags(self) -> List[str]:
@ -132,14 +139,23 @@ class SlotNode(Node):
# NOTE: Slot entry MUST be present. If it's missing, there was an issue upstream.
slot_fill = slots[self.node_id]
extra_context: Dict[str, Any] = {}
# If slot is using alias `{% slot "myslot" as "abc" %}`, then set the "abc" to
# the context, so users can refer to the slot from within the slot.
extra_context = {}
if slot_fill.alias:
if not slot_fill.alias.isidentifier():
raise TemplateSyntaxError(f"Invalid fill alias. Must be a valid identifier. Got '{slot_fill.alias}'")
extra_context[slot_fill.alias] = UserSlotVar(self, context)
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
# are made available through a variable name that was set on the `{% fill %}`
# tag.
if slot_fill.scope:
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
extra_context[slot_fill.scope] = slot_kwargs
# For the user-provided slot fill, we want to use the context of where the slot
# came from (or current context if configured so)
used_ctx = self._resolve_slot_context(context, slot_fill)
@ -177,6 +193,7 @@ class FillNode(Node):
nodelist: NodeList,
name_fexp: FilterExpression,
alias_fexp: Optional[FilterExpression] = None,
scope_fexp: Optional[FilterExpression] = None,
is_implicit: bool = False,
node_id: Optional[str] = None,
):
@ -185,6 +202,7 @@ class FillNode(Node):
self.name_fexp = name_fexp
self.alias_fexp = alias_fexp
self.is_implicit = is_implicit
self.scope_fexp = scope_fexp
self.component_id: Optional[str] = None
def render(self, context: Context) -> str:
@ -198,16 +216,29 @@ class FillNode(Node):
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
def resolve_alias(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
if not self.alias_fexp:
return self.resolve_fexp("alias", self.alias_fexp, context, component_name)
def resolve_scope(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
return self.resolve_fexp("scope", self.scope_fexp, context, component_name)
def resolve_fexp(
self,
name: str,
fexp: Optional[FilterExpression],
context: Context,
component_name: Optional[str] = None,
) -> Optional[str]:
if not fexp:
return None
resolved_alias: Optional[str] = self.alias_fexp.resolve(context)
if resolved_alias and not resolved_alias.isidentifier():
try:
resolved_alias = resolve_expression_as_identifier(context, fexp)
except ValueError as err:
raise TemplateSyntaxError(
f"Fill tag alias '{self.alias_fexp.var}' in component "
f"{component_name} does not resolve to "
f"a valid Python identifier. Got: '{resolved_alias}'."
)
f"Fill tag {name} '{fexp.var}' in component {component_name}"
f"does not resolve to a valid Python identifier."
) from err
return resolved_alias
@ -333,6 +364,7 @@ def resolve_slots(
nodelist=fill.nodes,
context_data=context_data,
alias=fill.alias,
scope=fill.scope,
)
for name, fill in fill_content.items()
}
@ -421,6 +453,7 @@ def resolve_slots(
nodelist=slot.nodelist,
context_data=context_data,
alias=None,
scope=None,
)
# Since the slot's default CAN include other slots (because it's defined in
# the same template), we need to enqueue the slot's children
@ -468,6 +501,7 @@ def _resolve_default_slot(
nodelist=default_fill.nodelist,
context_data=default_fill.context_data,
alias=default_fill.alias,
scope=default_fill.scope,
# Updated fields
name=slot.name,
escaped_name=_escape_slot_name(slot.name),

View file

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Tuple
import django.template
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode, Token, TokenType
from django.template.base import FilterExpression, NodeList, Parser, Token
from django.template.exceptions import TemplateSyntaxError
from django.utils.safestring import SafeString, mark_safe
@ -10,6 +10,7 @@ from django_components.attributes import HtmlAttrsNode
from django_components.component import RENDERED_COMMENT_TEMPLATE, ComponentNode
from django_components.component_registry import ComponentRegistry
from django_components.component_registry import registry as component_registry
from django_components.expression import resolve_string
from django_components.logger import trace_msg
from django_components.middleware import (
CSS_DEPENDENCY_PLACEHOLDER,
@ -29,6 +30,7 @@ register = django.template.Library()
SLOT_REQUIRED_OPTION_KEYWORD = "required"
SLOT_DEFAULT_OPTION_KEYWORD = "default"
SLOT_DATA_ATTR = "data"
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
@ -110,36 +112,9 @@ def component_js_dependencies_tag(preload: str = "") -> SafeString:
@register.tag("slot")
def do_slot(parser: Parser, token: Token) -> SlotNode:
bits = token.split_contents()
args = bits[1:]
# e.g. {% slot <name> %}
is_required = False
is_default = False
if 1 <= len(args) <= 3:
slot_name, *options = args
if not is_wrapped_in_quotes(slot_name):
raise TemplateSyntaxError(f"'{bits[0]}' name must be a string 'literal'.")
slot_name = strip_quotes(slot_name)
modifiers_count = len(options)
if SLOT_REQUIRED_OPTION_KEYWORD in options:
is_required = True
modifiers_count -= 1
if SLOT_DEFAULT_OPTION_KEYWORD in options:
is_default = True
modifiers_count -= 1
if modifiers_count != 0:
keywords = [
SLOT_REQUIRED_OPTION_KEYWORD,
SLOT_DEFAULT_OPTION_KEYWORD,
]
raise TemplateSyntaxError(f"Invalid options passed to 'slot' tag. Valid choices: {keywords}.")
else:
raise TemplateSyntaxError(
"'slot' tag does not match pattern "
"{% slot <name> ['default'] ['required'] %}. "
"Order of options is free."
)
# e.g. {% slot <name> ... %}
tag_name, *args = token.split_contents()
slot_name, is_default, is_required, slot_kwargs = _parse_slot_args(parser, args, tag_name)
# Use a unique ID to be able to tie the fill nodes with components and slots
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
slot_id = gen_id()
@ -153,6 +128,7 @@ def do_slot(parser: Parser, token: Token) -> SlotNode:
is_required=is_required,
is_default=is_default,
node_id=slot_id,
slot_kwargs=slot_kwargs,
)
trace_msg("PARSE", "SLOT", slot_name, slot_id, "...Done!")
@ -161,45 +137,35 @@ def do_slot(parser: Parser, token: Token) -> SlotNode:
@register.tag("fill")
def do_fill(parser: Parser, token: Token) -> FillNode:
"""Block tag whose contents 'fill' (are inserted into) an identically named
"""
Block tag whose contents 'fill' (are inserted into) an identically named
'slot'-block in the component template referred to by a parent component.
It exists to make component nesting easier.
This tag is available only within a {% component %}..{% endcomponent %} block.
Runtime checks should prohibit other usages.
"""
bits = token.split_contents()
tag = bits[0]
args = bits[1:]
# e.g. {% fill <name> %}
alias_fexp: Optional[FilterExpression] = None
if len(args) == 1:
tgt_slot_name: str = args[0]
# e.g. {% fill <name> as <alias> %}
elif len(args) == 3:
tgt_slot_name, as_keyword, alias = args
if as_keyword.lower() != "as":
raise TemplateSyntaxError(f"{tag} tag args do not conform to pattern '<target slot> as <alias>'")
alias_fexp = FilterExpression(alias, parser)
else:
raise TemplateSyntaxError(f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}.")
tag_name, *args = token.split_contents()
slot_name_fexp, alias_fexp, scope_var_fexp = _parse_fill_args(parser, args, tag_name)
# Use a unique ID to be able to tie the fill nodes with components and slots
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
fill_id = gen_id()
trace_msg("PARSE", "FILL", tgt_slot_name, fill_id)
trace_msg("PARSE", "FILL", str(slot_name_fexp), fill_id)
nodelist = parser.parse(parse_until=["endfill"])
parser.delete_first_token()
fill_node = FillNode(
nodelist,
name_fexp=FilterExpression(tgt_slot_name, tag),
name_fexp=slot_name_fexp,
alias_fexp=alias_fexp,
scope_fexp=scope_var_fexp,
node_id=fill_id,
)
trace_msg("PARSE", "FILL", tgt_slot_name, fill_id, "...Done!")
trace_msg("PARSE", "FILL", str(slot_name_fexp), fill_id, "...Done!")
return fill_node
@ -219,8 +185,8 @@ def do_component(parser: Parser, token: Token) -> ComponentNode:
"""
bits = token.split_contents()
bits, isolated_context = check_for_isolated_context_keyword(bits)
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
bits, isolated_context = _check_for_isolated_context_keyword(bits)
component_name, context_args, context_kwargs = _parse_component_with_args(parser, bits, "component")
# Use a unique ID to be able to tie the fill nodes with components and slots
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
@ -278,23 +244,11 @@ def do_html_attrs(parser: Parser, token: Token) -> HtmlAttrsNode:
```
"""
bits = token.split_contents()
attributes, default_attrs, append_attrs = parse_html_attrs_args(parser, bits, "html_attrs")
attributes, default_attrs, append_attrs = _parse_html_attrs_args(parser, bits, "html_attrs")
return HtmlAttrsNode(attributes, default_attrs, append_attrs)
def is_whitespace_node(node: Node) -> bool:
return isinstance(node, TextNode) and node.s.isspace()
def is_whitespace_token(token: Token) -> bool:
return token.token_type == TokenType.TEXT and not token.contents.strip()
def is_block_tag_token(token: Token, name: str) -> bool:
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
def check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], bool]:
def _check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], bool]:
"""Return True and strip the last word if token ends with 'only' keyword or if CONTEXT_BEHAVIOR is 'isolated'."""
if bits[-1] == "only":
@ -306,7 +260,7 @@ def check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], bool
return bits, False
def parse_component_with_args(
def _parse_component_with_args(
parser: Parser, bits: List[str], tag_name: str
) -> Tuple[str, List[FilterExpression], Mapping[str, FilterExpression]]:
tag_args, tag_kwarg_pairs = parse_bits(
@ -336,7 +290,7 @@ def parse_component_with_args(
return component_name, context_args, tag_kwargs
def parse_html_attrs_args(
def _parse_html_attrs_args(
parser: Parser, bits: List[str], tag_name: str
) -> Tuple[Optional[FilterExpression], Optional[FilterExpression], List[Tuple[str, FilterExpression]]]:
tag_args, tag_kwarg_pairs = parse_bits(
@ -372,6 +326,111 @@ def parse_html_attrs_args(
return attrs, defaults, tag_kwarg_pairs
def _parse_slot_args(
parser: Parser,
bits: List[str],
tag_name: str,
) -> Tuple[str, bool, bool, Dict[str, FilterExpression]]:
if not len(bits):
raise TemplateSyntaxError(
"'slot' tag does not match pattern "
"{% slot <name> ['default'] ['required'] [key=val, ...] %}. "
"Order of options is free."
)
slot_name, *options = bits
if not is_wrapped_in_quotes(slot_name):
raise TemplateSyntaxError(f"'{tag_name}' name must be a string 'literal'.")
slot_name = resolve_string(slot_name, parser)
# Parse flags - Since `parse_bits` doesn't handle "shorthand" kwargs
# (AKA `required` for `required=True`), we have to first get the flags out
# of the way.
def extract_value(lst: List[str], value: str) -> bool:
"""Check if value exists in list, and if so, remove it from said list"""
try:
lst.remove(value)
return True
except ValueError:
return False
is_default = extract_value(options, SLOT_DEFAULT_OPTION_KEYWORD)
is_required = extract_value(options, SLOT_REQUIRED_OPTION_KEYWORD)
# Parse kwargs that will be passed to the fill
_, tag_kwarg_pairs = parse_bits(
parser=parser,
bits=options,
params=[],
name=tag_name,
)
tag_kwargs: Dict[str, FilterExpression] = {}
for key, val in tag_kwarg_pairs:
if key in tag_kwargs:
# The keyword argument has already been supplied once
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
tag_kwargs[key] = val
return slot_name, is_default, is_required, tag_kwargs
def _parse_fill_args(
parser: Parser,
bits: List[str],
tag_name: str,
) -> Tuple[FilterExpression, Optional[FilterExpression], Optional[FilterExpression]]:
if not len(bits):
raise TemplateSyntaxError(
"'fill' tag does not match pattern " f"{{% fill <name> [{SLOT_DATA_ATTR}=val] [as alias] %}}. "
)
slot_name = bits.pop(0)
slot_name_fexp = parser.compile_filter(slot_name)
alias_fexp: Optional[FilterExpression] = None
# e.g. {% fill <name> as <alias> %}
if len(bits) >= 2 and bits[-2].lower() == "as":
alias = bits.pop()
bits.pop() # Remove the "as" keyword
alias_fexp = parser.compile_filter(alias)
# Even tho we want to parse only single kwarg, we use the same logic for parsing
# as we use for other tags, for consistency.
_, tag_kwarg_pairs = parse_bits(
parser=parser,
bits=bits,
params=[],
name=tag_name,
)
tag_kwargs: Dict[str, FilterExpression] = {}
for key, val in tag_kwarg_pairs:
if key in tag_kwargs:
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
tag_kwargs[key] = val
scope_var_fexp: Optional[FilterExpression] = None
if SLOT_DATA_ATTR in tag_kwargs:
scope_var_fexp = tag_kwargs.pop(SLOT_DATA_ATTR)
if not is_wrapped_in_quotes(scope_var_fexp.token):
raise TemplateSyntaxError(
f"Value of '{SLOT_DATA_ATTR}' in '{tag_name}' tag must be a string literal, got '{scope_var_fexp}'"
)
if scope_var_fexp and alias_fexp and scope_var_fexp.token == alias_fexp.token:
raise TemplateSyntaxError(
f"'{tag_name}' received the same string for slot alias (as ...) and slot data ({SLOT_DATA_ATTR}=...)"
)
if len(tag_kwargs):
extra_keywords = tag_kwargs.keys()
extra_keys = ", ".join(extra_keywords)
raise TemplateSyntaxError(f"'{tag_name}' received unexpected kwargs: {extra_keys}")
return slot_name_fexp, alias_fexp, scope_var_fexp
def _get_positional_param(
tag_name: str,
param_name: str,
@ -393,7 +452,3 @@ def _get_positional_param(
def is_wrapped_in_quotes(s: str) -> bool:
return s.startswith(('"', "'")) and s[0] == s[-1]
def strip_quotes(s: str) -> str:
return s.strip("\"'")

View file

@ -9,13 +9,13 @@ from .testutils import BaseTestCase
from django_components import component, types
from django_components.component import safe_resolve_dict, safe_resolve_list
from django_components.templatetags.component_tags import parse_component_with_args
from django_components.templatetags.component_tags import _parse_component_with_args
class ParserTest(BaseTestCase):
def test_parses_args_kwargs(self):
bits = ["component", "my_component", "42", "myvar", "key='val'", "key2=val2"]
name, raw_args, raw_kwargs = parse_component_with_args(Parser(""), bits, "component")
name, raw_args, raw_kwargs = _parse_component_with_args(Parser(""), bits, "component")
ctx = {"myvar": {"a": "b"}, "val2": 1}
args = safe_resolve_list(raw_args, ctx)
@ -35,7 +35,7 @@ class ParserTest(BaseTestCase):
"@event:na-me.mod=bzz",
"#my-id=True",
]
name, raw_args, raw_kwargs = parse_component_with_args(Parser(""), bits, "component")
name, raw_args, raw_kwargs = _parse_component_with_args(Parser(""), bits, "component")
ctx = Context({"date": 2024, "bzz": "fzz"})
args = safe_resolve_list(raw_args, ctx)

View file

@ -2392,3 +2392,214 @@ class IterationFillTest(BaseTestCase):
{'inner': ['ITER2_OBJ1', 'ITER2_OBJ2']} default
""",
)
class ScopedSlotTest(BaseTestCase):
def test_slot_data(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc def=var123 %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" %}
{{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.def }}
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<div>
def
456
</div>
"""
self.assertHTMLEqual(rendered, expected)
def test_slot_data_with_flags(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" default abc=abc 123=var123 required %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" %}
{{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.123 }}
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<div>
def
456
</div>
"""
self.assertHTMLEqual(rendered, expected)
def test_slot_data_fill_with_as(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" data="slot_data_in_fill" as "slot_var" %}
{{ slot_var.default }}
{{ slot_data_in_fill.abc }}
{{ slot_data_in_fill.123 }}
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<div>
Default text
def
456
</div>
"""
self.assertHTMLEqual(rendered, expected)
def test_slot_data_raises_on_slot_data_and_as_same_var(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" data="slot_var" as "slot_var" %}
{{ slot_var.default }}
{% endfill %}
{% endcomponent %}
"""
with self.assertRaisesMessage(
TemplateSyntaxError,
"'fill' received the same string for slot alias (as ...) and slot data (data=...)",
):
Template(template).render(Context())
def test_slot_data_fill_without_data(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" %}
overriden
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> overriden </div>"
self.assertHTMLEqual(rendered, expected)
def test_slot_data_fill_without_slot_data(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" %}Default text{% endslot %}
</div>
"""
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% fill "my_slot" data="data" %}
{{ data|safe }}
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> {} </div>"
self.assertHTMLEqual(rendered, expected)
def test_slot_data_no_fill(self):
@component.register("test")
class TestComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
</div>
"""
def get_context_data(self):
return {
"abc": "def",
"var123": 456,
}
template: types.django_html = """
{% load component_tags %}
{% component "test" %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = "<div> Default text </div>"
self.assertHTMLEqual(rendered, expected)