From 052945415e35b7bcc7e7bb4ea2a9d5228a48ce41 Mon Sep 17 00:00:00 2001 From: Gabriel Dugny Date: Fri, 2 Aug 2024 22:31:08 +0200 Subject: [PATCH] docs: add advanced topics --- docs/user_guide/SUMMARY.md | 3 + docs/user_guide/advanced/slot_rendering.md | 238 +++++++++++++++++++ docs/user_guide/advanced/slots_and_blocks.md | 166 +++++++++++++ 3 files changed, 407 insertions(+) create mode 100644 docs/user_guide/advanced/slot_rendering.md create mode 100644 docs/user_guide/advanced/slots_and_blocks.md diff --git a/docs/user_guide/SUMMARY.md b/docs/user_guide/SUMMARY.md index abef6c02..466bbf05 100644 --- a/docs/user_guide/SUMMARY.md +++ b/docs/user_guide/SUMMARY.md @@ -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) diff --git a/docs/user_guide/advanced/slot_rendering.md b/docs/user_guide/advanced/slot_rendering.md new file mode 100644 index 00000000..ecde6127 --- /dev/null +++ b/docs/user_guide/advanced/slot_rendering.md @@ -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()`. diff --git a/docs/user_guide/advanced/slots_and_blocks.md b/docs/user_guide/advanced/slots_and_blocks.md new file mode 100644 index 00000000..53102ca4 --- /dev/null +++ b/docs/user_guide/advanced/slots_and_blocks.md @@ -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 +
+ hello + {% slot "body" %}{% endslot %} +
+ ``` + + 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 +
+ hello + 123 +
+ ``` + +2. Slot and block + + So if you have a template `abc.html` like so: + + ```django +
+ hello + {% block inner %} + 1 + {% slot "body" %} + 2 + {% endslot %} + {% endblock %} +
+ ``` + + 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 +
hello 1 XYZ
+ ``` + + 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 +
hello 1 XYZ
+ ``` + + 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 %} + ```