mixxorz / slippers

A UI component framework for Django. Built on top of Django Template Language.

Home Page:https://mitchel.me/slippers/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Passing component parameters to subcomponents and content

vsajip opened this issue Β· comments

Consider this component (named tester):

<div {% attrs id %} aria-labelledby="{{ labelid }}">
{{ children }}
</div>

Instantiated like this:

{% #tester id="foo" labelid="bar" %}<h1 id="{{ labelid }}">My label</h1>{% /tester %}

Results in

<div id="foo" aria-labelledby="bar">
<h1 id="">My label</h1>
</div>

Clearly the intent is for the h1 to have id="bar", but it's not happening, seemingly because the component context isn't available when rendering the children. Is there some way to pass this context that I'm missing? To be clear, I mean just the context passed to the component - id and labelid.

Hi @vsajip,
From reading the docs I'm not sure 100% sure if this behaviour is deliberate or not in relation to reducing side-effects, however, if you haven't considered it already, you might be able to solve this issue by assigning your labelid to a var tag first?
i.e.

{% var label_id="bar" %}
{% #tester id="foo" labelid=label_id %}<h1 id="{{ label_id }}">My label</h1>{% /tester %}

Thanks, for the suggestion, Ben. Though that might work in this simple case, I'm not sure it's more generally applicable. According to the docs, var is

intended to be used inside component templates as a means to document the variables it requires and specify defaults.

I think that in your example, setting the var outside the component, as you have, is no different to having label_id in the context passed into the main template. So the h1 is picking up the global value, not the value passed to the component. If I had 10 different tester instances in a realistic case, am I to create 10 var directives too?

There's another issue - #44 - where I agree that one shouldn't pass the context automatically into every component, as that would make things hard to reason about and lead to unwanted coupling. However, In my case, I want values that are known about at the component level (labelid, id) to be available to the child content (whether just template text, as in my example, or subcomponents). That seems reasonable, as everything is inside the component. I would think it reasonable to have to pass e.g. labelid to subcomponents as necessary, so that they don't rely on being inside a particular component.

I created #52 to fix this issue and it's working for me, so far at least πŸ˜„

Yeah that's definitely a good point. I wanted to propose a potential workaround as a temporary solution in case this issue takes time to resolve, and you'd prefer not to fork the package.
I have a feeling it's a somewhat complex issue as your questions depend on solidifying the design pattern for the package itself, and your suggestion points towards the idea of decoupling data instead of restructuring your component's logic.

For example your suggested fix might be better off looking more like this instead:

{% #tester id="foo" labelid="bar" %}<h1 id="{{ props.labelid }}">My label</h1>{% /tester %}

Probably more of a question for the package maintainers however.

and you'd prefer not to fork the package

Well, you have to fork it to propose a PR πŸ˜„ and that's all I intended.

I have a feeling it's a somewhat complex issue

Is it? I see it as somewhat analogous to functions in that both are used to break bigger stuff down into smaller, more manageable chunks. Sort of like this:

def component(param1, param2):
    # You should be able to use param1 and param2 values here, which is the "children" bit
    param3 = ...  # analogous to a var directive
    # and pass to subcomponent, which can't automatically see param1 and param2
    # but can see param value
    subcomponent(param=param3) 

Edit: removed mention of globals

Is props documented somewhere, or an idea you're expressing? The documentation at https://mitchel.me/slippers/ doesn't appear to be searchable 😞 and I didn't come across props.

Well, you have to fork it to propose a PR πŸ˜„ and that's all I intended.

Sorry, for clarity I meant if you wanted a solution which didn't involve having to pin a forked package, and yes props was just an idea I was trying to express.

I did think this could be more complicated depending on what Slipper's expected behaviour is in terms of whether this is a bug or an issue needing to be resolved pending decisions around design patterns, I figured it would be the latter as it's still a still a pre-1.0 package, so my intetnions was to try and help out in the meantime.

Thinking about it more I guess my only main concern is around variable scope effecting code readability and maintainbility, if developers were to follow this pattern then it could be harder for developers to understand the data flow within components (e.g. not having a clear indication of their origin), the props example was suppose to highlight how you could support your change with less ambiguitly around variable scope.

I thought you might be introducing issues with variable shadowing, however, it seems your changes likely follow Django templates expected behaviour anyway, for example what happens if a another category variable is introduced to the template context with the code below:

{% #card heading="Card title" category="Tech" %}  
    <span>News summary report on {{ category }}.</span>
{% /card %}

Which value would a developer expect category to use? and should this throw a template error to help avoid introducing bugs? I think the category varaible from the card parameter is what you'd normal expect in DTL, so probably not an issue but one to check and write a test for.

I also thought there could be potential issues around altering parameters in the component itself, for example what if the component code for the card example above looked like this:

{% var category=category|default:"fashion" %}
<div class="card">  
    <h1 class="card__header">{{ heading }} | {{ category }}</h1>  
    <div class="card__body">
        {{ children }}
    </div>
</div>

and I define the component this way with a category variable set to "Tech" in the local context.

{% #card heading="Card title" %}  
    <span>News summary report on {{ category }}.</span>
{% /card %}

The behaviour here would be somewhat confusing, should category be "Tech", or should it render as "Fashion", but then again this might also follow what Django template tags expect, perhaps your PR will need another test to confirm the expected behaviour works.

Sure, I agree that providing clarity for developers about how the system behaves is key. I made the change locally so I could progress the project where I'm using Slippers - I'm new to Slippers but planning to use it quite heavily to simplify some quite gnarly templates, and I'll be looking out for surprises. In terms of adding tests etc., I thought to wait and see what @mixxorz might have to say about all this.

With the changes in my PR, I can illustrate the various possibilities. Assume the component tester is

<div>
{% var foo=foo|default:"Var foo" %}
{% var bar=bar|default:"Var bar" %}
In component, before children:
<pre class="mb-0">
Foo: {{ foo }}
Bar: {{ bar }}
</pre>
Children:
<div class="pl-4">
{{ children }}
</div>
</div>

and let the instantiation be

<pre>
Foo: {{ foo }}
Bar: {{ bar }}
</pre>
<hr>
{% #tester foo="Parameter foo" %}
<pre>
Foo: {{ foo }}
Bar: {{ bar }}
</pre>
{% /tester %}
<hr>
{% #tester bar="Parameter bar" %}
<pre>
Foo: {{ foo }}
Bar: {{ bar }}
</pre>
{% /tester %}
<hr>

And let only foo be defined in the context with value "Context foo". You then get the result
ss33
Which means that:

  • Parameters in an instantiation are visible in the component and in children.
  • var values are visible in the component but not in children (so they're private to the component).
  • Template context values are visible in the children, but not in the component (that's #44).

I expect it should be possible to make var definitions be visible to children, if that is seen as desirable (sort of like how a closure works), but some details would need to be worked out - for example, would it only be var declarations that appear before {{ children }} ?

I've been thinking about this. I am not yet sure what I think about this and expect I'll need some time to form a strong opinion.

One approach I've explored is not doing this by default and using a special keyword to enable this functionality.

{% #Card with title="My heading" %}
    <h1>{{ title }}</h1>
{% /Card %}

The keyword is with because this behavior is fairly close to the {% with %} tag.

The reason for me suggesting that it be the default is that there is no other way of getting variable/parametric information into component content which is not in subcomponents (whereas one could choose to pass parameters as e.g. labelid=labelid to nested components if needed).

We might disagree here, but for me that is a good thing.

I do not want component children to have a dependency on the component's context because you can end up breaking the rendering of children when you introduce changes at the component-level. Components will no longer be self-contained. Changing the component code could now have side-effects elsewhere in your code.

With the current implementation of Slippers, you can be sure that whatever happens to the component, your children will be rendered just as you passed them and you do not need to worry about side-effects.

I am leaning towards keeping the children decoupled from the component, at least by default.

Components will no longer be self-contained. Changing the component code could now have side-effects elsewhere in your code.

That is always true, is it not? For example, a breaking change could still be introduced into a component regardless of the resolution of this issue. "Breaking" in the sense that behaviour can change in an unexpected/surprising way after a change to a component.

Also, @mixxorz, what alternative method do you propose to address my use case in the initial post in this issue?