mirror of
https://github.com/django-components/django-components.git
synced 2025-10-09 13:40:18 +00:00
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:
parent
bdeb9c4e32
commit
b1b66fd751
7 changed files with 804 additions and 254 deletions
518
README.md
518
README.md
|
@ -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!
|
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
|
## Release notes
|
||||||
|
|
||||||
**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components.
|
**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.
|
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.
|
**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.
|
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.
|
template: t.django_html = """
|
||||||
- Components inside the auto-imported files still need to be registered with `@component.register()`
|
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
|
||||||
- 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).
|
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
|
## 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.
|
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
|
```python
|
||||||
# In a file called [project root]/components/calendar.py
|
# In a file called [project root]/components/calendar.py
|
||||||
from django_components import component
|
from django_components import component
|
||||||
from django_components import types as t
|
|
||||||
|
|
||||||
@component.register("calendar")
|
@component.register("calendar")
|
||||||
class Calendar(component.Component):
|
class Calendar(component.Component):
|
||||||
def get_context_data(self, date):
|
|
||||||
return {
|
template = """
|
||||||
"date": date,
|
<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 = {
|
||||||
template: t.django_html = """
|
"header": "Calendar header",
|
||||||
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
|
}
|
||||||
"""
|
return self.render_to_response(context, slots)
|
||||||
|
|
||||||
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.
|
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
|
## Using slots in templates
|
||||||
|
|
||||||
|
@ -348,6 +439,7 @@ _New in version 0.26_:
|
||||||
Components support something called 'slots'.
|
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.
|
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 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...
|
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 %}
|
{% 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
|
```htmldjango
|
||||||
<div class="calendar-component">
|
<div class="calendar-component">
|
||||||
|
@ -388,6 +480,10 @@ Since the header block is unspecified, it's taken from the base template. If you
|
||||||
</div>
|
</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.
|
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`.
|
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 %}
|
{% 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.
|
```htmldjango
|
||||||
|
<div class="calendar-component">
|
||||||
Here's an example of a calendar component defined as a view:
|
<div class="header">
|
||||||
|
{% slot "image" %}Image here{% endslot %}
|
||||||
```python
|
</div>
|
||||||
# In a file called [project root]/components/calendar.py
|
<div class="body">
|
||||||
from django_components import component
|
{% slot "image" %}Image here{% endslot %}
|
||||||
|
</div>
|
||||||
@component.register("calendar")
|
</div>
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```htmldjango
|
||||||
# In a file called [project root]/components/urls.py
|
{% component "calendar" date="2020-06-06" %}
|
||||||
from django.urls import path
|
{% fill "image" %}
|
||||||
from components.calendar.calendar import Calendar
|
<img src="..." />
|
||||||
|
{% endfill %}
|
||||||
urlpatterns = [
|
{% endcomponent %}
|
||||||
path("calendar/", Calendar.as_view()),
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
```htmldjango
|
||||||
|
<div class="calendar-component">
|
||||||
```python
|
<div class="header">
|
||||||
# In a file called [project root]/urls.py
|
<img src="..." />
|
||||||
from django.urls import include, path
|
</div>
|
||||||
|
<div class="body">
|
||||||
urlpatterns = [
|
<img src="..." />
|
||||||
path("components/", include("components.urls")),
|
</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.
|
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
|
```htmldjango
|
||||||
{% component "calendar" date="2020-06-06" %}
|
{% component "calendar" date="2020-06-06" %}
|
||||||
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
|
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces:
|
This produces:
|
||||||
|
|
||||||
```htmldjango
|
```htmldjango
|
||||||
<div class="calendar-component">
|
<div class="calendar-component">
|
||||||
|
@ -552,7 +650,7 @@ Produces:
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Conditional slots
|
### Conditional slots
|
||||||
|
|
||||||
_Added in version 0.26._
|
_Added in version 0.26._
|
||||||
|
|
||||||
|
@ -638,7 +736,7 @@ To negate the meaning of `component_vars.is_filled`, simply treat it as boolean
|
||||||
{% endif %}
|
{% 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`).
|
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___`.
|
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
|
```py
|
||||||
MIDDLEWARE = [
|
@component.register("my_comp")
|
||||||
# ... other middleware classes ...
|
class MyComp(component.Component):
|
||||||
'django_components.middleware.ComponentDependencyMiddleware'
|
template = """
|
||||||
# ... other middleware classes ...
|
<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
|
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).
|
||||||
COMPONENTS = {
|
|
||||||
"RENDER_DEPENDENCIES": True,
|
Using scoped slots consists of two steps:
|
||||||
# ... other component settings ...
|
|
||||||
}
|
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
|
## 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
|
> 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
|
> ```py
|
||||||
> {"attrs": {"my_key:two": 2}}
|
> {"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.
|
will be appended, instead of overwriting the previous value.
|
||||||
|
|
||||||
So if we have a variable `attrs`:
|
So if we have a variable `attrs`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
attrs = {
|
attrs = {
|
||||||
"class": "my-class pa-4",
|
"class": "my-class pa-4",
|
||||||
|
@ -938,19 +1113,20 @@ To simplify merging of variables, you can supply the same key multiple times, an
|
||||||
Renders:
|
Renders:
|
||||||
|
|
||||||
```html
|
```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`
|
### Rules for `html_attrs`
|
||||||
|
|
||||||
1. Both `attrs` and `defaults` can be passed as positional args
|
1. Both `attrs` and `defaults` can be passed as positional args
|
||||||
|
|
||||||
`{% html_attrs attrs defaults key=val %}`
|
`{% html_attrs attrs defaults key=val %}`
|
||||||
|
|
||||||
or as kwargs
|
or as kwargs
|
||||||
|
|
||||||
`{% html_attrs key=val defaults=defaults attrs=attrs %}`
|
`{% html_attrs key=val defaults=defaults attrs=attrs %}`
|
||||||
|
|
||||||
2. Both `attrs` and `defaults` are optional (can be omitted)
|
2. Both `attrs` and `defaults` are optional (can be omitted)
|
||||||
|
|
||||||
|
@ -979,64 +1155,64 @@ defaults = {
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
- Empty tag <br/>
|
- Empty tag <br/>
|
||||||
`{% html_attr %}`
|
`{% html_attr %}`
|
||||||
|
|
||||||
renders (empty string): <br/>
|
renders (empty string): <br/>
|
||||||
` `
|
` `
|
||||||
|
|
||||||
- Only kwargs <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/>
|
renders: <br/>
|
||||||
`class="some-class from-var" data-id="123"`
|
`class="some-class from-var" data-id="123"`
|
||||||
|
|
||||||
- Only attrs <br/>
|
- Only attrs <br/>
|
||||||
`{% html_attr attrs %}`
|
`{% html_attr attrs %}`
|
||||||
|
|
||||||
renders: <br/>
|
renders: <br/>
|
||||||
`class="from-attrs" type="submit"`
|
`class="from-attrs" type="submit"`
|
||||||
|
|
||||||
- Attrs as kwarg <br/>
|
- Attrs as kwarg <br/>
|
||||||
`{% html_attr attrs=attrs %}`
|
`{% html_attr attrs=attrs %}`
|
||||||
|
|
||||||
renders: <br/>
|
renders: <br/>
|
||||||
`class="from-attrs" type="submit"`
|
`class="from-attrs" type="submit"`
|
||||||
|
|
||||||
- Only defaults (as kwarg) <br/>
|
- Only defaults (as kwarg) <br/>
|
||||||
`{% html_attr defaults=defaults %}`
|
`{% html_attr defaults=defaults %}`
|
||||||
|
|
||||||
renders: <br/>
|
renders: <br/>
|
||||||
`class="from-defaults" role="button"`
|
`class="from-defaults" role="button"`
|
||||||
|
|
||||||
- Attrs using the `prefix:key=value` construct <br/>
|
- 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/>
|
renders: <br/>
|
||||||
`class="from-attrs" type="submit"`
|
`class="from-attrs" type="submit"`
|
||||||
|
|
||||||
- Defaults using the `prefix:key=value` construct <br/>
|
- Defaults using the `prefix:key=value` construct <br/>
|
||||||
`{% html_attr defaults:class="from-defaults" %}`
|
`{% html_attr defaults:class="from-defaults" %}`
|
||||||
|
|
||||||
renders: <br/>
|
renders: <br/>
|
||||||
`class="from-defaults" role="button"`
|
`class="from-defaults" role="button"`
|
||||||
|
|
||||||
- All together (1) - attrs and defaults as positional args: <br/>
|
- 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/>
|
renders: <br/>
|
||||||
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
|
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
|
||||||
|
|
||||||
- All together (2) - attrs and defaults as kwargs args: <br/>
|
- 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/>
|
renders: <br/>
|
||||||
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
|
`class="from-attrs added_class from-var" type="submit" role="button" data-id=123`
|
||||||
|
|
||||||
- All together (3) - mixed: <br/>
|
- 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/>
|
renders: <br/>
|
||||||
`class="from-attrs added_class from-var" type="submit" data-id=123`
|
`class="from-attrs added_class from-var" type="submit" data-id=123`
|
||||||
|
|
||||||
### Full example for `html_attrs`
|
### Full example for `html_attrs`
|
||||||
|
|
||||||
|
@ -1132,14 +1308,16 @@ attrs = {
|
||||||
will be merged.
|
will be merged.
|
||||||
|
|
||||||
So in the end `MyComp` will render:
|
So in the end `MyComp` will render:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div
|
<div
|
||||||
class="pa-0 border-solid my-comp-date extra-class"
|
class="pa-0 border-solid my-comp-date extra-class"
|
||||||
data-id="123"
|
data-id="123"
|
||||||
data-json='{"value": 456}'
|
data-json='{"value": 456}'
|
||||||
@click="(e) => onClick(e, 'from_parent')"
|
@click="(e) => onClick(e, 'from_parent')"
|
||||||
>
|
>
|
||||||
...
|
...
|
||||||
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rendering HTML attributes outside of templates
|
### 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`.
|
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
|
## 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.
|
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.
|
||||||
|
|
|
@ -31,6 +31,7 @@ from django_components.context import (
|
||||||
make_isolated_context_copy,
|
make_isolated_context_copy,
|
||||||
prepare_context,
|
prepare_context,
|
||||||
)
|
)
|
||||||
|
from django_components.expression import safe_resolve_dict, safe_resolve_list
|
||||||
from django_components.logger import logger, trace_msg
|
from django_components.logger import logger, trace_msg
|
||||||
from django_components.middleware import is_dependency_middleware_active
|
from django_components.middleware import is_dependency_middleware_active
|
||||||
from django_components.slots import DEFAULT_SLOT_KEY, FillContent, FillNode, SlotName, resolve_slots
|
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(
|
slot_name: FillContent(
|
||||||
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
|
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
|
||||||
alias=None,
|
alias=None,
|
||||||
|
scope=None,
|
||||||
)
|
)
|
||||||
for (slot_name, content) in slots_data.items()
|
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
|
is_default_slot = len(self.fill_nodes) == 1 and self.fill_nodes[0].is_implicit
|
||||||
if is_default_slot:
|
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:
|
else:
|
||||||
fill_content = {}
|
fill_content = {}
|
||||||
for fill_node in self.fill_nodes:
|
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)
|
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(
|
component: Component = component_cls(
|
||||||
registered_name=resolved_component_name,
|
registered_name=resolved_component_name,
|
||||||
|
@ -436,20 +445,3 @@ class ComponentNode(Node):
|
||||||
|
|
||||||
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
|
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
|
||||||
return output
|
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
|
|
||||||
|
|
46
src/django_components/expression.py
Normal file
46
src/django_components/expression.py
Normal 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)
|
|
@ -12,8 +12,10 @@ from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
from django_components.app_settings import ContextBehavior, app_settings
|
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.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.logger import trace_msg
|
||||||
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
|
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
|
from django_components.utils import gen_id
|
||||||
|
|
||||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||||
|
@ -23,6 +25,7 @@ DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||||
SlotId = str
|
SlotId = str
|
||||||
SlotName = str
|
SlotName = str
|
||||||
AliasName = str
|
AliasName = str
|
||||||
|
ScopeName = str
|
||||||
|
|
||||||
|
|
||||||
class FillContent(NamedTuple):
|
class FillContent(NamedTuple):
|
||||||
|
@ -42,6 +45,7 @@ class FillContent(NamedTuple):
|
||||||
|
|
||||||
nodes: NodeList
|
nodes: NodeList
|
||||||
alias: Optional[AliasName]
|
alias: Optional[AliasName]
|
||||||
|
scope: Optional[ScopeName]
|
||||||
|
|
||||||
|
|
||||||
class Slot(NamedTuple):
|
class Slot(NamedTuple):
|
||||||
|
@ -77,6 +81,7 @@ class SlotFill(NamedTuple):
|
||||||
nodelist: NodeList
|
nodelist: NodeList
|
||||||
context_data: Dict
|
context_data: Dict
|
||||||
alias: Optional[AliasName]
|
alias: Optional[AliasName]
|
||||||
|
scope: Optional[ScopeName]
|
||||||
|
|
||||||
|
|
||||||
class UserSlotVar:
|
class UserSlotVar:
|
||||||
|
@ -107,12 +112,14 @@ class SlotNode(Node):
|
||||||
is_required: bool = False,
|
is_required: bool = False,
|
||||||
is_default: bool = False,
|
is_default: bool = False,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
|
slot_kwargs: Optional[Dict[str, FilterExpression]] = None,
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.nodelist = nodelist
|
self.nodelist = nodelist
|
||||||
self.is_required = is_required
|
self.is_required = is_required
|
||||||
self.is_default = is_default
|
self.is_default = is_default
|
||||||
self.node_id = node_id or gen_id()
|
self.node_id = node_id or gen_id()
|
||||||
|
self.slot_kwargs = slot_kwargs or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_flags(self) -> List[str]:
|
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.
|
# NOTE: Slot entry MUST be present. If it's missing, there was an issue upstream.
|
||||||
slot_fill = slots[self.node_id]
|
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
|
# 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.
|
# the context, so users can refer to the slot from within the slot.
|
||||||
extra_context = {}
|
|
||||||
if slot_fill.alias:
|
if slot_fill.alias:
|
||||||
if not slot_fill.alias.isidentifier():
|
if not slot_fill.alias.isidentifier():
|
||||||
raise TemplateSyntaxError(f"Invalid fill alias. Must be a valid identifier. Got '{slot_fill.alias}'")
|
raise TemplateSyntaxError(f"Invalid fill alias. Must be a valid identifier. Got '{slot_fill.alias}'")
|
||||||
extra_context[slot_fill.alias] = UserSlotVar(self, context)
|
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
|
# For the user-provided slot fill, we want to use the context of where the slot
|
||||||
# came from (or current context if configured so)
|
# came from (or current context if configured so)
|
||||||
used_ctx = self._resolve_slot_context(context, slot_fill)
|
used_ctx = self._resolve_slot_context(context, slot_fill)
|
||||||
|
@ -177,6 +193,7 @@ class FillNode(Node):
|
||||||
nodelist: NodeList,
|
nodelist: NodeList,
|
||||||
name_fexp: FilterExpression,
|
name_fexp: FilterExpression,
|
||||||
alias_fexp: Optional[FilterExpression] = None,
|
alias_fexp: Optional[FilterExpression] = None,
|
||||||
|
scope_fexp: Optional[FilterExpression] = None,
|
||||||
is_implicit: bool = False,
|
is_implicit: bool = False,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
|
@ -185,6 +202,7 @@ class FillNode(Node):
|
||||||
self.name_fexp = name_fexp
|
self.name_fexp = name_fexp
|
||||||
self.alias_fexp = alias_fexp
|
self.alias_fexp = alias_fexp
|
||||||
self.is_implicit = is_implicit
|
self.is_implicit = is_implicit
|
||||||
|
self.scope_fexp = scope_fexp
|
||||||
self.component_id: Optional[str] = None
|
self.component_id: Optional[str] = None
|
||||||
|
|
||||||
def render(self, context: Context) -> str:
|
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)}.>"
|
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]:
|
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
|
return None
|
||||||
|
|
||||||
resolved_alias: Optional[str] = self.alias_fexp.resolve(context)
|
try:
|
||||||
if resolved_alias and not resolved_alias.isidentifier():
|
resolved_alias = resolve_expression_as_identifier(context, fexp)
|
||||||
|
except ValueError as err:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Fill tag alias '{self.alias_fexp.var}' in component "
|
f"Fill tag {name} '{fexp.var}' in component {component_name}"
|
||||||
f"{component_name} does not resolve to "
|
f"does not resolve to a valid Python identifier."
|
||||||
f"a valid Python identifier. Got: '{resolved_alias}'."
|
) from err
|
||||||
)
|
|
||||||
return resolved_alias
|
return resolved_alias
|
||||||
|
|
||||||
|
|
||||||
|
@ -333,6 +364,7 @@ def resolve_slots(
|
||||||
nodelist=fill.nodes,
|
nodelist=fill.nodes,
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
alias=fill.alias,
|
alias=fill.alias,
|
||||||
|
scope=fill.scope,
|
||||||
)
|
)
|
||||||
for name, fill in fill_content.items()
|
for name, fill in fill_content.items()
|
||||||
}
|
}
|
||||||
|
@ -421,6 +453,7 @@ def resolve_slots(
|
||||||
nodelist=slot.nodelist,
|
nodelist=slot.nodelist,
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
alias=None,
|
alias=None,
|
||||||
|
scope=None,
|
||||||
)
|
)
|
||||||
# Since the slot's default CAN include other slots (because it's defined in
|
# 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
|
# the same template), we need to enqueue the slot's children
|
||||||
|
@ -468,6 +501,7 @@ def _resolve_default_slot(
|
||||||
nodelist=default_fill.nodelist,
|
nodelist=default_fill.nodelist,
|
||||||
context_data=default_fill.context_data,
|
context_data=default_fill.context_data,
|
||||||
alias=default_fill.alias,
|
alias=default_fill.alias,
|
||||||
|
scope=default_fill.scope,
|
||||||
# Updated fields
|
# Updated fields
|
||||||
name=slot.name,
|
name=slot.name,
|
||||||
escaped_name=_escape_slot_name(slot.name),
|
escaped_name=_escape_slot_name(slot.name),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Tuple
|
||||||
|
|
||||||
import django.template
|
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.template.exceptions import TemplateSyntaxError
|
||||||
from django.utils.safestring import SafeString, mark_safe
|
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 import RENDERED_COMMENT_TEMPLATE, ComponentNode
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.component_registry import registry as component_registry
|
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.logger import trace_msg
|
||||||
from django_components.middleware import (
|
from django_components.middleware import (
|
||||||
CSS_DEPENDENCY_PLACEHOLDER,
|
CSS_DEPENDENCY_PLACEHOLDER,
|
||||||
|
@ -29,6 +30,7 @@ register = django.template.Library()
|
||||||
|
|
||||||
SLOT_REQUIRED_OPTION_KEYWORD = "required"
|
SLOT_REQUIRED_OPTION_KEYWORD = "required"
|
||||||
SLOT_DEFAULT_OPTION_KEYWORD = "default"
|
SLOT_DEFAULT_OPTION_KEYWORD = "default"
|
||||||
|
SLOT_DATA_ATTR = "data"
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
|
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
|
||||||
|
@ -110,36 +112,9 @@ def component_js_dependencies_tag(preload: str = "") -> SafeString:
|
||||||
|
|
||||||
@register.tag("slot")
|
@register.tag("slot")
|
||||||
def do_slot(parser: Parser, token: Token) -> SlotNode:
|
def do_slot(parser: Parser, token: Token) -> SlotNode:
|
||||||
bits = token.split_contents()
|
# e.g. {% slot <name> ... %}
|
||||||
args = bits[1:]
|
tag_name, *args = token.split_contents()
|
||||||
# e.g. {% slot <name> %}
|
slot_name, is_default, is_required, slot_kwargs = _parse_slot_args(parser, args, tag_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."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use a unique ID to be able to tie the fill nodes with components and slots
|
# 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
|
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
|
||||||
slot_id = gen_id()
|
slot_id = gen_id()
|
||||||
|
@ -153,6 +128,7 @@ def do_slot(parser: Parser, token: Token) -> SlotNode:
|
||||||
is_required=is_required,
|
is_required=is_required,
|
||||||
is_default=is_default,
|
is_default=is_default,
|
||||||
node_id=slot_id,
|
node_id=slot_id,
|
||||||
|
slot_kwargs=slot_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
trace_msg("PARSE", "SLOT", slot_name, slot_id, "...Done!")
|
trace_msg("PARSE", "SLOT", slot_name, slot_id, "...Done!")
|
||||||
|
@ -161,45 +137,35 @@ def do_slot(parser: Parser, token: Token) -> SlotNode:
|
||||||
|
|
||||||
@register.tag("fill")
|
@register.tag("fill")
|
||||||
def do_fill(parser: Parser, token: Token) -> FillNode:
|
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.
|
'slot'-block in the component template referred to by a parent component.
|
||||||
It exists to make component nesting easier.
|
It exists to make component nesting easier.
|
||||||
|
|
||||||
This tag is available only within a {% component %}..{% endcomponent %} block.
|
This tag is available only within a {% component %}..{% endcomponent %} block.
|
||||||
Runtime checks should prohibit other usages.
|
Runtime checks should prohibit other usages.
|
||||||
"""
|
"""
|
||||||
bits = token.split_contents()
|
|
||||||
tag = bits[0]
|
|
||||||
args = bits[1:]
|
|
||||||
# e.g. {% fill <name> %}
|
# e.g. {% fill <name> %}
|
||||||
alias_fexp: Optional[FilterExpression] = None
|
tag_name, *args = token.split_contents()
|
||||||
if len(args) == 1:
|
slot_name_fexp, alias_fexp, scope_var_fexp = _parse_fill_args(parser, args, tag_name)
|
||||||
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)}.")
|
|
||||||
|
|
||||||
# Use a unique ID to be able to tie the fill nodes with components and slots
|
# 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
|
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
|
||||||
fill_id = gen_id()
|
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"])
|
nodelist = parser.parse(parse_until=["endfill"])
|
||||||
parser.delete_first_token()
|
parser.delete_first_token()
|
||||||
|
|
||||||
fill_node = FillNode(
|
fill_node = FillNode(
|
||||||
nodelist,
|
nodelist,
|
||||||
name_fexp=FilterExpression(tgt_slot_name, tag),
|
name_fexp=slot_name_fexp,
|
||||||
alias_fexp=alias_fexp,
|
alias_fexp=alias_fexp,
|
||||||
|
scope_fexp=scope_var_fexp,
|
||||||
node_id=fill_id,
|
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
|
return fill_node
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,8 +185,8 @@ def do_component(parser: Parser, token: Token) -> ComponentNode:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
bits, isolated_context = check_for_isolated_context_keyword(bits)
|
bits, isolated_context = _check_for_isolated_context_keyword(bits)
|
||||||
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
|
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
|
# 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
|
# 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()
|
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)
|
return HtmlAttrsNode(attributes, default_attrs, append_attrs)
|
||||||
|
|
||||||
|
|
||||||
def is_whitespace_node(node: Node) -> bool:
|
def _check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], 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]:
|
|
||||||
"""Return True and strip the last word if token ends with 'only' keyword or if CONTEXT_BEHAVIOR is 'isolated'."""
|
"""Return True and strip the last word if token ends with 'only' keyword or if CONTEXT_BEHAVIOR is 'isolated'."""
|
||||||
|
|
||||||
if bits[-1] == "only":
|
if bits[-1] == "only":
|
||||||
|
@ -306,7 +260,7 @@ def check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], bool
|
||||||
return bits, False
|
return bits, False
|
||||||
|
|
||||||
|
|
||||||
def parse_component_with_args(
|
def _parse_component_with_args(
|
||||||
parser: Parser, bits: List[str], tag_name: str
|
parser: Parser, bits: List[str], tag_name: str
|
||||||
) -> Tuple[str, List[FilterExpression], Mapping[str, FilterExpression]]:
|
) -> Tuple[str, List[FilterExpression], Mapping[str, FilterExpression]]:
|
||||||
tag_args, tag_kwarg_pairs = parse_bits(
|
tag_args, tag_kwarg_pairs = parse_bits(
|
||||||
|
@ -336,7 +290,7 @@ def parse_component_with_args(
|
||||||
return component_name, context_args, tag_kwargs
|
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
|
parser: Parser, bits: List[str], tag_name: str
|
||||||
) -> Tuple[Optional[FilterExpression], Optional[FilterExpression], List[Tuple[str, FilterExpression]]]:
|
) -> Tuple[Optional[FilterExpression], Optional[FilterExpression], List[Tuple[str, FilterExpression]]]:
|
||||||
tag_args, tag_kwarg_pairs = parse_bits(
|
tag_args, tag_kwarg_pairs = parse_bits(
|
||||||
|
@ -372,6 +326,111 @@ def parse_html_attrs_args(
|
||||||
return attrs, defaults, tag_kwarg_pairs
|
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(
|
def _get_positional_param(
|
||||||
tag_name: str,
|
tag_name: str,
|
||||||
param_name: str,
|
param_name: str,
|
||||||
|
@ -393,7 +452,3 @@ def _get_positional_param(
|
||||||
|
|
||||||
def is_wrapped_in_quotes(s: str) -> bool:
|
def is_wrapped_in_quotes(s: str) -> bool:
|
||||||
return s.startswith(('"', "'")) and s[0] == s[-1]
|
return s.startswith(('"', "'")) and s[0] == s[-1]
|
||||||
|
|
||||||
|
|
||||||
def strip_quotes(s: str) -> str:
|
|
||||||
return s.strip("\"'")
|
|
||||||
|
|
|
@ -9,13 +9,13 @@ from .testutils import BaseTestCase
|
||||||
|
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
from django_components.component import safe_resolve_dict, safe_resolve_list
|
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):
|
class ParserTest(BaseTestCase):
|
||||||
def test_parses_args_kwargs(self):
|
def test_parses_args_kwargs(self):
|
||||||
bits = ["component", "my_component", "42", "myvar", "key='val'", "key2=val2"]
|
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}
|
ctx = {"myvar": {"a": "b"}, "val2": 1}
|
||||||
args = safe_resolve_list(raw_args, ctx)
|
args = safe_resolve_list(raw_args, ctx)
|
||||||
|
@ -35,7 +35,7 @@ class ParserTest(BaseTestCase):
|
||||||
"@event:na-me.mod=bzz",
|
"@event:na-me.mod=bzz",
|
||||||
"#my-id=True",
|
"#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"})
|
ctx = Context({"date": 2024, "bzz": "fzz"})
|
||||||
args = safe_resolve_list(raw_args, ctx)
|
args = safe_resolve_list(raw_args, ctx)
|
||||||
|
|
|
@ -2392,3 +2392,214 @@ class IterationFillTest(BaseTestCase):
|
||||||
{'inner': ['ITER2_OBJ1', 'ITER2_OBJ2']} default
|
{'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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue