2

The following _receipt_number partial (HTML formatting tags removed for brevity's sake)

<%= turbo_frame_tag :receipt_number do %>
      <%= t('scan') %> <%= t('cart.receipt_number') %>
      <h3><%= @cart.receipt_number %></h3>
      <% if @cart.receipt_number %>
        <%= button_to t('change'), clear_receipt_number_cart_path(id: @cart.id), class: 'button warning', method: :patch %>       
      <% end %>
<% end %>

is rendered in HTML as:

<turbo-frame id="receipt_number">
      scan receipt number
      <h3></h3>
</turbo-frame>

Via a javascript that controls the device camera a scan is run and is processed (this is the relevant javascript code for the instance)

let formData = new FormData();
let CodeParams = {
  code_data: result.text,
  id: <%= @cart.id %>
};
formData.append("code_json_data", JSON.stringify(CodeParams));
fetch("update_receipt_number", {
  method: "post", 
  format: "turbo_stream",
  body: formData,
  headers: {
    "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
  }
});
codeReader.reset();

via the controller action with:

@cart.update(receipt_number: result['code_data'])
respond_to do |format|
  format.turbo_stream 
end

the log shows the action processing, however without a format pre-defined for the contorller and rendering required objects:

Started POST "/carts/6/update_receipt_number" for 127.0.0.1 at 2023-02-27 08:24:38 +0100
Processing by CartsController#update_receipt_number as */*


  Cart Update (9.6ms)  UPDATE "carts" SET "updated_at" = $1, "receipt_number" = $2 WHERE "carts"."id" = $3
 Rendering carts/update_receipt_number.turbo_stream.erb
  Rendered carts/_receipt_number.html.erb (Duration: 0.8ms | Allocations: 349)
  Rendered carts/update_receipt_number.turbo_stream.erb (Duration: 1.9ms | Allocations: 663)

The turbo_stream.erb response is set as:

<%= turbo_stream.replace :receipt_number do %>
  # replacing the partial with turbo
  <%= render partial: 'carts/receipt_number', locals: {cart: @cart} %>
<% end %>

and the console advises that the HTML is received over the wire XHRPOSThttp://localhost:3000/carts/6/update_receipt_number [HTTP/1.1 200 OK 357ms] with the following payload

<turbo-stream action="replace" target="receipt_number"><template>
    # replacing the partial with turbo
    <turbo-frame id="receipt_number">
      scan receipt number
<h3>20230224T12423036270004131601843bbaa16909b1b07b2c32414fdea5092de2769aabaa2f610860c827f27fb386d4</h3>
        <form class="button_to" method="post" action="/carts/6/clear_receipt_number"><input type="hidden" name="_method" value="patch" autocomplete="off" /><button class="button warning" type="submit">change</button><input type="hidden" name="authenticity_token" value="DKVdhPK4UfmY_PzvA2amrvh9IejYEnyjHrJQANs0rlj6HHNAZjk3n6zxAgHRg0s27SWwgPZxMXOuLDiqSn6oQw" autocomplete="off" /></form>       
</turbo-frame>
</template></turbo-stream>  

Yet the browser window does not update.

I do not believe the controller's absence of request type is at play, as the response arrives to the browser. So why is the browser not refreshing the turbo_frame? the same was attempted with a div in lieu of the turbo_frame, leading to identical behaviour

What am I missing?

Jerome
  • 5,583
  • 3
  • 33
  • 76
  • 1
    Your expecations here are fundamentally wrong. The browser doesn't magically update the DOM with the returned HTML. That is whats done by the event handlers that Turbo creates and you're bypassing them completely. – max Feb 27 '23 at 08:49
  • I realise that, as the same pattern with rails-ujs runs as expected. Thus, it is my assumption that the `fetch` block is mistaken , the `format` appears inert. It should somehow pass `data-turbolinks='false'` ? – Jerome Feb 27 '23 at 09:05
  • You're misstaken. Even with Rails UJS nothing would happen if you manually send an ajax request to the endpoint requesting a `js.erb` template. Its only actually when its sent through Rails UJS and it takes the response and "evals" it by popping it into a script tag that anything happens. – max Feb 27 '23 at 09:12
  • I'm not sure how you can trigger the right turbo events programatically from JS. But as a workaround you could consider adding a form element to the page (with button_to) that targets your turbo frame inside of a ` – max Feb 27 '23 at 09:17

1 Answers1

3

Since you're issuing fetch request yourself, you have to handle the response yourself as well. In other words, you have called a method and now you have to do something with the returned value.

Normally turbo would handle the nitty gritty details, but thankfully, it does expose an api for rendering a stream manually:

fetch("update_receipt_number", {
  method: "post",
  // format: "turbo_stream", // <= i don't think, this is a thing
  body: formData,
  headers: {
    "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
    // set Accept header to issue a proper TURBO_STREAM request
    "Accept": "text/vnd.turbo-stream.html"
  }
})
.then(response => response.text())
.then(text => Turbo.renderStreamMessage(text));

https://turbo.hotwired.dev/reference/streams#processing-stream-elements

https://developer.mozilla.org/en-US/docs/Web/API/Response/text

Alex
  • 16,409
  • 6
  • 40
  • 56
  • 1
    Brilliant answer. You're right about the `format:` option. It doesn't actually do anything. You set the format with the accept and content-type headers. The valid options are listed here https://developer.mozilla.org/en-US/docs/Web/API/fetch – max Feb 27 '23 at 23:19