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

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

Open
JulianFeinauer opened this issue Dec 25, 2020 · 9 comments
Open

Comments

@JulianFeinauer
Copy link
Contributor

JulianFeinauer commented Dec 25, 2020

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

#60

@DamnedScholar
Copy link
Contributor

DamnedScholar commented Dec 25, 2020

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

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

  • A Stimulus controller can appear multiple times on a page and a full-fledged UI written in Stimulus is likely to have multiple controllers. It's much easier to compile data-attributes in OOP code with full access to things like dicts and tuples than it is to write everything out in a clunky template language, so it makes sense to ship the guts of whole controllers with the context so that you can programmatically reason about the details with the full power of Python behind you. It might be graceful from the end-developer perspective to have the template tag accept both dot notation to a dict in the context and a series of arguments, but I personally want to move my controller details out of the templates and into context as much as I can.
  • 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.
  • Stimulus also allows multiple controllers per element, so it might be necessary to have the ability to reason about that built into the tag. The controller names have to live on the same data-attribute, so they'd have to be defined in one template tag (which can be smart enough to assign all of the Values/Classes data to the appropriate places).
  • Stimulus 2 uses data-user-target="avatar syntax, and targets have to be inside a controller. An element can have as many target identifiers as you like, but identifiers for the same controller have to live on the same attribute.
  • Multiple actions can live on the same element, and they have to be in the same attribute. They can be for different controllers, and the required minimum syntax changes depending on the element in question.
  • An element with a data-controller and a data-reflex actually has two controllers, one explicit and one implicit. The implicit controller is attached by stimulus_reflex at page load.
  • data-reflex is a wrapper around data-action that does relatively little on the client and runs this.stimulate(). Some controllers will want to do client-side processing or messaging around the reflex actions, so even in a case where most of the interactivity is going through your server, you don't want to hamstring Stimulus. These methods aren't compatible. When you trigger the data-action on an element, you have to call this.stimulate() yourself unless you've also triggered the data-reflex, and when you trigger the data-reflex the explicit controller has no way of tracking that reflex because you've gone through a different controller.
  • Multiple reflexes can live on the same element, and they have to be in the same attribute.
  • Reflexes also use data attributes, but they don't have to be controller-namespaced. This is something where people could get tripped up, with their Reflexes recognizing data that their controllers don't.

@jonathan-s
Copy link
Owner

jonathan-s commented Dec 25, 2020

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

jonathan-s commented Dec 25, 2020

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

JulianFeinauer commented Dec 25, 2020

@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
Copy link
Contributor Author

@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
Copy link
Contributor

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

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

@jonathan-s
Copy link
Owner

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"

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