EmilStenstrom / django-components

Create simple reusable template components in Django.
MIT License
965 stars 60 forks source link

fix: loader tags compatibility #468

Closed JuroOravec closed 3 weeks ago

JuroOravec commented 3 weeks ago

Added tests to explore behavior of components with "loader tags" (extends, include, block). Tested out all combinations I could think of:

Surprisingly, it all mostly worked as expected 😄, and I had to update only the traversal logic, so it could extract nodes from extends / include tags.

Closes https://github.com/EmilStenstrom/django-components/issues/134

JuroOravec commented 3 weeks ago

I'll write the behavior out, since it also took me some time to wrap my head around it:

  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 I have a template abc.html:

    <div>
      hello
      {% slot "body" %}{% endslot %}
    </div>

    And components that make use of abc.html via include or extends:

    @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 I can set slot fill for the slot imported via include/extends:

    {% component "my_comp_extends" %}
        {% fill "body" %}
            123
        {% endfill %}
    {% endcomponent %}

    And it will render:

    <div>
      hello
      123
    </div>
  2. Slot and block

    So if I have a template abc.html like so:

    <div>
      hello
      {% block inner %}
        1
        {% slot "body" %}
          2
        {% endslot %}
      {% endblock %}
    </div>

    and component my_comp:

    @component.register("my_comp")
    class MyComp(component.Component):
        template_name = "abc.html"

    Then:

    1. Since the block wasn't overriden, we can use the body slot:

      {% component "my_comp" %}
          {% fill "body" %}
              XYZ
          {% endfill %}
      {% endcomponent %}

      And we get:

      <div>
        hello
        1
          XYZ
      </div>
    2. blocks CANNOT be overriden through the component tag, so something like this:

      {% component "my_comp" %}
          {% fill "body" %}
              XYZ
          {% endfill %}
      {% endcomponent %}
      {% block "inner" %}
          456
      {% endblock %}

      Will still render the component content just the same:

      <div>
        hello
        1
          XYZ
      </div>
    3. I 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:

      @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). I can insert even new slots inside these "overriding" blocks:

      @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 I can then pass fill for this new_slot when rendering the component:

      {% component "my_comp" %}
          {% fill "new_slot" %}
              XYZ
          {% endfill %}
      {% endcomponent %}

      NOTE: Currently I can supply fills for both new_slot and body slots, and I 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:

      {% component "my_comp" %}
          {% fill "new_slot" %}
              XYZ
          {% endfill %}
          {% fill "body" %}
              www
          {% endfill %}
      {% endcomponent %}
JuroOravec commented 3 weeks ago

I've included the explainer in docs/slots_and_blocks.md

EmilStenstrom commented 3 weeks ago

I just wanted to say that this is great work again. Thank you!