forked from fixin.me/fixin.me
Display form errors using custom validity messages
This commit is contained in:
@@ -55,9 +55,9 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def render_no_content(record)
|
||||
helpers.render_errors(record)
|
||||
render html: nil, layout: true
|
||||
def render_errors(record)
|
||||
target = record.new_record? ? [:new, record.parent || record.class] : [:edit, record]
|
||||
render 'errors', assigns: {record: record, target: target}
|
||||
end
|
||||
|
||||
def rescue_turbo(exception)
|
||||
|
||||
@@ -43,12 +43,15 @@ class QuantitiesController < ApplicationController
|
||||
permitted = params.require(:quantity).permit(:parent_id)
|
||||
@previous_ancestors = @quantity.ancestors
|
||||
|
||||
# Until UI blocks all disallowed reparents, render error messages if present
|
||||
render_no_content(@quantity) unless @quantity.update(permitted)
|
||||
|
||||
if @quantity.update(permitted)
|
||||
@ancestors = @quantity.ancestors
|
||||
@self_and_progenies = @quantity.with_progenies
|
||||
@before = @self_and_progenies.last.successive
|
||||
else
|
||||
# Until UI blocks all disallowed reparents, render error messages if present.
|
||||
flash.now.alert = @quantity.errors.full_messages
|
||||
render :nothing
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -22,7 +22,7 @@ class UnitsController < ApplicationController
|
||||
@before = @unit.successive
|
||||
flash.now[:notice] = t('.success', unit: @unit)
|
||||
else
|
||||
render :new
|
||||
render_errors @unit
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ class UnitsController < ApplicationController
|
||||
if @unit.update(params.except(:base_id).expect(Unit::ATTRIBUTES))
|
||||
flash.now[:notice] = t('.success', unit: @unit)
|
||||
else
|
||||
render :edit
|
||||
render_errors @unit
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -91,10 +91,7 @@ module ApplicationHelper
|
||||
[:text_field, :password_field, :text_area].each do |selector|
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{selector}(method, options = {})
|
||||
options[:maxlength] ||= object.class.type_for_attribute(method).limit
|
||||
if object.errors.include?(method)
|
||||
options[:pattern] = except_pattern(object.public_send(method), options[:pattern])
|
||||
end
|
||||
options[:maxlength] ||= object.type_for_attribute(method).limit
|
||||
super
|
||||
end
|
||||
RUBY_EVAL
|
||||
@@ -129,10 +126,6 @@ module ApplicationHelper
|
||||
svg_name = object ? (object.persisted? ? 'update' : 'plus-circle-outline') : ''
|
||||
@template.svg_tag(svg_name, super)
|
||||
end
|
||||
|
||||
def except_pattern(value, pattern = nil)
|
||||
"(?!^#{Regexp.escape(value)}$)" + (pattern || ".*")
|
||||
end
|
||||
end
|
||||
|
||||
def tabular_fields_for(record_name, record_object = nil, options = {}, &block)
|
||||
@@ -142,8 +135,6 @@ module ApplicationHelper
|
||||
record_object, options = nil, record_object if record_object.is_a?(Hash)
|
||||
extra_options = {builder: TabularFormBuilder, skip_default_ids: true}
|
||||
options = merge_attributes(options, extra_options)
|
||||
# TODO: set error message with setCustomValidity instead of rendering to flash?
|
||||
render_errors(record_object || record_name)
|
||||
fields_for(record_name, record_object, **options, &block)
|
||||
end
|
||||
|
||||
@@ -210,16 +201,9 @@ module ApplicationHelper
|
||||
link_to name, options, html_options
|
||||
end
|
||||
|
||||
def render_errors(records)
|
||||
# Conversion of flash to Array only required because of Devise
|
||||
# TODO: override Devise message setting to Array()?
|
||||
flash[:alert] = Array(flash[:alert])
|
||||
Array(records).each { |record| flash[:alert] += record.errors.full_messages }
|
||||
end
|
||||
|
||||
def render_flash_messages
|
||||
def render_flash
|
||||
flash.map do |entry, messages|
|
||||
# Conversion of flash to Array only required because of Devise
|
||||
# Conversion of flash to Array only required because of Devise.
|
||||
Array(messages).map do |message|
|
||||
tag.div class: "flash #{entry}" do
|
||||
tag.span(sanitize(message)) +
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Configure your import map in config/importmap.rb. Read more:
|
||||
// https://github.com/rails/importmap-rails
|
||||
import "@hotwired/turbo-rails"
|
||||
// TODO: access `data-` attributes through
|
||||
// `keyname in`/`dataset.keyname`/`delete dataset.keyname`
|
||||
|
||||
|
||||
// Show page if hidden for testing purposes.
|
||||
@@ -146,6 +148,33 @@ Turbo.StreamActions.unselect = function() {
|
||||
})
|
||||
}
|
||||
|
||||
Turbo.StreamActions.validate = function() {
|
||||
this.targetElements.forEach((form) => {
|
||||
this.templateContent.querySelectorAll("span").forEach((span) => {
|
||||
var control = document.querySelector(
|
||||
`[form="${form.id}"][name="${span.dataset.name}"]`
|
||||
)
|
||||
control.setCustomValidity(span.textContent)
|
||||
// Add event listener in capture phase to clear custom validity before
|
||||
// `[oninput]` or any other bubble phase handler is invoked.
|
||||
// https://www.quirksmode.org/js/events_order.html
|
||||
// TODO: keep invalidating current value whenever user re-enters it. Store
|
||||
// invalid value and validation message in `[data-invalid-message/value]`
|
||||
// and check input value on "input" event (below) (using `[pattern]` may not
|
||||
// be suitable due to inability to customize validtion message; also
|
||||
// pattern may be used for other, conflicting purposes).
|
||||
// After user executes any other action (while form is open), remove `[data-]`
|
||||
// attributes and custom validation, as value may no longer be invalid.
|
||||
control.addEventListener(
|
||||
"input",
|
||||
function(event) { event.target.setCustomValidity("") },
|
||||
{capture: true, once: true}
|
||||
)
|
||||
})
|
||||
form.reportValidity()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Keyboard event processing handlers.
|
||||
function formProcessKey(event) {
|
||||
@@ -209,6 +238,8 @@ window.dragStart = dragStart
|
||||
* rapid pointer moves.
|
||||
* NOTE: sometimes Leave is not emitted when pointer goes fast over table
|
||||
* and outside. This should probably be fixed in browser, than patched here.
|
||||
* TODO: replace Leave table with Enter document?
|
||||
* https://www.quirksmode.org/js/events_order.html#link4
|
||||
*/
|
||||
function dragEnter(event) {
|
||||
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
|
||||
|
||||
@@ -3,6 +3,7 @@ class Unit < ApplicationRecord
|
||||
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :base, optional: true, class_name: "Unit"
|
||||
alias_method :parent, :base
|
||||
has_many :subunits, class_name: "Unit", inverse_of: :base,
|
||||
dependent: :restrict_with_error
|
||||
|
||||
|
||||
7
app/views/application/errors.turbo_stream.erb
Normal file
7
app/views/application/errors.turbo_stream.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<%= turbo_stream.validate dom_target(*@target, :form) do %>
|
||||
<% @record.errors.attribute_names.map do |attr| %>
|
||||
<%= tag.span data: {name: field_name(dom_class(@record), attr)} do %>
|
||||
<%- @record.errors.full_messages_for(attr).map { |m| m + '.' }.join(' ') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
0
app/views/application/nothing.turbo_stream.erb
Normal file
0
app/views/application/nothing.turbo_stream.erb
Normal file
@@ -44,7 +44,7 @@
|
||||
</header>
|
||||
|
||||
<div id="flashes">
|
||||
<%= render_flash_messages %>
|
||||
<%= render_flash %>
|
||||
</div>
|
||||
|
||||
<%# Allows overwriting/clearing navigation menu for some views %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= yield %>
|
||||
|
||||
<%# Some views may convert ActiveRecord errors to flashes, render at the end. %>
|
||||
<%# Render unconditionally, to clear previous flash messages. %>
|
||||
<%= turbo_stream.update :flashes do %>
|
||||
<%= render_flash_messages %>
|
||||
<%= render_flash %>
|
||||
<% end %>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<%# For some reason flash messages are duplicated in bot flash and flash.now %>
|
||||
<% flash.discard %>
|
||||
|
||||
@@ -34,4 +34,8 @@ ActiveSupport.on_load :turbo_streams_tag_builder do
|
||||
def unselect(target)
|
||||
action :unselect, target, allow_inferred_rendering: false
|
||||
end
|
||||
|
||||
def validate(target, content = nil, **rendering, &block)
|
||||
action :validate, target, content, **rendering, &block
|
||||
end
|
||||
end
|
||||
|
||||
@@ -163,31 +163,32 @@ class UnitsTest < ApplicationSystemTestCase
|
||||
link = all(:link, exact_text: label).sample
|
||||
link.click
|
||||
|
||||
get_values = ->{ all(:field).map { |f| [f[:name], f.value] }.to_h }
|
||||
values = nil
|
||||
within :table_row, {}, with_focus: true do
|
||||
get_values = ->{
|
||||
find(:table_row, {}, with_focus: true)
|
||||
.all(:field).map { |f| [f[:name], f.value] }.to_h
|
||||
}
|
||||
|
||||
# Provide duplicate :symbol as server-side invalidated input.
|
||||
field = find(:table_cell, column(:symbol)).find(:fillable_field)
|
||||
field = find(:field, focused: true)
|
||||
field.fill_in with: (symbols - [field.value]).sample
|
||||
values = get_values[]
|
||||
send_keys :enter
|
||||
end
|
||||
|
||||
assert_selector '.flash.alert',
|
||||
text: t('activerecord.errors.models.unit.attributes.symbol.taken')
|
||||
assert_matches_selector field, :field, validation_message:
|
||||
/#{t('activerecord.errors.models.unit.attributes.symbol.taken')}/
|
||||
assert_no_selector '.flash.alert'
|
||||
assert_equal values, get_values[]
|
||||
if action == :edit
|
||||
assert_no_selector :link, exact_text: link[:text]
|
||||
else
|
||||
assert_matches_selector link, :link, disabled: true
|
||||
end
|
||||
|
||||
within :table_row, {}, with_focus: true do
|
||||
assert_equal values, get_values[]
|
||||
click_on t(:cancel)
|
||||
end
|
||||
|
||||
assert_no_selector '.flash.alert'
|
||||
assert_equal symbols, list_symbols
|
||||
refresh
|
||||
assert_no_selector '.flash.alert'
|
||||
assert_equal symbols, list_symbols
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user