docs: add advanced topics

This commit is contained in:
Gabriel Dugny 2024-08-02 22:31:08 +02:00 committed by Emil Stenström
parent ab0b5c6266
commit 052945415e
3 changed files with 407 additions and 0 deletions

View file

@ -14,3 +14,6 @@
* [Settings](integration/settings.md)
* [CLI Commands](integration/commands.md)
* [Logging and Debugging](integration/logging_debugging.md)
* Advanced topics
* [Slot rendering](advanced/slot_rendering.md)
* [Slots and Blocks](advanced/slots_and_blocks.md)

View file

@ -0,0 +1,238 @@
# Slot rendering
This doc serves as a primer on how component slots and fills are resolved.
## Flow
1. Imagine you have a template. Some kind of text, maybe HTML:
```django
| ------
| ---------
| ----
| -------
```
2. The template may contain some vars, tags, etc
```django
| -- {{ my_var }} --
| ---------
| ----
| -------
```
3. The template also contains some slots, etc
```django
| -- {{ my_var }} --
| ---------
| -- {% slot "myslot" %} ---
| -- {% endslot %} ---
| ----
| -- {% slot "myslot2" %} ---
| -- {% endslot %} ---
| -------
```
4. Slots may be nested
```django
| -- {{ my_var }} --
| -- ABC
| -- {% slot "myslot" %} ---
| ----- DEF {{ my_var }}
| ----- {% slot "myslot_inner" %}
| -------- GHI {{ my_var }}
| ----- {% endslot %}
| -- {% endslot %} ---
| ----
| -- {% slot "myslot2" %} ---
| ---- JKL {{ my_var }}
| -- {% endslot %} ---
| -------
```
5. Some slots may be inside fills for other components
```django
| -- {{ my_var }} --
| -- ABC
| -- {% slot "myslot" %}---
| ----- DEF {{ my_var }}
| ----- {% slot "myslot_inner" %}
| -------- GHI {{ my_var }}
| ----- {% endslot %}
| -- {% endslot %} ---
| ------
| -- {% component "mycomp" %} ---
| ---- {% slot "myslot" %} ---
| ------- JKL {{ my_var }}
| ------- {% slot "myslot_inner" %}
| ---------- MNO {{ my_var }}
| ------- {% endslot %}
| ---- {% endslot %} ---
| -- {% endcomponent %} ---
| ----
| -- {% slot "myslot2" %} ---
| ---- PQR {{ my_var }}
| -- {% endslot %} ---
| -------
```
5. I want to render the slots with `{% fill %}` tag that were defined OUTSIDE of this template. How do I do that?
1. Traverse the template to collect ALL slots
- NOTE: I will also look inside `{% slot %}` and `{% fill %}` tags, since they are all still
defined within the same TEMPLATE.
I should end up with a list like this:
```txt
- Name: "myslot"
ID 0001
Content:
| ----- DEF {{ my_var }}
| ----- {% slot "myslot_inner" %}
| -------- GHI {{ my_var }}
| ----- {% endslot %}
- Name: "myslot_inner"
ID 0002
Content:
| -------- GHI {{ my_var }}
- Name: "myslot"
ID 0003
Content:
| ------- JKL {{ my_var }}
| ------- {% slot "myslot_inner" %}
| ---------- MNO {{ my_var }}
| ------- {% endslot %}
- Name: "myslot_inner"
ID 0004
Content:
| ---------- MNO {{ my_var }}
- Name: "myslot2"
ID 0005
Content:
| ---- PQR {{ my_var }}
```
2. Note the relationships - which slot is nested in which one
I should end up with a graph-like data like:
```txt
- 0001: [0002]
- 0002: []
- 0003: [0004]
- 0004: []
- 0005: []
```
In other words, the data tells us that slot ID `0001` is PARENT of slot `0002`.
This is important, because, IF parent template provides slot fill for slot 0001,
then we DON'T NEED TO render it's children, AKA slot 0002.
3. Find roots of the slot relationships
The data from previous step can be understood also as a collection of
directled acyclig graphs (DAG), e.g.:
```txt
0001 --> 0002
0003 --> 0004
0005
```
So we find the roots (`0001`, `0003`, `0005`), AKA slots that are NOT nested in other slots.
We do so by going over ALL entries from previous step. Those IDs which are NOT
mentioned in ANY of the lists are the roots.
Because of the nature of nested structures, there cannot be any cycles.
4. Recursively render slots, starting from roots.
1. First we take each of the roots.
2. Then we check if there is a slot fill for given slot name.
3. If YES we replace the slot node with the fill node.
- Note: We assume slot fills are ALREADY RENDERED!
```django
| ----- {% slot "myslot_inner" %}
| -------- GHI {{ my_var }}
| ----- {% endslot %}
```
becomes
```django
| ----- Bla bla
| -------- Some Other Content
| ----- ...
```
We don't continue further, because inner slots have been overriden!
4. If NO, then we will replace slot nodes with their children, e.g.:
```django
| ---- {% slot "myslot" %} ---
| ------- JKL {{ my_var }}
| ------- {% slot "myslot_inner" %}
| ---------- MNO {{ my_var }}
| ------- {% endslot %}
| ---- {% endslot %} ---
```
Becomes
```django
| ------- JKL {{ my_var }}
| ------- {% slot "myslot_inner" %}
| ---------- MNO {{ my_var }}
| ------- {% endslot %}
```
5. We check if the slot includes any children `{% slot %}` tags. If YES, then continue with step 4. for them, and wait until they finish.
5. At this point, ALL slots should be rendered and we should have something like this:
```django
| -- {{ my_var }} --
| -- ABC
| ----- DEF {{ my_var }}
| -------- GHI {{ my_var }}
| ------
| -- {% component "mycomp" %} ---
| ------- JKL {{ my_var }}
| ---- {% component "mycomp" %} ---
| ---------- MNO {{ my_var }}
| ---- {% endcomponent %} ---
| -- {% endcomponent %} ---
| ----
| -- {% component "mycomp2" %} ---
| ---- PQR {{ my_var }}
| -- {% endcomponent %} ---
| ----
```
- NOTE: Inserting fills into {% slots %} should NOT introduce new {% slots %}, as the fills should be already rendered!
## Using the correct context in {% slot/fill %} tags
In previous section, we said that the `{% fill %}` tags should be already rendered by the time they are inserted into the `{% slot %}` tags.
This is not quite true. To help you understand, consider this complex case:
```django
| -- {% for var in [1, 2, 3] %} ---
| ---- {% component "mycomp2" %} ---
| ------ {% fill "first" %}
| ------- STU {{ my_var }}
| ------- {{ var }}
| ------ {% endfill %}
| ------ {% fill "second" %}
| -------- {% component var=var my_var=my_var %}
| ---------- VWX {{ my_var }}
| -------- {% endcomponent %}
| ------ {% endfill %}
| ---- {% endcomponent %} ---
| -- {% endfor %} ---
| -------
```
We want the forloop variables to be available inside the `{% fill %}` tags. Because of that, however, we CANNOT render the fills/slots in advance.
Instead, our solution is closer to [how Vue handles slots](https://vuejs.org/guide/components/slots.html#scoped-slots). In Vue, slots are effectively functions that accept a context variables and render some content.
While we do not wrap the logic in a function, we do PREPARE IN ADVANCE:
1. The content that should be rendered for each slot
2. The context variables from `get_context_data()`
Thus, once we reach the `{% slot %}` node, in it's `render()` method, we access the data above, and, depending on the `context_behavior` setting, include the current context or not. For more info, see `SlotNode.render()`.

View file

@ -0,0 +1,166 @@
# Using `slot` and `block` tags
1. First let's clarify how `include` and `extends` tags work inside components.
So when component template includes `include` or `extends` tags, it's as if the "included"
template was inlined. So if the "included" template contains `slot` tags, then the component
uses those slots.
So if you have a template `abc.html`:
```django
<div>
hello
{% slot "body" %}{% endslot %}
</div>
```
And components that make use of `abc.html` via `include` or `extends`:
```py
@component.register("my_comp_extends")
class MyCompWithExtends(component.Component):
template = """{% extends "abc.html" %}"""
@component.register("my_comp_include")
class MyCompWithInclude(component.Component):
template = """{% include "abc.html" %}"""
```
Then you can set slot fill for the slot imported via `include/extends`:
```django
{% component "my_comp_extends" %}
{% fill "body" %}
123
{% endfill %}
{% endcomponent %}
```
And it will render:
```html
<div>
hello
123
</div>
```
2. Slot and block
So if you have a template `abc.html` like so:
```django
<div>
hello
{% block inner %}
1
{% slot "body" %}
2
{% endslot %}
{% endblock %}
</div>
```
and component `my_comp`:
```py
@component.register("my_comp")
class MyComp(component.Component):
template_name = "abc.html"
```
Then:
1. Since the `block` wasn't overriden, you can use the `body` slot:
```django
{% component "my_comp" %}
{% fill "body" %}
XYZ
{% endfill %}
{% endcomponent %}
```
And we get:
```html
<div>hello 1 XYZ</div>
```
2. `blocks` CANNOT be overriden through the `component` tag, so something like this:
```django
{% component "my_comp" %}
{% fill "body" %}
XYZ
{% endfill %}
{% endcomponent %}
{% block "inner" %}
456
{% endblock %}
```
Will still render the component content just the same:
```html
<div>hello 1 XYZ</div>
```
3. You CAN override the `block` tags of `abc.html` if my component template
uses `extends`. In that case, just as you would expect, the `block inner` inside
`abc.html` will render `OVERRIDEN`:
````py
@component.register("my_comp")
class MyComp(component.Component):
template_name = """
{% extends "abc.html" %}
{% block inner %}
OVERRIDEN
{% endblock %}
"""
```
````
4. This is where it gets interesting (but still intuitive). You can insert even
new `slots` inside these "overriding" blocks:
```py
@component.register("my_comp")
class MyComp(component.Component):
template_name = """
{% extends "abc.html" %}
{% load component_tags %}
{% block "inner" %}
OVERRIDEN
{% slot "new_slot" %}
hello
{% endslot %}
{% endblock %}
"""
```
And you can then pass fill for this `new_slot` when rendering the component:
```django
{% component "my_comp" %}
{% fill "new_slot" %}
XYZ
{% endfill %}
{% endcomponent %}
```
NOTE: Currently you can supply fills for both `new_slot` and `body` slots, and you will
not get an error for an invalid/unknown slot name. But since `body` slot is not rendered,
it just won't do anything. So this renders the same as above:
```django
{% component "my_comp" %}
{% fill "new_slot" %}
XYZ
{% endfill %}
{% fill "body" %}
www
{% endfill %}
{% endcomponent %}
```