<input type="checkbox">(チェックボックス)を使用します。HTMLのinput要素はもともとOptimistic UIであり、UI操作の結果を直ちに画面に反映させます1。チェックボックスのステートをCSS擬似セレクタで読み取り、表示を変えます。TodoLikesControllerを使用して、「いいね」の数をJavaScriptで更新します。<% highlight = local_assigns.fetch(:highlight, false) %> <tr class="group p-2" id="<%= dom_id(todo) %>"> <td class="<%= 'highlight-on-appear' if highlight %> p-2 border-gray-400 border-t group-[:first-child]:border-none"> <div class="flex"> <div class="flex grow items-center"> <%= render 'like_button', todo: %> <%= todo.title %> <!-- ... --> </div> <!-- ... --> </div> </td> </tr>
<% todo = local_assigns.fetch(:todo) %> <%= form_with id: dom_id(todo, :like_button), url: todo_likes_path(todo), method: :post, class: "flex items-center w-16 aria-busy:opacity-30", data: { controller: "todo-likes", action: "submit->todo-likes#optimistic" } do %> <%= label_tag nil, id: dom_id(todo, :like_button), class: "group flex cursor-pointer select-none" do %> <%= check_box_tag :like, "1", todo.liked_by?(current_user), class: "opacity-0 w-0", data: { action: "change->todo-likes#submit", todo_likes_target: "checkbox" } %> <div class="hidden group-has-[:checked]:block"> <%= liked_icon %> </div> <div class="block group-has-[:checked]:hidden"> <%= unliked_icon %> </div> <% end %> <div> : <span data-todo-likes-target="count"><%= todo.likes_count %></span> </div> <% end %>
checked属性)はCSS擬似セレクタで読み取れますので、周辺の表示も楽観的に変えられます(group-has-[:checked]:block/hiddenの箇所)data-action="submit->todo-likes#optimisticで行います<form>にaria-busyが自動的につくのを利用して、aria-busy:opacity-30で行います。<form>の中に配置された<button>を使用しましたので、クリックイベントを受け取り、サーバにリクエストを投げるのはブラウザネイティブな機能でやってくれました
data-action="change->todo-likes#submitでformの自動送信を行います<form> <button>を使わなくしたためにデータの送信にStimulusが必要になりました。 また「いいね」数の楽観的な更新はStimulusを使う必要がありますimport { Controller } from "@hotwired/stimulus" // Connects to data-controller="todo-likes" export default class extends Controller { static targets = ["count", "checkbox"] connect() { } optimistic(event) { let count = this.countTarget.textContent if (this.checkboxTarget.checked) { count++ } else { count-- } this.countTarget.textContent = count } submit(event) { event.currentTarget.form.requestSubmit() } }
targetsのcountは「いいね」数を表示する場所、checkboxは「いいね」したかどうかのステートを保持するチェックボックスですsubmitとoptimisticの2つがあります
submitはチェックボックスのステートが変更されたらformを自動送信するものです(チェックがついたり、消えたりした時)optimisticは"count" targetの値を楽観的に更新するものですウェブブラウザはネットワークが極めて貧弱だった1990年代中旬に誕生しました。当時は1秒間に数キロバイトしか転送できなかったため、楽観的UI以外は考えられませんでした。<input>や<select>はこの頃からありましたので、楽観的UIです。 ↩