Struggling with Turbo coming from HTMX

I have used htmx before and want to test Turbo now. I think my mindset is still rooted in htmx, which is way Turbo is not working for me and I need some help.

In htmx if I want to swap a page element, I respond to a frontend request with just the new element and the id of what to replace, nothing more i.e. not a full page. I’m trying to achieve the same behavior with Turbo but failing. I’ve seen people describe using Streams (e.g. Form does not update turbo-frame - #2 by remast) in the way I want.

I’m sending a POST request with a form (which is not inside a turbo frame, as per the post above):

  • text/vnd.turbo-stream.html is not in the accept headers (Turbo Handbook says it should)
  • I respond with (see below) and add the MIME type from the previous point
<turbo-stream action="replace" target="uuid-form-body">
    <template>
        <h1>UPDATED</h1>
    </template>
</turbo-stream>
  • I’m redirected to the form’s action url and see a sea of nothingness instead of, at least, “UPDATED”

What am I doing wrong? Should I use a frame instead and if so do I always need to respond with the full page so that Turbo diffs it?

NOTE: I am using Clojure in my project, not Ruby.

Seems like the turbo library might not be loaded. If you put a hyperlink on the page, does a click result in a fetch request? (it should with Turbo working)

you are right, there are no fetch requests that I can see, just normal GETs.
interestingly there is an error in my console

Uncaught SyntaxError: Unexpected token 'export' turbo.js:6

I was getting turbo from

 https://cdn.skypack.dev/pin/@hotwired/turbo@v7.0.0-beta.8-3sRZNgOTAvJtHRkcbT7v/mode=imports,min/optimized/@hotwired/turbo.js

which is not what I expected. Now I’m importing turbo correctly and I see fetch requests when I click on links, but responding with the stream in the original post results in the same white screen (I still don’t see text/vnd.turbo-stream.html in the headers)

It would help to show the actual form html along with the response stream html.

Here you go
form:

  <div class="w-96" id="uuid-input-body"> 
   <form action="/invoices" id="uuid-input-form" method="POST"> 
    <textarea class="block w-full font-mono md:text-center text-xs md:text-sm text-gray-300 px-4 py-3 rounded-sm shadow bg-gray-700 placeholder-gray-500 focus:outline-none focus:ring focus:border-indigo-300" id="uuid-input-textarea" name="uuids" onchange="this.form.submit()" placeholder="Give me UUID(s)" rows="11" type="text" value=""></textarea> 
   </form> 
  </div>

response from /invoices

<turbo-stream action="replace" target="uuid-input-form">
    <template>
        <div>
            <h1>FOOOOOO</h1>
        </div>
    </template>
</turbo-stream>

I believe the issue with this code is onchange="this.form.submit()". Using this to submit may circumvent turbo.

To confirm this as the issue, try adding a standard <input type="submit"> button in the form and see if clicking the submit button works.

1 Like

@tleish yeah that was it, it works now

  1. POST form
  2. respond with a stream partial containing a frame and src (spinner)
  3. respond to 2. with the data

very nice, this way I’m not keeping track of any state.
I’ll try requestSubmit() with another browser, I was using submit() because Safari doesn’t support the requestSubmit() and I really wanted to have no button in this form

Glad to see that there was some help and this was figured out. As someone who uses Clojure whenever I can, I’d be interested in hearing at some point how your experience using Hotwire with a Clojure backend (or with Clojurescript) so if you ever have the time please post an experience report from your test.

One workaround, you could add a hidden submit button and use javascript to “click” the submit button. This should trigger the turbo submit.

You might also be interested in this thread:

No problem! I don’t know Javascript at all, so using things like htmx and turbo lets me be as fast as (or faster) than my colleagues worrying about the latest JS framework. I’ll write it up once I have time. It’s a smooth experience.

@tleish one last question about htmx vs turbo. I was relying a lot on htmx’s hx-swap-oob to “piggyback” multiple updates on a single request e.g. swapping multiple elements for spinners and then again with the final results. To achieve this with Turbo my understanding is I’ll have to setup websockets. Is that correct?

Assuming you are referring to turbo-streams, they are not exclusive to websockets. turbo-streams can be used with http requests or websockets, depending on the need. If you want to update users DOM elements as part of an HTTP request, just return HTML with turbo-stream element(s). If you want to update users DOM outside an HTTP request (delated job, another user updated data, etc), then you must use websockets with turbo-stream element(s).

So if the server responds to request with

<turbo-stream action="replace" targets="some_id">
  <template>
     <!-- Some new element !-->
  </template>
</turbo-stream>
<turbo-stream action="replace" targets="some_other_id">
  <template>
    <!-- Some other new element !-->
  </template>
</turbo-stream>

it will update both targets? the documentation is a bit ambiguous in regards to this?

My use-case is to update two tables far away from each other after a single user action.

I know the documentation can be a bit confusing around this. To clarify:

target

Targets single element with by ID with a single action.

<turbo-stream action="replace" target="some_id">

targets (added to 7.0.0-beta.8)

Target multiple elements with by CSS query selector with a single action.

<turbo-stream action="replace" targets="[data-value='foo']">

It does not make sense to use targets for a single id (e.g. targets='#some_id'), use target instead

Multiple turbo-streams

You can include multiple turbo-streams in a single response. Turbo will process each one of them.

<turbo-stream action="replace" target="some_id">
  <template>
     <!-- Some new element -->
  </template>
</turbo-stream>

<turbo-stream action="after" target="some_other_id">
  <template>
    <!-- another new element -->
  </template>
</turbo-stream>

<turbo-stream action="replace" targets="[data-value='foo']">
  <template>
    <!-- shared content between multiple elements -->
  </template>
</turbo-stream>