Custom Form Handling With Turbo. Turbo will be a default part of Rails… | by Ollie Haydon-Mulligan | Sep, 2021

Ollie Haydon-Mulligan

Turbo will be a default part of Rails from Rails 7, replacing Turbolinks and rails-ujs. This post is a result of time I spent digging into Turbo, in particular its implications for forms that don’t seem to fit what Turbo is designed for: that is, forms that don’t necessarily (or only) trigger a redirect or DOM changes. I don’t have a definitive answer for what we should do in these cases, but I’ll explain some options that might be useful if or when Turbo’s constraints feel a bit awkward.

Introducing Turbo…

Most of this post is about Turbo Drive, one of the four techniques that together constitute Turbo.

Turbo Drive is the bit that intercepts link clicks and form submissions to avoid full page reloads. It’s the new incarnation of Turbolinks, which has been a default part of Rails apps for a long time. Turbolinks only intercepted link clicks, not form submissions — but now, if you have Turbo installed, a form without any data attributes will automatically be handled and ultimately submitted by Turbo’s javascript. This means form submissions are by default ajax requests, which don’t result in a full page load when the browser gets a response.

So what does happen with the response after Turbo submits a form?

  1. If the response is a redirect, Turbo will follow that redirect, navigating to the new page (without a full page load) as if the user had clicked a link. This is equivalent to the redirect support in Turbolinks-Rails when a form is submitted as an ajax request — in other words, we did have a way pre-Turbo to submit a form and redirect without a full page load.
  2. If the response is and the status is 4XX or 5XX, Turbo will render that (without changing the URL). Turbolinks-Rails didn’t do this. Previously, if a request returned some , nothing would happen without custom javascript to swap that into the page or simulate a Turbolinks visit.
  3. If the response is a ‘Turbo Stream’ response, Turbo will process it… A what? Turbo Streams are a new kind of response. Their content-type header is and they contain one or more Turbo Stream elements, which are custom elements. Turbo automatically appends these elements to the DOM and whenever such an element is added, it triggers DOM changes (such as appending or replacing or removing as specified by the markup in the Turbo Stream element.

Those three alternatives are the only things Turbo is designed to do after a form is submitted:

  1. follow a redirect,
  2. render if the status is 4XX or 5XX, or
  3. process Turbo Streams, which can trigger only a limited range of DOM changes

Doing what Turbo isn’t designed for…

These constraints are deliberate and there’s no reason to debate them. But it is important to understand them and what they mean in practice. If we want to do something Turbo isn’t really designed for, what should we do? What can we do?

I was learning about Turbo soon after implementing a checkout flow in Cookpad using stripe js. It works by creating a Payment Method in Stripe, then submitting the Payment Method’s in a form to our server. If all goes well processing the purchase, the user is redirected to a success page. But the purchase might fail because the user needs to authorise the payment with their bank. In that scenario, our server returns the data needed to call stripe’s confirmCardPayment function. And that function launches the authorisation flow for the user’s bank. [1]

Calling javascript functions using data returned by the server doesn’t feel like one of the Three Things Turbo is designed to do after submitting a form. So as I read about Turbo, I kept asking myself this: what if we need (or want) to do something else? Or, being a bit more specific:

With Turbo set up, (how) can we submit a form then handle the response — in particular an error response — in a custom way, without only redirecting or inserting and/or removing some html?

Option 1…

One option is to use Turbo up to a point, then, at that point, take over from it. Let Turbo submit the request, let Turbo handle a redirect, but prevent Turbo handling the response if, instead of rendering or appending Turbo Stream elements, we want to do “other stuff” like call some javascript functions.

This is doable by listening for the event, emitted on the after the request has been made but before the response has been used.

We can put this stimulus action on a form:

Then define in a stimulus controller:

Now, if the server responds with an error, we can do whatever we want. See how the response doesn’t even have to be .

But there’s a problem. Because the event target is , I couldn’t find a nice way to be sure it corresponds to the correct form on the page. We could check the URL the request was sent to, or we could put a DOM identifier in the response, but neither is ideal. If the target was the element that triggered the request, we could listen for the event on the specific form we want to handle. That would be a convenient way to let Turbo make the request then optionally ‘take over’ when the response is ready. [2]

Option 2a…

Another option is to trigger the ‘other stuff’ (the stuff that isn’t inserting and/or removing by inserting some .

For example, if we want to trigger stripe’s card authorisation flow, we can return a Turbo Stream element that appends a block of that attaches a stimulus controller that triggers the card authorisation flow.

The Turbo Stream element could be rendered like this:

When it’s added to the DOM, it will update the contents of the with an authentication partial.

The authentication partial could look like this:

And the stimulus controller’s connect function could look like this:

I think using a Turbo Stream to insert as a way to do other things – things that could be done without inserting at all – is in line with what the Turbo docs advocate here:

Turbo Streams consciously restricts you to seven actions: append, prepend, (insert) before, (insert) after, replace, update, and remove. If you want to trigger additional behavior when these actions are carried out, you should attach behavior using Stimulus controllers.

Option 2b…

In the above example, the ‘other behaviour’ is triggered when a stimulus controller connects, which happens when an element is added to the DOM. In that sense, the additional behaviour is triggered by the DOM change.

But we could also use a Turbo Stream to trigger behaviour in a more roundabout way: the Turbo Stream could cause a stimulus controller (A) to connect, which could emit an event, which we could listen for in some other stimulus controller (B). Stimulus controller B would then perform the action not because it has just connected, making the resulting behaviour a bit more removed from the thing the Turbo Stream is designed for: making a DOM change.

We could render a Turbo Stream like this:

The ‘pass-error’ stimulus controller could connect like this:

And we could listen for the custom error event in the same way we can listen for events:

This effectively means using a Turbo Stream to simulate the standard way we (at Cookpad) currently handle a response payload. It feels a bit like hacking Turbo Streams to let us handle non- responses, and isn’t really in the spirit of Turbo… but it could be useful, especially if you want to switch to Turbo but continue acting on events similar to .

Option 3…

Finally, even with Turbo installed (and Turbolinks removed), we don’t have to use it. We can disable Turbo on an individual form by adding a attribute. This will result in a standard non-ajax form submission. Or we can add a attribute to the form. As long as we still have installed, the ‘data-remote’ attribute will stop Turbo handling the submission because will intercept it first.

This is definitely a way to have Turbo set up while handling a form response in ways Turbo isn’t designed for. Submit the form with instead and act on the events it emits to do whatever needs to be done. Great.

Except that by default we then lose the option of responding to the submission with a redirect. Without Turbolinks-Rails installed, if you try to redirect in response to a form submission, nothing will happen…

What we need for to be viable in a non-Turbolinks setup is a way to redirect with Turbo when a non-Turbo ajax form is submitted.

And here it is, in the Turbo docs. A Turbo version of the Turbolinks-Rails redirect_to method. Drop this into your , and you can redirect with Turbo even when Turbo didn’t submit the form.


I don’t know how many others are or will be asking themselves the question I found myself asking, and I haven’t found a definitive answer to that question anyway… But hopefully I have explained a few approaches that might help as we adapt to Rails without Turbolinks and without .

I’ll finish with a bit of practical advice, because something that has become clear as I’ve tried out these approaches is a way to make the leap to Turbo a bit calmer and more gradual.

If your existing application submits forms, there’s no need to rewrite them all straight away. Let continue intercepting the submissions. Let it continue emitting the convenient and hooks. Start by letting Turbo take over the other forms: Turbo will seamlessly [3] turn them into ajax submissions and handle them without a full page load. Then consider each ‘remote’ form individually, either removing and refactoring to deliver the necessary behaviour with Turbo, or keeping , or using neither nor Turbo.

Thanks for reading, and feel free to get in touch with me @olliedoodleday. 👋

Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here