1

I want to click an SVG icon, trigger a Stimulus action which updates my model's (trx) "cleared" attribute on the backend, and then update the view to show a different SVG icon indicating the trx is cleared, without a page refresh.

Initially, my view partial displays the correct SVG depending on the model's "cleared" value (... if trx.cleared?).
app/views/trxes/_trx.html.erb

<div id="<%= dom_id trx %>" data-controller="trx-toggle" data-trx-toggle-url-value="<%= trx_path(trx) %>" data-trx-toggle-cleared-value="<%= trx.cleared %>">
...
  <div>
    <% if trx.cleared? %>
      <%= inline_svg_tag "icons/cleared_closed.svg", class: "h-6 w-6 fill-green-500", aria_hidden: true, data: { action: "click->trx-toggle#toggleCleared", trx_toggle_target: "cleared"} %>
    <% else %>
      <%= inline_svg_tag "icons/cleared_open.svg", class: "h-6 w-6 fill-green-500", aria_hidden: true, data: { action: "click->trx-toggle#toggleCleared", trx_toggle_target: "cleared" } %>
    <% end %>
  </div>
...
</div>

Clicking the icon correctly triggers this Stimulus action which sends the opposite boolean value in the params
trx_toggle_controller.js

...
toggleCleared() {
    let formData = new FormData()
    formData.append("trx[cleared]", !this.clearedValue) // "true" or "false"
    const token = document.getElementsByName("csrf-token")[0].content;
    fetch(this.urlValue,{
      body:formData,
      method: 'PATCH',
      dataType: 'script',
      credentials: 'include',
      headers: {
        "X-CSRF-Token": token,
        "Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
      },
    })
  }
...

Which appears to send the correct params, and correctly updates the record (log output)

Started PATCH "/trxes/123" for 127.0.0.1 at 2023-04-07 22:31:38 -0700
Processing by TrxesController#update as TURBO_STREAM
  Parameters: {"trx"=>{"cleared"=>"true"}, "id"=>"123"}
  ...
  Trx Update (0.3ms)  UPDATE "trxes" SET "updated_at" = ?, "cleared" = ? WHERE "trxes"."id" = ?  [["updated_at", "2023-04-08 05:31:38.761532"], ["cleared", 1], ["id", 123]]
  ↳ app/controllers/trxes_controller.rb:45:in `block in update'
  ...
  Rendered trxes/_trx.html.erb (Duration: 7.4ms | Allocations: 2233)
Completed 200 OK in 24ms (Views: 0.2ms | ActiveRecord: 5.2ms | Allocations: 8649)

app/controllers/trxes_controller.rb

def update
  trx(new_trx_params)
  respond_to do |format|
    if trx.save!
      format.turbo_stream { render turbo_stream: turbo_stream.replace(
        helpers.dom_id(trx),
        partial: 'trx',
        locals: { trx: trx }
      ) }
      format.html { render_to_string(partial: 'trx', locals: {trx:trx }) }
    else
      trx.errors.full_messages.each do |e|
        puts "DEBUG Trx Error: #{e}"
      end
      render :new
    end
  end
end

The record updates, and a new JS request is visible on the NETWORK tab of my browser but I need to refresh the page to see the updated icon.
I have a slideover form for editing the entire trx record, and it works without a page refresh. The only difference I see is in the NETWORK tab: it shows a POST request initiated by turbo.min... with CONTENT-TYPE: application/x-www-form-urlencoded;charset=UTF-8. Whereas my stimulus controller request is PATCH initiated by trx_toggle_controller... and CONTENT-TYPE: multipart/form-data; boundary=---------------------------419727981959891446676073942

chug
  • 71
  • 6
  • 1
    you can just make a button or link with whatever parameters you need. you're only duplicating the work of Turbo with this fetch request. you can add some extra things like loading animation with stimulus, but let Turbo send the request and handle the response. but if you insist on making a fetch request yourself: https://stackoverflow.com/a/75579188/207090 – Alex Apr 08 '23 at 07:13
  • Thank you for both options! As mentioned in your answer to the linked question, I didn't realize I needed to handle the fetch response myself. Adding the following to the fetch() makes things work .then(response => response.text()) .then(text => Turbo.renderStreamMessage(text)); I need a Stimulus controller in order to update a "cleared amount" counter on another part of the page, but once I untangle my code I'll look to use Turbo all by itself. – chug Apr 08 '23 at 15:37

1 Answers1

0

I'll just show a simple example, maybe it will give you some new ideas on how to avoid doing manual fetch request, a lot of the times it's not needed:

// app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["item"];

  // do something when new item shows up
  // no manual fetching
  itemTargetConnected(target) {
    document.querySelector("#item-updater").innerHTML = this.itemTargets.length;
  }
}
# app/controllers/items_controller.rb

def create
  render turbo_stream: turbo_stream.append(
    "items", '<div data-hello-target="item">1</div>'
  )
end
<!-- app/views/home/index.html.erb -->

<div id="item-updater"></div>

<div id="items" data-controller="hello">
</div>

<%= button_to "create", "/items" %>

https://stimulus.hotwired.dev/reference/targets#connected-and-disconnected-callbacks


You can also render multiple turbo streams:


def create
  render turbo_stream: [
    turbo_stream.append("items", '<div data-hello-target="item">1</div>'),
    turbo_stream.before("item-updater", '<p>updated</p>')
  ]
end
Alex
  • 16,409
  • 6
  • 40
  • 56