EmilStenstrom / django-components

Create simple reusable template components in Django.
MIT License
1.18k stars 76 forks source link

Support for AlpineJS in slots #644

Open JuroOravec opened 2 months ago

JuroOravec commented 2 months ago

Background

When I have a slot with isolated context, I expect that the variables accessible inside the slot are the same as outside of the component. In other words, whatever variables the component defines, it does NOT leak into the slot, unless explicitly set:

{{ outside_var }}
{% component "table" %}
    {{ outside_var }}    <-- same as above
{% endcomponent %}

For the UI component library, I'm using AlpineJS. To emulate slot Vue behaviour, I need this behavior of isolated context to be true also for AlpineJS variables when using context_behavior="isolated":

<div x-data="{ outsideVar: 123 }">
  {% component "table" %}
    <div x-text="outsideVar"> <-- same as above
    </div>
  {% endcomponent %}
</div>

Implementation

I was able to achieve this using x-teleport like so:

  1. Django component that supports these sort of "alpine-enabled" slots defines the django slots either at the very end or very start of its template, so they are not nested in anything else.
  2. But instead of using plain {% slot %} tag, I wrap it in two HTML elements, <template> and <div>
  3. And then, at the place where I wanted to ORIGINALLY put the slot, I define an element with an ID into which the slot is inserted
<template x-teleport="#abc">
    <div {% html_attrs attrs %}>
        {% slot "default" default %}
    </div>
</template>

{# The actual component body #}
<main>
    <div>
        <span id="abc">
        </span>
        ...
    </div>
</main>

Notes:

Accessing slot data

By adding x-data to the <div>, I am able to expose the Alpine data to the slot the same way I can expose Django data to the slot using {% slot data="var" %}:

<template x-teleport="#abc">
    <div x-data="{ $slot: data }" {% html_attrs attrs %}>
        {% slot "default" default %}
    </div>
</template>

So, from within the slot, the data can then be accessed as $slot:

<div x-data="{ outsideVar: 123 }">
  {% component "table" %}
    <div x-text="outsideVar"> <-- same as above
    </div>
    <div x-text="$slot.exposedVar"> <-- exposed from "table"
    </div>
  {% endcomponent %}
</div>

API

Ideally, this would be marked on the {% slot %} tag. Plus we need to allow define what Alpine data should be exposed to the slot.

So it could look like this:

{# Normal slot #}
{% slot "mytable" / %} 

{# Alpine slot, uses `x-teleport`, `$slot` is implicitly an empty JS object #}
{% slot "mytable" alpine / %}

{# `$slot` is explicitly an empty JS object #}
{% slot "mytable" alpine="{}" / %}

{# `$slot` is explicitly a JS object #}
{% slot "mytable" alpine="{ abc: 123 }" / %}

{# `$slot` is explicitly an Alpine variable myJsVar #}
{% slot "mytable" alpine="myJsVar" / %}

If the slot HAS an alpine keyword, we render the <template x-teleport="#abc"> at the very start or an end of the django template. The {% slot %} would be inside the <template>. And, in place of the original slot position, we insert <div id="abc"></div>.

Other considerations

EmilStenstrom commented 2 months ago

Not knowing AlpineJS, I'm trying to wrap my head around this. Are you saying that we should add support for AlpineJS to slot tags? I was hoping that we could avoid having any code that is specific to a certain library in django_components, and that this was something that anyone can add in a separate library? Are you thinking differently?

JuroOravec commented 2 months ago

I'm thinking the same, ideally it would be a plugin. But right now it's hard to imagine how plugins should look like, so I was thinking of first implementing this, and then reverse-engineering API for plugins. But we can also do it in a single step.

For the Alpine slots support, I imagine the plugin would:

EmilStenstrom commented 2 months ago

Not sure if this warrants the complexity of maintaining a whole plugin system, but you're doing the work here :)

jdare-compass commented 1 month ago

Just wanted to chime in that I'm running into this issue as well and would make use of a plugin that addresses it

EmilStenstrom commented 1 month ago

@jdare-compass @zachbellay Tell us more. In your own words: What is the problem, and how should we solve it in your opinion?