admin管理员组

文章数量:1220279

I'm starting to use Hotwire in my Rails app and I implemented something similar to the tutorial video where a form gets submitted and refresh another part of the page. Like in the video, I had to implement my own reset form controller:

import {Controller} from "stimulus"

export default class extends Controller {
  reset() {
    this.element.reset()
  }
}

to reset the values in the form. The form looks something like:

<%= form_with(model: @invitation,
              data: {controller: "reset-form",
                     action: "turbo:submit-end->reset-form#reset"}) do |form| %>
  <!-- other form elements -->
  <%= form.submit "Invite" %>
<% end %>

which generates in button like this:

<input type="submit" name="commit" value="Invite" data-disable-with="Invite">

Because there's a data-disable-with, when you press the button, it gets disabled to avoid double-click submissions, which is great. The problem is that this.element.reset() doesn't reset it.

What's the proper way of dealing with this?

I'm not searching for a workaround, I know many, but I'm searching for the clean solution to this problem.

Is this disabling of a button caused by UJS? does it mean UJS shouldn't be used in Stimulus application?

I can re-enable the button from the reset JavaScript function but if the input button is specified like this:

<%= form.submit "Invite", data: {disable_with: "Inviting, please wait..."} %>

then the original label (value) of the button is lost and I don't have a way to re-establish it, which makes me think whatever is implementing this functionality (UJS?) it's not designed for hotwire/spa applications, and expects a full reload of the page.

I could just throw:

config.action_view.automatically_disable_submit_tag = false

and implement my own double-click prevention with a Stimulus controller, but that feels wrong. The problem is not with the attribute data-disable-with but how it was implemented.

I'm starting to use Hotwire in my Rails app and I implemented something similar to the tutorial video where a form gets submitted and refresh another part of the page. Like in the video, I had to implement my own reset form controller:

import {Controller} from "stimulus"

export default class extends Controller {
  reset() {
    this.element.reset()
  }
}

to reset the values in the form. The form looks something like:

<%= form_with(model: @invitation,
              data: {controller: "reset-form",
                     action: "turbo:submit-end->reset-form#reset"}) do |form| %>
  <!-- other form elements -->
  <%= form.submit "Invite" %>
<% end %>

which generates in button like this:

<input type="submit" name="commit" value="Invite" data-disable-with="Invite">

Because there's a data-disable-with, when you press the button, it gets disabled to avoid double-click submissions, which is great. The problem is that this.element.reset() doesn't reset it.

What's the proper way of dealing with this?

I'm not searching for a workaround, I know many, but I'm searching for the clean solution to this problem.

Is this disabling of a button caused by UJS? does it mean UJS shouldn't be used in Stimulus application?

I can re-enable the button from the reset JavaScript function but if the input button is specified like this:

<%= form.submit "Invite", data: {disable_with: "Inviting, please wait..."} %>

then the original label (value) of the button is lost and I don't have a way to re-establish it, which makes me think whatever is implementing this functionality (UJS?) it's not designed for hotwire/spa applications, and expects a full reload of the page.

I could just throw:

config.action_view.automatically_disable_submit_tag = false

and implement my own double-click prevention with a Stimulus controller, but that feels wrong. The problem is not with the attribute data-disable-with but how it was implemented.

Share Improve this question edited Apr 13, 2021 at 12:25 Sebastián Palma 33.4k6 gold badges44 silver badges63 bronze badges asked Mar 21, 2021 at 15:14 Pablo FernandezPablo Fernandez 287k139 gold badges401 silver badges642 bronze badges 1
  • could you just replace the whole form in your controller's turbo-stream response? I suppose you would add a new element to the page via turbo-stream[append] and could turbo-stream[replace] the form which a "fresh" one. This way, you could also remove the stimulus controller. But in this solution the cursor position would be lost (bad for a chat like application) – stwienert Commented Apr 17, 2021 at 20:28
Add a comment  | 

2 Answers 2

Reset to default 13

Turbo has been disabling the submit button for a while now and more recently added data-turbo-submits-with as alternative to UJS data-disable-with:

# available since: turbo v7.3.0 (turbo-rails v1.4.0) thx @PaulOdeon
<%= form_with model: @invitation do |form| %>
  <%= form.submit "Invite", data: {turbo_submits_with: "Inviting..."} %>
<% end %>

Resetting the form is optional:

<%= form_with model: @invitation, data: {
  controller: "reset-form",
  action: "turbo:submit-end->reset-form#reset"
} do |form| %>
  <%= form.submit "Invite", data: {turbo_submits_with: "Inviting..."} %>
<% end %>
# NOTE: form reset is implemented as stimulus controller in the question

https://turbo.hotwired.dev/reference/attributes


Yes, you should just throw this in:

config.action_view.automatically_disable_submit_tag = false

it only adds data-disable-with attribute, which is UJS specific and doesn't affect Turbo.


However, it's not like it wasn't doable before. This is a simplified example if you want to implement your own custom behavior. To make a clear distinction here, I'm using non-turbo and non-ujs attributes (Turbo only sets and removes disabled attribute for us):

<%= form_with model: @invitation, data: {reset: true} do |form| %>
  <%= form.submit "Invite", data: {disable_append: "..."} %>
<% end %>

For simple, one shot actions you can skip Stimulus and only use Turbo events:

// app/javascript/application.js

document.addEventListener('turbo:submit-start', (event) => {
  const { detail: { formSubmission, formSubmission: { submitter } } } = event
  // disable-append or maybe disable-loading and make it spin
  if (submitter.dataset.disableAppend) {
    // TODO: handle <button> element and use innerHTML instead of value.
    // save original text
    formSubmission.originalText = submitter.value
    submitter.value += submitter.dataset.disableAppend
  }
})

document.addEventListener('turbo:submit-end', (event) => {
  const { detail: { formSubmission, formSubmission: { formElement, submitter } } } = event
  // put it back (formSubmission is the same object during form submission lifecycle)
  if (formSubmission.originalText) {
    submitter.value = formSubmission.originalText
  }
  // data-reset
  if (formElement.dataset.reset === 'true') {
    formElement.reset()
  }
})

That's pretty much how it's actually done in turbo now:
https://github.com/hotwired/turbo/blob/v7.3.0/src/core/drive/form_submission.ts#L217-L239

The most "Hotwire way" of replacing HTML content on the page upon a user action is to use a turbo stream response.

If I understand correctly, you want to return the form (or just the button) back to its original state after form submission.

You would wrap your form with a turbo frame, like so

<%= turbo_frame_tag dom_id(@invitation) do %>
  <%= form_with(...) %>
  <% end %>
<% end %>

Then, like @stwienert said, have your controller return a format.turbo_stream response that renders the partial/template for your form. Here is an example of a turbo_stream response adapted from the Turbo docs:

format.turbo_stream do
  render turbo_stream: turbo_stream.replace(@invitation, partial: "your/form",
    locals: { invitation: @invitation }) # or Invitation.new, perhaps
end

The goal would be for your controller response to target the DOM id of your form, such that on form submit, your data is handled appropriately and then the client receives a turbo stream response containing exactly the HTML you want to see inside your turbo_frame tag.

If you only want to replace the button, wrap only the button in the turbo_frame, give it a unique DOM id, and pass that ID to turbo_stream.replace/append/etc, e.g. turbo_stream.replace(:button_dom_id_here, partial: ...)

本文标签: javascriptWhat39s the hotwire way of dealing with datadisablewithStack Overflow