Module: Tuile::Component::HasValidation

Extended by:
Listeners::Declare
Included in:
HasBadInput, HasValue
Defined in:
lib/tuile/component/has_validation.rb,
sig/tuile.rbs

Overview

A field's verdict slot: one message, written from outside the field, and shown by the field as a red well — Theme#error_bg_color, or Theme#error_active_bg_color while focused.

 = Component::Button.new(caption: "Log in")
.on_click do
username.error_message = username.empty? ? "Required" : nil
password.error_message = password.empty? ? "Required" : nil
next if [username, password].any?(&:error_message)

authenticate(username.value, password.value)
end

The field turns red on its own; the message needs cells the field doesn't own, so whoever has them — a FormLayout, or an app's own Label — subscribes to #on_error_message_change and paints #shown_message in Theme#error_color.

Included by HasValue, so every field has it. Include it directly in a component that can be invalid without being a field (a form section wrapping several).

Implementation details

A well, not ink on the glyphs. A field's background is what shows its boundary in the first place, so an invalid field gets a red one and loses nothing — where red text is invisible on the empty field that is the required-field case, and invisible again on content carrying colors of its own. It takes two tokens rather than one because a focused invalid field still has to look focused (design/decisions.md D_has_validation).

The well reaches the whole widget with nothing forwarding it: a composed field's inner face is marked Tuile::ComponentBackground::INHERIT and a group's List declares no background, so both walk up the ordinary background chain and land on the composer's answer.

The field never writes this. It computes no verdicts — it cannot see the sibling a rule compares against — so it has nothing to write, and that is what leaves exactly one writer: whoever validates. The discipline that writer owes is one sentence: set or clear it on every validate pass, as the example above does with its : nil branches. Vaadin's custom-field guide warns that sharing one invalid cell between internal and external validation ends with each overriding the other; Tuile's answer is that the field's own report is a different member (Tuile::Component::HasBadInput#bad_input? — derived on read, never stored), so the two never share a cell.

There is deliberately no invalid?: a second predicate beside bad_input? gives a caller no way to know which to ask, and invalid is a non-nil message. Assign "" for a verdict with nothing to say.

Unlike bad_input?, this fact is discrete — asserted at a click or a binder pass, not recomputed per keystroke — so its notice fires straight off the write, where Tuile::Component::HasBadInput#on_bad_input_change has a continuous fact to settle first (design/decisions.md D_bad_input, D_has_validation).

Defined Under Namespace

Classes: ErrorMessageChangeEvent

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Listeners::Declare

listener

Class Method Details

.listener ⇒ void

This method returns an undefined value.

Defines the slot's reader, which returns the Listeners — or, given a block, registers it and returns the Proc.

An optional block is the slot's transition block, run on the owner whenever the list becomes non-empty or empty again. It is what lets a widget install a bridge only while somebody is listening:

listener :on_enter do |claimed|
claimed ? editor.on_enter << @bridge : editor.on_enter.remove(@bridge)
end

The list is built on first read, so no mixin has to remember a constructor line — which means in-class code goes through the reader and never touches @on_foo, nil until somebody asks.

@param name — the slot's full name, on_-prefixed. Spelling it out is what keeps on_value_change greppable from its declaration.

@return — name.

Parameters:

  • name (Symbol)


13128
# File 'sig/tuile.rbs', line 13128

def self.listener: (Symbol name) ?{ (bool claimed) -> void } -> Symbol

Instance Method Details

#error_bg_color ⇒ Color?

The invalid well, picked up by everything this component paints — including the inner face of a composed field and the List of a group, neither of which forwards anything: both declare no background of their own, so the ordinary chain walks up to this (overrides Tuile::Component#error_bg_color).

Returns:



120
121
122
123
124
# File 'lib/tuile/component/has_validation.rb', line 120

def error_bg_color
  return nil unless error_ink?

  active? ? screen.theme.error_active_bg_color : screen.theme.error_bg_color
end

#error_ink? ⇒ Boolean

Whether to paint the invalid well right now. Its own hook because Tuile::Component::HasBadInput widens it: a field holding input its value cannot represent is invalid on the face too, even with no verdict written.

Returns:

  • (Boolean)


130
# File 'lib/tuile/component/has_validation.rb', line 130

def error_ink? = !error_message.nil?

#error_message ⇒ StyledString?

@return — why the field is invalid, or nil when it is not; nil until something sets it.

Returns:



81
# File 'lib/tuile/component/has_validation.rb', line 81

def error_message = @error_message

#error_message=(new_message) ⇒ void

This method returns an undefined value.

Sets the verdict and repaints the field in Theme#error_color; nil clears it. No-op (no repaint, no listener) when unchanged. A String is parsed via StyledString.parse, as Tuile::Component::HasCaption#caption= does.

Safe on a detached field — an app validates a form it assembled but has not mounted, and Tuile::Component#invalidate is already a no-op there.

@param new_message

Parameters:



91
92
93
94
95
96
97
98
# File 'lib/tuile/component/has_validation.rb', line 91

def error_message=(new_message)
  new_message = StyledString.parse(new_message) unless new_message.nil?
  return if error_message == new_message

  @error_message = new_message
  invalidate
  on_error_message_change.fire(ErrorMessageChangeEvent.new(source: self, error_message: new_message))
end

#inspect_details ⇒ ::Array[String]

Adds error_message=… to Tuile::Component#inspect, omitted while valid — so a Testing.get failure dump says which field is already flagged.

Returns:

  • (::Array[String])


135
136
137
138
# File 'lib/tuile/component/has_validation.rb', line 135

def inspect_details
  m = error_message
  m.nil? ? super : super + ["error_message=#{m.to_s.inspect}"]
end

#on_error_message_change ⇒ Listeners

Fired with an ErrorMessageChangeEvent whenever #error_message actually changes — never on a no-op set. The container that paints the message registers here, and so may an app painting its own: the list takes both, which a single slot could not.

Returns:



77
# File 'lib/tuile/component/has_validation.rb', line 77

listener :on_error_message_change

#shown_message ⇒ StyledString, ...

The message a consumer with cells of its own paints beside the field: the verdict here, and Tuile::Component::HasBadInput widens it to prefer the field's own report, which outranks a verdict computed a pass ago.

field.on_error_message_change { message_label.caption = field.shown_message.to_s }
field.on_bad_input_change     { message_label.caption = field.shown_message.to_s }

Register on both: two channels with two writers, either of which moves it.

@return — nil when there is nothing to show; a plain String when it is the field's own bad-input report.

Returns:



110
# File 'lib/tuile/component/has_validation.rb', line 110

def shown_message = error_message