Hotwire Discussion

Destroy record in turbo-frame

Hi there,
I am using a turbo-frame to render a small edit view within a popover.
It works pretty well, to handle the update of it (including the redirect), as well as displaying possible errors within the form.

What doesn’t work is, when I tried to add a delete button within that popover->turbo-frame.
The delete button is just a rails_ujs link with data-method="destroy" property.

This is what happens: The deletion request itself works well. Afterwards the controller redirects to the index action, what is valid behaviour. But the redirect isn’t performed within the turbo-frame (as it is in the create&update case), but within the whole application.

I also tried to influence that behaviour using the target option in tag, but without any impact.

Of course it seems, that rails_ujs doesnt work properly with turbo - as I can see, that turbo is actually performing the redirection within its FormSubmitObserver .
And for me it seems, that rails_ujs might not be the best way of doing this anymore… is it?

So, if I’m not doing any detail wrong in my implementation, I am actually wondering, what would be the correct (new) way to submit destroy-method-requests in links, like earlier with rails_ujs, but respecting the new Turbo infrastructure?

Thanks for your help!

You’ll want to submit a form rather than click a link. The button_to method will be your friend.

If you’re rendering that link with link_to, the change will be dead easy:

# You’ll change something like this...

<%= link_to "Delete", post, data: { method: :delete } %>

# to this...

<%= button_to "Delete", post, method: :delete %>

In hindsight, it was probably a bad idea for rails ujs to hijack links to make non-GET requests. It works against the grain of HTML. A link doesn’t make something happen, it takes you somewhere. HTML gives you a separate tool to make something happen, which is a form. (See https://twitter.com/sstephenson/status/1341735693077327874 for where I originally saw this thought expressed.)

7 Likes

gr8, thanks alot, works!

Would mind going into a bit more detail as to why the <%= link_to "Delete", post, data: { method: :delete } %> is problematic here?

Many thanks for the work around!

1 Like

As far as I remember, the main issue was, that turbo followed the link (even if it shouldn’t). But it’s not really turbo’s fault.

So if you declare the link like:

= link_to xy_path, data: { turbo: false, method: :post }

ujs library will grab it (and turbo ignores the link). But ujs creates a hidden form to perform the post, and doesn’t transfer the links’ properties (like data-turbo=“false”) to the form.

That’s why turbo gets active then and performs the form submit, and this leads to the following issues. I didn’t patch it, so I’m not sure, if that’s the only issue, but this is what I noticed when I created the topic.

So the workaround works, but in my opinion it’s not very good, because form-in-form is an unsupported/invalid markup. But that’s what happens, if you render button_to within another forms’ context. So whenever you want to render the destroy button within the area, where the form is visible, you need to hassle with a complex markup- and style structure.

Many will probably argue, that this won’t happen, if the code and ui/ux is clean… however, I’m sure, it will happen in many projects.

Hope that answers your question?

Best regards
Philipp

2 Likes

Thanks for the explanation!

@julian from a more conceptual point of view…the answer is it’s just what the web wants us to do. HTML doesn’t natively give us a way for a link to make a POST request, because that’s not what a link is for! The HTML standard says a link is for navigation. When you navigate around the web, you make a series of GET requests. Navigation doesn’t imply you want to do anything while there. Per the HTML standard: “These are links to other resources that are generally exposed…so that the user can cause the user agent to navigate to those resources, e.g. to visit them in a browser or download them.” And that’s why a link will always make a GET request. A GET request is a visit, it says “show me this” and it’s idempotent. When you make the same request it’ll show the same thing.

I think it’s actually semantically incorrect for a link to be anything other than a GET. That’d imply a side-effect (and that, of course, is exactly what you want ujs to do). HTML’s answer for making a request to do something (cause a side effect) is a form.

Now, I understand someone might think this is all a bit academic. We style links to look buttons and sometimes buttons to look like links. It is academic. But it’s analgous to believing, say, a query method in Ruby — def valid? or something else that ends in a ? — shouldn’t have side effects.

@pgr Presumably you can’t do this:

<%= form_with model: @model do |form| %>
<%# some form %>
<% end %>

<%= button_to "Delete", @model, method: :delete %>

(maybe with a wrapper <div> or something)? Is that because your button is somewhere in the midde of the form?

If so, you could try putting formmethod and formaction attributes on a submit input inside your form. Those will override the action and method on the parent form, but only when it’s submitted using that input

1 Like

@dan thanks for the explanation. I indeed agreed with the (probably academic) approach how it is described above in the twitter link, and I still think, it’s not a turbo issue (but the problem started to appear with it :slight_smile: ).

When I agree with the “HTML intention” to not provide navigational-links with different (non-GET) methods, that’s the one thing. The other is, how to get those writing things done (in an easy, replicable way).
There are tons of ways, which brings me to the following options:

  1. You can use forms to permit the request, what might be the most html-like way (as you pointed out).
  2. You could handle the clicks on any element, as you like, within your own connected stimulus controllers (or other js library) and perform the requests “manually” in JS.
  3. You can write a more generic library, which handles it for you for recurring situations, if you want to get the same behaviour, without creating a stimulus (-like) controller for each separate situation.
    The alternative to this is to use rails_ujs, as it did exactly that for you. But now it seems to stay in conflict with turbo. And that’s why I mentioned that I guess, it might happen in many projects, as rails_ujs is there for (I am just guessing) a decade now (but anyways used in many projects, I assume).

Your mentioned option of using formmethod and formaction sounds as a good try/option. I didn’t ran in the situation right now, but will try it, if I am facing it. BUT I am a bit sceptical, if it works, as rails uses the _method attribute (as hidden input) especially for “non-native” methods like destroy? (at least in the referred docs only get and post is pointed out to be valid).

Please don’t get me wrong, I did not try to convince anyone from something.

I just was happy, that there is a workaround for my situation, and a bit sceptical at the same time, if that workaround will work for most situations).

This PR might be of interest! Turn links with methods into form submissions by dhh · Pull Request #277 · hotwired/turbo · GitHub

So the workaround works, but in my opinion it’s not very good , because form-in-form is an unsupported/invalid markup. But that’s what happens, if you render button_to within another forms’ context. So whenever you want to render the destroy button within the area, where the form is visible, you need to hassle with a complex markup- and style structure.

The other challenge I’ve found with using a button instead of a link styling. A button is more challenging to style than a link. There’s some additional padding or margins that are difficult to remove.

The simplest solution I’ve found that solves both the “form in form” issue and the styling issue is to simply use a label.

<%= form_with model: @model do |form| %>
  <%# some form %>
  <%= label('delete-me', "Delete") %>
<% end %>

<%= button_to "Delete", @model, method: :delete, id: 'delete-me', hidden: true %>

This creates 2 forms with the 2nd form hidden. The 2nd form is then executed by clicking on the label embedded in the first form.

Hi @dan,

thanks for the information about the PR. It seems like this could be the missing part - nearly

The problems with form-in-form will still exist, when I understand that correctly, won’t it?

The link is turned into a form that’s added next to the link in the dom, …

However I really appreciate the improvement in that direction (and am sure that is pretty important for many others).

Thanks for that workaround @tleish!

To be honest, I’m not feeling, like this makes the markup “clean” or “good” for just following to “do what html wants you to do” - but yeah, it’s a solution to the form-in-form problem :slight_smile:

Thanks alot for so much contribution!

Has anyone been able to get this new feature referenced above to work? I’ve tried using:

link_to 'Whatever', my_path, data: { turbo_method: 'delete' }

It does seem to create a form right when the link is clicked, but always sends a GET request, not a DELETE to the server.

1 Like

[quote=“kikikiblue, post:15, topic:2731, full:true”]
After updating hotwire-After updating hotwire-rails gem, I tried it and was successful inside turbo_frame_tag.

I did not tried globally(without turbo_frame_tag), but It should work with the PR

Same! ........

Cool. button_to works for me.