hotwired / stimulus

A modest JavaScript framework for the HTML you already have

Home Page:https://stimulus.hotwired.dev/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Defining Stimulus values on controller child elements

hschne opened this issue · comments

According to the documentation, "you can read and write HTML data attributes on controller elements as typed values using special controller properties".

However, defining values on child elements doesn't work.

This behavior is inconsistent with targets, which can be defined on any DOM node under a controller element. Also, allowing value attributes on arbitrary child elements would allow greater flexibility. I could imagine doing some nifty stuff with that (e.g. updating values via turbo streams without reloading the controller?)

Reproduction

Assuming a trivial Stimulus controller such as this:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = { name: String };

  connect() {
    console.log("Initial name: ", this.nameValue);
  }

  stateValueChanged() {
    console.log("Name changed: ", this.nameValue);
  }
}

The following HTML code works; the name value is printed to the console correctly.

<div
  id="posts"
  data-controller="posts"
  data-posts-name-value="The name!"
>
</div>

However, the following code doesn't work, only an empty string is printed to the console.

<div id="posts" data-controller="posts">
  <div data-posts-name-value="The name!"></div>
</div>

Proposition

As outlined above, values should be able to be defined using child elements of a given controller.

<div id="posts" data-controller="posts">
  <div data-posts-name-value="The name!"></div>
</div>

I don't know why this is not currently so. Maybe because of some architectural limitation of Stimulus? In any case, I'd be happy to work on this (might need some pointers, though).

By design values are to be defined on the controller element, as they are global to the controller (and uniq).

If you need data attributes on child elements you might be looking for the action params API https://stimulus.hotwired.dev/reference/actions#action-parameters

If this doesn't solve your use case could you elaborate on when you would need a value on a child element?

Thanks for the info! It's not quite what I'm looking for, as params would only be available within actions, right?

If this doesn't solve your use case could you elaborate on when you would need a value on a child element?

So, I'll preface this because I'm not well-versed with Stimulus & Turbo, so maybe I'm just going completely wrong. That said, I had this idea of getting state into javascript land (aka. Stimulus controllers) via Turbo streams. Specifically, I was trying to add markers to a (OpenStreet) map representing connected users. The street map SDK initialization happens within JS.

That works well enough with ActionCable, I can make everything work with subscriptions and connect callbacks:

// map_controller.js
export default class extends Controller {

connect() {
  this.initializeMapSdk();
  this.connectToChannel();
}

this.subscription = consumer.subscriptions.create(
  {
    channel: "LocationChannel",
    key: this.keyValue,
  },
  {
    connected: this._connected.bind(this),
    disconnected: this._disconnected.bind(this),
    received: this._received.bind(this),
  },

// ...
}

However, I got it into my head that maybe you could use value updates in conjunction with Turbo Streams to essentially stream changes into Stimulus / JS land. Call it a scientific inquiry 🤷

We can use values to get the state into Stimulus on page load, and we can also use values to update the state within a Stimulus controller using the callbacks on value changes. As changes to values are observed, I was wondering if we could mutate only the data values attribute via Turbo.

So I thought I could wrap a child element containing the stimulus value in a turbo frame and update only that. And that's where I'm now stuck 😅

export default class extends Controller {
  static values = { state: String };

  stateValueChanged() {
    console.log("State changed", this.stateValue);
  }
}
<div id="posts" data-controller="posts">
  <%= turbo_frame_tag :posts_state do %>
    <div data-posts-state-value="<%= posts.to_json %>"></div>
  <% end %>

I can't wrap the entire controller element in a Turbo frame, as that would cause the controller to disconnect and reconnect (which results in flickering and generally a not-so-nice user experience).

Again, I have a working solution; I just thought it would be neat (and fun) to leverage pure Turbo & Stimulus to solve this. Less Javascript and all, you know. I hope I've made my use case somewhat clear, but please do let me know if I'm not making sense.

Thanks a bunch in advance!

For similar things, I have used two approaches:

Custom action update_data_attribute

use a custom action to target your controller element an update the data attribute (Turbo Boost provides this action)
or you can create yours https://marcoroth.dev/posts/guide-to-custom-turbo-stream-actions

Targets callback

Each markers could be a target element (with it own data attribute). Targets are observed and you get connected/disconnected callbacks.

Thanks a bunch @adrienpoly! I did not know about Custom Turbo Actions, I think those will prove useful 🙌