jonathan-s / django-sockpuppet

Build reactive applications with the django tooling you already know and love.
https://github.com/jonathan-s/django-sockpuppet
MIT License
450 stars 22 forks source link

Provide Template Tag to generate data-reflex html attributes #59

Open JulianFeinauer opened 3 years ago

JulianFeinauer commented 3 years ago

Feature Request

Currently reflex attributes have to be hand-craftet inside the templates. A custom tag could help users to get these right.

Is your feature request related to a problem?

No

Describe the solution you'd like

I suggest a template tag like: {% stimulus 'lazy_load_reflex#increment' count=count increment=1 %} which would be used in a Template like

<a  href="#" {% stimulus 'lazy_load_reflex#increment' count=count increment=1 %}>Increment {{ count }}</a>

and would generate the following HTML (formatted differently)

<a  href="#"
        data-reflex="click->Lazy_LoadReflex#increment"
        data-count="0"
        data-increment="1"
>Increment {{ count }}</a>

PR link

https://github.com/jonathan-s/django-sockpuppet/pull/60

DamnedScholar commented 3 years ago

Stimulus also happens to be the name of the front-end framework where it's entirely possible to build an entire UI without once ever referencing the server past the initial page load, so a tag should not be targeted at the subset functionality of this package while being labeled more generally, as that would be confusing.

I think it actually makes sense to have a stimulus tag and a reflex tag. You don't use them on the same element, in most cases, since a Stimulus controller is likely to manage a whole component while Reflexes are fired by individual controls inside that component.

As long as the semantics are right, I love the idea.

DamnedScholar commented 3 years ago

What ultimately gets my knickers in a twist is the possibility of doing something like

{% for user in users %}
<section {% stimulus controllers.user %}>
    <a href="#" id="profile-link" {% reflex user.profile %}>{{ user.name }}</a>
    <img src="#" alt="{{ user.name }}" {% target user.avatar %}>
    <a href="#" {% target user.msg %}{% action user.message %}>Tell them how cool they are.</a>
</section>
{% endfor %}

Things to ponder

jonathan-s commented 3 years ago

Stimulus tracks data-attributes with controller-namespaced names (eg. data-user-id="", data-user-friendslist-class="") and it's important to be able to dole those out correctly.

Yes, this is the new stimulus 2.0 syntax. The current docs still contain syntax for stimulus 2.0, see #61

jonathan-s commented 3 years ago

It's an interesting idea using

{% stimulus_controller 'example_reflex' %}

{% endcontroller %}

However controllers can be nested like this.

<div data-reflex="someEvent->ExampleReflex#increment">
  <div data-reflex="click->OtherReflex#do_something">

  </div>
</div>

From what I can tell the stimulus_controller won't handle nesting particularly well.

When thinking about this we really have two concepts here that are easy to conflate. We've got stimulus and reflex. They just happen to share some attributes. So it could be wise to separate the two.

So if we have a reflex template tag that only cares about name, action and method to generate. It would also make sure to validate that we've got a reflex in the backend and that it has the method, if not it would throw an error.

{% reflex name="MyReflex" action="click" method="my_method" %}
data-reflex="click->MyReflex#my_method"

Then we could have a stimulus tag that could generate all the data other attributes.

One of the slightly annoying bits in stimulus2.0 is that there is syntax that looks like this, the data attributes become fairly verbose.

https://stimulus.hotwire.dev/reference/values
<div data-controller="loader"
     data-loader-url-value="/messages">
</div>

https://stimulus.hotwire.dev/reference/targets
<form data-controller="search checkbox">
  <input type="checkbox" data-search-target="projects" data-checkbox-target="input">
  <input type="checkbox" data-search-target="messages" data-checkbox-target="input">
  …
</form>

https://stimulus.hotwire.dev/reference/css-classes
<form data-controller="search"
      data-search-loading-class="search--busy"
      data-search-no-results-class="search--empty"
>

# Stimulus and reflexes can also access normal data attributes. 
# The data attributes don't belong to any particular controller or reflex.
<div data-hello="world" data-hello2="world">

</div>

Personally I'd be happy to limit the current PR for reflex things only to limit the scope. Getting a good solution for the stimulus parts seems to increase the scope quite dramatically.

Thoughts on this @DamnedScholar and @JulianFeinauer?

JulianFeinauer commented 3 years ago

@jonathan-s I agree with your comment but thats also possible at the moment. In fact, the *_reflex tag can optionally get two list parameters. Then, the first is the controller and the second the method, like that example (from tag_example.html):

<a href="#" {% click_reflex 'example_reflex' 'increment' parameter=parameter %}>click me</a>

If the rexlex is defined INSIDE a {% stimulus 'xx' %} Block it is allowed to not give a controller (and the one from the stimulus tag is used).

This snippet would be similar:

{% stimulus_controller 'example_reflex' %}
  <a href="#" {% click_reflex 'increment' parameter=parameter %}>click me</a>
{% endcontroller %}

Of cours all of this is undocumented but.. just to give you some context. The nice thing would be that we isolate all the technical details away from the user.

JulianFeinauer commented 3 years ago

@DamnedScholar I added the possibility to pass a dict to the *_reflex functions which contains information about controller, reflex and parameters. You can see this in the test test_tag_by_dict where a single dict from the context is used for parametrization.

DamnedScholar commented 3 years ago

So, when stimulus_reflex wakes up, it goes through and attaches a new controller to every element with a data-reflex. Unless I misunderstand something, I don't think there's any chance to convert a declared Reflex into a normal action that triggers your normal controller. The data-reflex syntax never allows you to specify a controller, while it's explicit inside data-action.

JulianFeinauer commented 3 years ago

@DamnedScholar to clarify... I do this in Django and insert the controller on the reflex (as it's needed).

jonathan-s commented 3 years ago

I've been thinking on this for a few days and this an alternative and I think this api for the templatetag could work pretty well.

# The following allows you to add multiple reflexes as a first argument and is shorter. 
{% reflex 'click->MyReflex#method' param="param1" param2="param" combined=True permanent=True %}

# this would expand to 

data-reflex="click->MyReflex#method" 
data-param="param1" 
data-param2="param" 
dataset-reflex-dataset="combined" 
data-reflex-permanent

For the stimulus controller itself. I would suggest

# We can have several actions. 
{% stimulus controller='calendar' (optional) for="calendar" (optional) v-myvalue="value" t-mytarget="target" c-myclass="myclass" action="click->calendar#next" param="param1" %}

# Either "controller" or "for" needs to be defined. controller would expand 
# to data-controller="calendar" `for` would be used if this is applied 
# to a child element of where the parent already has controller defined. 
# Ie data-controller is omitted, but the "for" value is used for to get the correct
# data.

# all in all this would generate the following code 

data-controller="calendar" 
data-calendar-myvalue-value="value" 
data-calendar-target="target" 
data-calendar-myclass-class="myclass" 
data-action="click->calendar#next" 
data-param="param1"

I think this hits the balance of giving flexibility while still being less verbose. Plus it's possible to do validation if you are so inclined. It might be that you can't write t-mytarget ie using hyphen. If not, we could have this be t_mytarget.

And just a note, it looks like rails use this as a template tag.

{% data param="1" param2="2" %} which would expand to data-param="1" data-param2="2"