Live form validations and error rendering. Live markdown preview
Here’s how you can get character count, error validation, and makdown preview while you type, without page refresh, without extra JS

The trick: #
- let the form have a second submit button with a
different URL - submit the
different URL - the
different URLwill respond with a turbo_stream
HOWTO: #
rails generate scaffold Message content:text
rails g stimulus form
# app/models/message.rb
validates :content, presence: true
validates :content, length: { in: 5..1000 }
- div id message_preview
- hidden button with formatction to a different url
- submit the hidden button oninput (with a stimulus controller)
# app/views/messages/_form.html.erb
<%= form_with(model: message, data: { controller: "form", action: "input->form#remotesubmit" }) do |form| %>
<div>
<%= form.label :content, style: "display: block" %>
<%= form.text_area :content %>
</div>
<div id="message_preview">
<%= markdown message.content %>
</div>
<div>
<%= form.button "Preview Message", formaction: preview_messages_path, name: "_method", value: "post", data: { form_target: "submitbtn" } %>
<%= form.submit %>
</div>
<% end %>
The Stimulus controller to:
- hide the second
submitbutton - autosubmit whenever there are any changes
// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["submitbtn"]
// hide the submit button
connect() {
this.submitbtnTarget.hidden = true
}
// click the hidden button -> submit the form
remotesubmit() {
this.submitbtnTarget.click()
}
// same as above, but with "debounce"
// remotesubmit() {
// clearTimeout(this.timeout)
// this.timeout = setTimeout(() => {
// this.submitbtnTarget.click()
// }, 500)
// }
}
So now we try to send a request to the server each time we change something in the form…
We need a way to respond to it!
- create a new route that will respond to the second form
submitbutton
# config/routes.rb
resources :messages do
collection do
post :preview
end
end
- create another message object from the submitted params
- respond with a turbo_stream
# app/controllers/messages_controller.rb
def preview
# params.dig(:message, :content)
@preview = Message.new(message_params)
respond_to do |format|
format.turbo_stream
end
end
- update the
message_previewwith the attributes from the@previewobject - render errors, sanitized
@preview.content, or anything based on the@previewobject
# app/views/messages/preview.turbo_stream.erb
<%= turbo_stream.update "message_preview" do %>
<%= @preview.content.length %>
<%= @preview.valid? %>
<%= @preview.errors.full_messages %>
<%= @preview.attributes %>
<%= simple_format @preview.content %>
<% end %>
The drawback of this approach is having to exchange MANY request-responces with the server. Unlike a pure JS approach, that would handle everything on the client side.
This is inspired by the amazing idea of a form having a second submit button to a different URL, that was described in Thoughtbot’s: Server-rendered live previews
Did you like this article? Did it save you some time?