Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for AlpineJS in slots #644

Open
JuroOravec opened this issue Sep 4, 2024 · 5 comments
Open

Support for AlpineJS in slots #644

JuroOravec opened this issue Sep 4, 2024 · 5 comments

Comments

@JuroOravec
Copy link
Collaborator

JuroOravec commented Sep 4, 2024

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:

  • <template> is required by AlpineJS, and that's where we define the teleport
  • <div> inside the <template> is used for 2 reasons:
    1. Because x-teleport requires a single element inside it, so by inserting the <div>, we allow the actual slot content to be anything.
    2. As a place to bind slot data to the slot, similarly how we do {% slot data="var" %}

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

  • The generated IDs could look like "slot-{component_id}-{slot_name}", e.g. slot-a0b1c3-content

  • Because we use the <div> inside the x-teleport, and another to specify the target ID, it means that slots using the alpine keyword would have 2 extra <div>s wrapping the actual content.

    So if I had

     {% component "table" %}
       {% slot "content" alpine %}
         Hello
       {% endslot %}
     {% endcomponent %}
    

    In the slot's position, we'd actually render:

     <div id="abc">
         <div x-data="{ $slot: data }">
     		hello
     	<div>
     </div>

    This could break some styling setups, as users wouldn't be able to modify the divs.

    So instead we could add CSS classes to the slots:

     <div id="abc" class="slot-outer__my_slot">
         <div x-data="{ $slot: data }" class="slot-inner__my_slot">
     		hello
     	<div>
     </div>

    Then, together with scoped CSS, people would be able to style these as:

     me .slot-outer__my_slot {
     	flex: auto;
     }
    
     me .slot-inner__my_slot {
     	background: red;
     }
    
@EmilStenstrom
Copy link
Owner

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
Copy link
Collaborator Author

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:

  • Pre-process inputs passed to tags, e.g. so slot could accept alpine="{}" keyword, which the plugin would detect, and remove from inputs.

  • Once a tag was been processed into a Node, the plugin would then need to store the plugin-specific data somewhere. So each Node would have a meta or extra attribute, where plugin authors could store the extra data that could then later be accessed in render phase.

    So e.g. given {% slot alpine="{ abc: 123 }" %}, the plugin would extract and set SlotNode.meta["alpine_slots__data"] = "{ abc: 123 }"

    alpine_slots__ being a unique prefix for the plugin.

  • They could also modify the template (or rather, make copies from the original templates?)
    So that the support for Alpine slots could be implmenteded as template transformation.

@EmilStenstrom
Copy link
Owner

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

@jdare-compass
Copy link

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
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants