From 55ea460d3e0c4dc598d4189f71ae51dac685e20a Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 7 May 2025 08:46:47 +0200 Subject: [PATCH] Handle form errors with a stimulus class We need to keep the focus in place when submitting the form. We are using a data-turbo-permanent for that. We were rendering the error form with a turbo stream. The problem was that turbo streams ignore permanent elements as per this https://github.com/hotwired/turbo/pull/688, so the system wasn't updating the input field as expected. Handling this as the stimulus level will also help with managing additional error states (such as the one we need for confirmations). --- app/controllers/commands_controller.rb | 2 +- app/javascript/controllers/commands_controller.js | 15 +++++++++++++++ app/javascript/controllers/form_controller.js | 4 ---- app/views/commands/_form.html.erb | 12 +++++++----- app/views/commands/_terminal.html.erb | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/controllers/commands_controller.rb b/app/controllers/commands_controller.rb index 270e63dd9..c70750bfc 100644 --- a/app/controllers/commands_controller.rb +++ b/app/controllers/commands_controller.rb @@ -16,7 +16,7 @@ class CommandsController < ApplicationController redirect_back_or_to root_path end else - render turbo_stream: turbo_stream.replace("commands_form", partial: "commands/form", locals: { error: true }) + head :unprocessable_entity end end diff --git a/app/javascript/controllers/commands_controller.js b/app/javascript/controllers/commands_controller.js index e51177f43..177aa5a23 100644 --- a/app/javascript/controllers/commands_controller.js +++ b/app/javascript/controllers/commands_controller.js @@ -2,10 +2,25 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "input" ] + static classes = [ "error" ] // Actions focus() { this.inputTarget.focus() } + + handleSubmit(event) { + if (event.detail.success) { + event.target.reset() + } else { + this.#handleErrorResponse(event.detail.fetchResponse.response.status) + } + } + + #handleErrorResponse(code) { + if (code == 422) { + this.element.classList.add(this.errorClass) + } + } } diff --git a/app/javascript/controllers/form_controller.js b/app/javascript/controllers/form_controller.js index cb0ca9370..d291de2bb 100644 --- a/app/javascript/controllers/form_controller.js +++ b/app/javascript/controllers/form_controller.js @@ -19,10 +19,6 @@ export default class extends Controller { event.target.select() } - reset(event) { - this.element.reset() - } - showPicker(event) { if ("showPicker" in HTMLInputElement.prototype) { event.target.showPicker() diff --git a/app/views/commands/_form.html.erb b/app/views/commands/_form.html.erb index b8ad7401e..518842c1c 100644 --- a/app/views/commands/_form.html.erb +++ b/app/views/commands/_form.html.erb @@ -1,12 +1,14 @@ -<% has_error = local_assigns.fetch(:error, false) %> - <%= form_tag commands_path, id: "commands_form", - class: [ "flex align-center gap-half", { "has-error" => has_error } ], - data: { controller: "form", action: "turbo:submit-end->form#reset turbo:submit-end->commands#focus keydown.meta+k@document->commands#focus" } do %> + class: [ "flex align-center gap-half" ], + data: { + controller: "form commands", + commands_error_class: "has-error", + action: "turbo:submit-end->commands#focus keydown.meta+k@document->commands#focus turbo:submit-end->commands#handleSubmit" + } do %> - <%= text_field_tag "command", has_error ? params[:command] : nil, + <%= text_field_tag "command", nil, autocomplete: "off", autocorrect: "off", autocapitalize: "off", diff --git a/app/views/commands/_terminal.html.erb b/app/views/commands/_terminal.html.erb index f90a92bce..17ecd559d 100644 --- a/app/views/commands/_terminal.html.erb +++ b/app/views/commands/_terminal.html.erb @@ -1,5 +1,5 @@ <%= tag.div class: "terminal full-width flex flex-column gap justify-end", data: { - controller: "toggle-class commands", + controller: "toggle-class", toggle_class_toggle_class: "terminal--open" } do %> <%= turbo_frame_tag :recent_commands, src: commands_path, refresh: "morph" %>