Skip to content

Instantly share code, notes, and snippets.

@gkemmey
Created June 5, 2019 15:56
Show Gist options
  • Select an option

  • Save gkemmey/36043b3322b30c55e06dede2bf496591 to your computer and use it in GitHub Desktop.

Select an option

Save gkemmey/36043b3322b30c55e06dede2bf496591 to your computer and use it in GitHub Desktop.
Rails remote form error handling
# -------------------------------------------------------------------------------------------------
# monkey patches ActionView::Helpers::Tags module and adds a wrapper tag. additionally,
# patches ActionView::Helpers::FormBuilder to add a `wrapper` method. this will allow you to
# use the following in your forms:
#
# <%= f.wrapper :email, :div, class: "control" do %>
# <%= f.email_field :email, placeholder: "you@example.com", class: "input is-danger" %>
# <% end %>
#
# we do it this way, so the wrapper is aware of the model object it's building, and thus the
# wrapped html is passed to the `field_error_proc` so we can customize it if the field has an error
#
# this is heavily synthesized from the ActionView::Helpers::Tags::Label code that's invoked
# when you use `f.label :email ...` in a form here:
# https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/tags/label.rb
#
# other relevant files:
# https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb -- where
# the ActionView::Helpers::FormBuilder class and the ActionView::Helpers::FormHelper module are
# defined, and of course the `FormBuilder#label` method
#
module ActionView
module Helpers
module Tags
class Wrapper < Base
def initialize(object_name, method_name, template_object, tag, options)
@tag = tag
super(object_name, method_name, template_object, options)
end
def render(&block)
if block_given?
content_tag @tag, @template_object.capture(&block), @options
else
content_tag @tag, nil, @options
end
end
end
end
end
end
class ActionView::Helpers::FormBuilder
def wrapper(method, tag = :div, options = {}, &block)
tag, options = :div, tag if tag.is_a?(Hash)
ActionView::Helpers::Tags::Wrapper.new(@object_name, method, @template, tag, objectify_options(options)).render(&block)
end
end
# -------------------------------------------------------------------------------------------------
# inspired by: https://rubyplus.com/articles/3401-Customize-Field-Error-in-Rails-5
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
result = html_tag
if instance.is_a?(ActionView::Helpers::Tags::Wrapper) # ok, we're dealing with our custom helper 💪
_html_tag = Nokogiri::HTML::DocumentFragment.parse(html_tag)
wrapper = _html_tag.elements.first
wrapper['class'] = [wrapper['class'], "is-danger"].compact.join(" ")
Array(instance.error_message).map(&:humanize).uniq.each do |error|
_html_tag.add_child(%(<p class="help is-danger">#{error}</p>))
end
result = _html_tag.to_s
else
form_fields = ['textarea', 'input', 'select']
elements = Nokogiri::HTML::DocumentFragment.parse(html_tag).css((['label'] + form_fields).join(', '))
elements.each do |e|
if e.node_name == 'label'
# not doing anything, but wanted to capture how to do something for futures 🔮
elsif form_fields.include?(e.node_name)
_html_tag = Nokogiri::HTML::DocumentFragment.parse(html_tag)
form_field = _html_tag.elements.first
form_field['class'] = [form_field['class'], "is-danger"].compact.join(" ")
result = _html_tag.to_s
end
end
end
result.html_safe
end
// stolen from: https://github.com/turbolinks/turbolinks-rails/pull/20#issuecomment-332909770
// more relavant conversation conversation here: https://github.com/turbolinks/turbolinks/issues/85
(function(document, window, Turbolinks) {
document.addEventListener("turbolinks:load", function() {
for (let element of document.querySelectorAll("form[data-remote='true']")) {
element.addEventListener("ajax:send", function(event) {
document.activeElement.blur()
Turbolinks.dispatch("turbolinks:click", event)
});
// the original script mentioned above, used 'ajax:complete' and looked for a "Location"
// header to decide, "hey i need to redirect". if there wasn't a "Location" header, it
// assumed we must want to re-render with errors so do that. however, that may not always
// be the case for instance if you're responding with server-rendered javascript.
//
// anyway, thinking if you want the page re-rendered you need to indicate the form submission
// failed in the controller with something like `render :new, status: 422`. if the status
// is 200, we'll assume you had your own plans and opt not to do anything here.
//
// idk, if the "Location" header checking is necessary, now, but leaving for now.
element.addEventListener("ajax:error", function(event) {
let xhr = event.detail.filter((object) => ( object instanceof XMLHttpRequest ))[0]
// if the server responds with javascript, we wanna run it, so do nothing and let
// rails-ujs pick it up
if ((xhr.getResponseHeader("Content-Type") || '').match(/javascript/)) {
return;
}
if (!xhr.getResponseHeader("Location")) {
current_snapshot = Turbolinks.Snapshot.fromHTMLElement(document)
new_snapshot = Turbolinks.Snapshot.wrap(xhr.responseText)
Turbolinks.controller.currentVisit = Turbolinks.controller.createVisit(window.location.href, "replace", {})
Turbolinks.SnapshotRenderer.render(
Turbolinks.controller,
function() {
Turbolinks.dispatch("turbolinks:load")
// you may want to prioitize scrolling to a flash message vs focussing the first error
// stolen from: https://github.com/turbolinks/turbolinks/issues/85#issuecomment-297528382
// if ($("#flash_message").length > 0) {
// scrollTo(0,0);
// }
// else {
// $(".has-error input, .has-error select").first().focus();
// }
let first_error = document.body.querySelector(".has-error input, .has-error select")
if (first_error) { first_error.focus() }
},
current_snapshot,
new_snapshot
)
}
});
}
});
})(document, window, Turbolinks);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment