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

Allow a single stream response to update multiple elements #113

Merged
merged 17 commits into from
Jun 17, 2021

Conversation

blopker
Copy link
Contributor

@blopker blopker commented Jan 16, 2021

Hey all, I'm working with the folks integrating Turbo into Django at https://github.com/hotwire-django. Right now we're making a demo site based off of the RealWorld app spec (https://github.com/gothinkster/realworld) to show how the integration works in practice. Over all it's been a smooth experience.

However, there's one feature we're having a little trouble with. Specifically the 'Favorite button' feature. You can see it in action here. Notice the two Favorite Article buttons. They both contain the same content and both need to be updated in response to the user's action.

Ideally we would respond to the button press with a single Turbo Stream replace response to update both buttons at the same time with the new favorite count and button active state.

In this proposal PR I've made the buttons work by using querySelectorAll instead of getElementById to select all elements with the target ID on the page. Then I've updated each Stream action method to iterate over a list.

What do you think?

@shadowtime2000
Copy link

I am pretty sure that querySelectorAll is really slow, but ngl I am not that good with raw DOM ops like these and I am not a maintainer or anything

@blopker
Copy link
Contributor Author

blopker commented Jan 16, 2021

So I looked into it and while querySelectorAll is slower than getElementById, they both preform operations at order of millions per second. I don't know if performance is necessarily a concern here.

@dhh
Copy link
Member

dhh commented Jan 16, 2021

Dom IDs are supposed to be unique. I don't think we want to encourage straying from that. I'd have each button have their own ID, then just replace each with their own replace action.

@blopker
Copy link
Contributor Author

blopker commented Jan 16, 2021

Hey David, I do agree that the semantics around dom IDs are problematic here. I only bring this up since I do see UIs where there are identical/similar elements on the page that need updating in response to user action. Having to make the stream response aware of those elements add a bit of friction since all the other code to render the elements is identical.

If it's an issue to have multiple of the same IDs on the page for this case, then I'd like to propose a few alternative solutions.

  1. Change the element's target attribute from id to something like data-stream-target. This way we lose the uniqueness semantics around IDs and it becomes more obvious that the attribute is being used for something and is not just an anchor. Or
  2. Allow the stream's target attribute to be a CSS selector. This would allow the developer a little more freedom when selecting stream targets without having to change their HTML.

Both of these proposals would mean an API break, but both would allow a single stream response to update multiple elements without breaking the semantics of ID.

@dhh
Copy link
Member

dhh commented Jan 17, 2021

I think we could introduce a CSS selector style without API breaking by simply reserving the unadorned target to mean #target. Then if you do target=".message", we know that it's a class target. I like that idea and would support an extension for it 👍

@Audrius
Copy link

Audrius commented Jan 17, 2021

Hey, maybe this one could be off-topic, but it is a similar situation I'm trying to solve. I'm testing hotwire rails and I want to update by single action a couple of DOM elements in the page. Those elements could be placed far away from each other around the page and outside of the turbo frame. The way I did - I just placed two actions on update_move.turbo_stream.erb and it worked perfectly. Is this how it should work, right ? Thank you.

# ../update_move.turbo_stream.erb
<turbo-stream action="replace" target="player-to-move">
  <template>
    <div id="player-to-move">
      <strong>Player to move:</strong>
      <%= @board.player_to_move %>
    </div>
  </template>
</turbo-stream>

<turbo-stream action="replace" target="moves-list">
  <template>
    <div id="moves-list">
      <strong>Moves:</strong>
      <%= @board.moves %>
    </div>
  </template>
</turbo-stream>

@blopker
Copy link
Contributor Author

blopker commented Jan 18, 2021

OK! I've added the ability to use CSS selectors as a stream target. The implementation tries to not know anything about CSS selectors by just trying to find the target as if it were an ID, then only using the target as a CSS selector if no elements with that ID are found. This has a nice side effect of not breaking any ID targets that might already look like a CSS selector.

The implementation also continues not to allow multiple IDs unless specified as a CSS selector.

I've added a test as well. Please let me know if more coverage is desirable.

if (cssTarget.length !== 0) {
return Array.prototype.slice.call(cssTarget)
}
this.raise(`target "${this.target}" not found`)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this exception. I'm assuming not finding the target is a programming error, but maybe that's too strict?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too strict, yeah. And a breaking change. You may well stream an update to something that's no longer there.

@blopker blopker changed the title Proposal: Allow a single stream response to update multiple elements Allow a single stream response to update multiple elements Feb 1, 2021
@blopker
Copy link
Contributor Author

blopker commented Feb 11, 2021

I'm happy to fix this up if it's still something we want.

@seanpdoyle seanpdoyle added the enhancement New feature or request label Apr 1, 2021
@dhh
Copy link
Member

dhh commented Jun 8, 2021

Still like this. Let's shape it up.

@dhh
Copy link
Member

dhh commented Jun 17, 2021

Thanks for all the work on this @blopker. I ended up changing the API to avoid any potential conflicts. The existing target="dom_id" attribute is retained just for getElementById lookups, and then a new targets="css_selector" attribute is added as an alternative that'll flow through querySelectorAll.

@dhh dhh merged commit 045833b into hotwired:main Jun 17, 2021
@blopker
Copy link
Contributor Author

blopker commented Jun 18, 2021

I see, that's a good compromise. Glad this worked out.

t27duck added a commit to t27duck/turbo-rails that referenced this pull request Jun 29, 2021
hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

I'm not 100% happy with the "API" to invoke it, but it's the best I could come up with. Feedback appreciated.

The gist of it is the turbo stream tag builder helpers now accept a `target_multiple` keyword arg (default `false`). When set to `true` the end result of the tag will have a `targets` attribute instead of a `target` attribute.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
t27duck added a commit to t27duck/turbo-rails that referenced this pull request Jun 30, 2021
hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

I'm not 100% happy with the "API" to invoke it, but it's the best I could come up with. Feedback appreciated.

The gist of it is the turbo stream tag builder helpers now accept a `target_multiple` keyword arg (default `false`). When set to `true` the end result of the tag will have a `targets` attribute instead of a `target` attribute.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
t27duck added a commit to t27duck/turbo-rails that referenced this pull request Jun 30, 2021
hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

I'm not 100% happy with the "API" to invoke it, but it's the best I could come up with. Feedback appreciated.

The gist of it is the turbo stream tag builder helpers now accept a `target_multiple` keyword arg (default `false`). When set to `true` the end result of the tag will have a `targets` attribute instead of a `target` attribute.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
t27duck added a commit to t27duck/turbo-rails that referenced this pull request Jul 1, 2021
Background: hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

This adds the functionality to the tag builder. All "action methods" now have an `_all` varient which is piped into a new `action_all` method. The only real difference in these methods is the end result is setting `targets` instead of `target` in `turbo_stream_action_tag` which has also been updated to handle this feature.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
t27duck added a commit to t27duck/turbo-rails that referenced this pull request Jul 1, 2021
Background: hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

This adds the functionality to the tag builder. All "action methods" now have an `_all` varient which is piped into a new `action_all` method. The only real difference in these methods is the end result is setting `targets` instead of `target` in `turbo_stream_action_tag` which has also been updated to handle this feature.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
dhh pushed a commit to hotwired/turbo-rails that referenced this pull request Jul 16, 2021
Background: hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

This adds the functionality to the tag builder. All "action methods" now have an `_all` varient which is piped into a new `action_all` method. The only real difference in these methods is the end result is setting `targets` instead of `target` in `turbo_stream_action_tag` which has also been updated to handle this feature.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
ceritium added a commit to ceritium/turbo-rails that referenced this pull request Jul 29, 2021
Background:
- hotwired/turbo#113 allowed for multiple targets in turbo frame streams.
- hotwired#194 implemented it for `Turbo::Streams::TagBuilder`

This attempts to expose the feature on `Turbo::StreamsChannel` methods.
ceritium added a commit to ceritium/turbo-rails that referenced this pull request Aug 26, 2021
Background:
- hotwired/turbo#113 allowed for multiple targets in turbo frame streams.
- hotwired#194 implemented it for `Turbo::Streams::TagBuilder`

This attempts to expose the feature on `Turbo::StreamsChannel` methods.
dhh pushed a commit to hotwired/turbo-rails that referenced this pull request Aug 26, 2021
* Turbo::StreamsChannel can set targets attribute

Background:
- hotwired/turbo#113 allowed for multiple targets in turbo frame streams.
- #194 implemented it for `Turbo::Streams::TagBuilder`

This attempts to expose the feature on `Turbo::StreamsChannel` methods.

* Add assertions for `targets` argument
atosbucket added a commit to atosbucket/turbo-rails that referenced this pull request Apr 11, 2024
Background: hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

This adds the functionality to the tag builder. All "action methods" now have an `_all` varient which is piped into a new `action_all` method. The only real difference in these methods is the end result is setting `targets` instead of `target` in `turbo_stream_action_tag` which has also been updated to handle this feature.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
atosbucket added a commit to atosbucket/turbo-rails that referenced this pull request Apr 11, 2024
* Turbo::StreamsChannel can set targets attribute

Background:
- hotwired/turbo#113 allowed for multiple targets in turbo frame streams.
- hotwired/turbo-rails#194 implemented it for `Turbo::Streams::TagBuilder`

This attempts to expose the feature on `Turbo::StreamsChannel` methods.

* Add assertions for `targets` argument
luisjose1996 added a commit to luisjose1996/Turbo-Rails that referenced this pull request May 10, 2024
Background: hotwired/turbo#113 allowed for multiple targets in turbo frame streams. This attempts to expose the feature to the Rails turbo_stream tag helper. Instead of a `target` which results in a call to `document.getElementById` on the JS level, a `targets` attribute will be passed into `document.querySelectorAll`

This adds the functionality to the tag builder. All "action methods" now have an `_all` varient which is piped into a new `action_all` method. The only real difference in these methods is the end result is setting `targets` instead of `target` in `turbo_stream_action_tag` which has also been updated to handle this feature.

I purposfully did not touch the ActionCable-related part of the gem as I think its usecase is mostly geared to always updating one element at a time. Regardless, if it were to get this feature, I think it would be better in a separate pull request. For now, I think giving the developer the abiltiy to target multiple elements at once in the turbo stream response from a controller is ok for now.
luisjose1996 added a commit to luisjose1996/Turbo-Rails that referenced this pull request May 10, 2024
* Turbo::StreamsChannel can set targets attribute

Background:
- hotwired/turbo#113 allowed for multiple targets in turbo frame streams.
- hotwired/turbo-rails#194 implemented it for `Turbo::Streams::TagBuilder`

This attempts to expose the feature on `Turbo::StreamsChannel` methods.

* Add assertions for `targets` argument
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

Successfully merging this pull request may close these issues.

5 participants