Module: Tuile::Component::HasBadInput
- Extended by:
- Listeners::Declare
- Includes:
- HasValidation
- Included in:
- BigDecimalField, DateField, DateTimeField, FloatField, IntegerField, TimeField
- Defined in:
- lib/tuile/component/has_bad_input.rb,
sig/tuile.rbs
Overview
The one fact a parsing field knows that Tuile::Component::HasValue#on_value_change cannot carry: the input is something the field's value cannot represent.
field = Component::IntegerField.new
# …the user types a lone minus, which no Integer can represent:
field.value # => nil, exactly as for an untouched field
field.empty? # => true, likewise — empty of *value*
field.bad_input? # => true
field. # => "not a whole number"
So a form asks this before empty?, on every field that can answer:
bad = fields.select { _1.respond_to?(:bad_input?) && _1.bad_input? }
Component::ConfirmWindow.alert("Cannot save", bad.map(&:bad_input_message).join("\n")) if bad.any?
Include it in a field whose parse is partial — whose input can be something its value cannot represent, as a date field's can. Not in a ComboBox, the near miss: its input is a filter rather than a formatting of the value, so a no-match is not a failed conversion and it reverts the query instead.
A pull and a push, answering differently on purpose
#bad_input? is derived on read and always current, which is what a save gate asked at a click wants. A consumer with cells — the form item painting the message beside the field — cannot be asked at a click, so it registers on #on_bad_input_change and paints Tuile::Component::HasValidation#shown_message:
field.on_bad_input_change { |e| .caption = e..to_s }
The push carries the showable report, #bad_input_message gated by #bad_input_settled? and diffed, because the underlying fact is continuous — every prefix of a valid date is bad input — and a display following it raw would flash through the act of typing correctly.
Implementation details
An includer overrides #bad_input_message and nothing else. Two rules
bind that override, and design/decisions.md D_bad_input has the why:
- Empty input is not bad input. Return
nilfor an empty buffer even though it parses to nothing, or every blank optional field blocks a save. - One frozen constant per field kind, no interpolation —
"not a valid date", never"'xyz' is not a valid date". It is read per call, the error ink reads it per paint, and the push diffs it per edit.
The status is never stored: @last_bad_input is the diff guard the push
needs and nothing reads it back, as AbstractWrappingField's @last_value
is for the value notice. Which fields can answer is a class fact worth
caching; what they answer is not.
An includer wrapping an editor is wired by this module — every buffer edit
funnels through handle_editor_change, and the sole writer rides it. One
that latches #bad_input_settled? or relays a child's report calls
sync_bad_input from wherever that moves (DateField, DateTimeField).
Defined Under Namespace
Classes: BadInputChangeEvent
Class Method Summary collapse
-
.listener ⇒ void
Defines the slot's reader, which returns the Listeners — or, given a block, registers it and returns the
Proc.
Instance Method Summary collapse
-
#bad_input? ⇒ Boolean
@return — true iff the field is holding input its value cannot represent.
-
#bad_input_message ⇒ String?
Why the current input cannot be turned into a Tuile::Component::HasValue#value — the single override point.
-
#bad_input_settled? ⇒ Boolean
Whether the report may be shown yet.
-
#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 #error_bg_color).
-
#error_ink? ⇒ Boolean
Widens Tuile::Component::HasValidation#error_ink?: bad input paints the invalid well too, with no verdict written — once it is showable, and unless a child wears it instead.
-
#error_message ⇒ StyledString?
@return — why the field is invalid, or
nilwhen it is not;niluntil something sets it. -
#error_message= ⇒ void
Sets the verdict and repaints the field in Theme#error_color;
nilclears it. -
#handle_editor_change ⇒ void
Rides AbstractWrappingField's edit funnel, so every includer wrapping an editor announces its report with no wiring of its own.
-
#inspect_details ⇒ ::Array[String]
Adds
error_message=…to #inspect, omitted while valid — so a Testing.get failure dump says which field is already flagged. -
#on_bad_input_change ⇒ Listeners
Fired with a BadInputChangeEvent whenever the showable report changes — see the class doc for what that is and why it is not #bad_input?.
-
#on_error_message_change ⇒ Listeners
Fired with an Tuile::Component::HasValidation::ErrorMessageChangeEvent whenever #error_message actually changes — never on a no-op set.
-
#shown_message ⇒ StyledString, ...
The field's own report when it is showable, else whatever Tuile::Component::HasValidation#shown_message has — bad input outranks a verdict.
-
#sync_bad_input ⇒ void
Fires #on_bad_input_change when the showable report has really changed — the sole writer of
@last_bad_input, called from wherever an input of that expression moves. -
#wears_bad_input_ink? ⇒ Boolean
Whether this component paints the well for its own report.
Methods included from Listeners::Declare
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.
11917 |
# File 'sig/tuile.rbs', line 11917
def self.listener: (Symbol name) ?{ (bool claimed) -> void } -> Symbol
|
Instance Method Details
#bad_input? ⇒ Boolean
@return — true iff the field is holding input its value cannot represent.
95 |
# File 'lib/tuile/component/has_bad_input.rb', line 95 def bad_input? = !.nil? |
#bad_input_message ⇒ String?
Why the current input cannot be turned into a Tuile::Component::HasValue#value — the single override point.
@return — the reason, or nil when the input converts (a
field holding no input converts: it is empty, not bad).
91 |
# File 'lib/tuile/component/has_bad_input.rb', line 91 def = raise(NotImplementedError, "#{self.class} must implement bad_input_message") |
#bad_input_settled? ⇒ Boolean
Whether the report may be shown yet. true here, so the well and the
notice are as continuous as the report: a FloatField reddens at the
half-typed "1.", which is a fair warning while the residue is one or
two transient buffers. Override it to latch where the grammar makes
every prefix bad input, or the field is red for the whole time the
user types a correct value:
def bad_input_settled? = @settled # set on commit, cleared on an edit
It gates what is shown — the ink and the push — and never #bad_input?,
the pull a save gate asks at the click (design/decisions.md
D_bad_input). Public because its readers are the app and a composite
relaying a child's report, the same reason #bad_input_message is.
111 |
# File 'lib/tuile/component/has_bad_input.rb', line 111 def bad_input_settled? = true |
#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).
11944 |
# File 'sig/tuile.rbs', line 11944
def error_bg_color: () -> Color?
|
#error_ink? ⇒ Boolean
Widens Tuile::Component::HasValidation#error_ink?: bad input paints the invalid well too, with no verdict written — once it is showable, and unless a child wears it instead.
124 |
# File 'lib/tuile/component/has_bad_input.rb', line 124 def error_ink? = (bad_input? && bad_input_settled? && wears_bad_input_ink?) || super |
#error_message ⇒ StyledString?
@return — why the field is invalid, or nil when it
is not; nil until something sets it.
11927 |
# File 'sig/tuile.rbs', line 11927
def error_message: () -> StyledString?
|
#error_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
11937 |
# File 'sig/tuile.rbs', line 11937
def error_message=: ((String | StyledString)? new_message) -> void
|
#handle_editor_change ⇒ void
This method returns an undefined value.
Rides AbstractWrappingField's edit funnel, so every includer wrapping an editor announces its report with no wiring of its own.
super is guarded because an includer need not wrap an editor —
DateTimeField is a Layout::Horizontal and never calls this.
139 140 141 142 |
# File 'lib/tuile/component/has_bad_input.rb', line 139 def handle_editor_change super if defined?(super) sync_bad_input 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.
11948 |
# File 'sig/tuile.rbs', line 11948
def inspect_details: () -> ::Array[String]
|
#on_bad_input_change ⇒ Listeners
Fired with a BadInputChangeEvent whenever the showable report changes — see the class doc for what that is and why it is not #bad_input?.
Empty means nobody outside the field is showing the report, which is the common case: the red well needs no notice, since it reads the pull on every paint.
84 |
# File 'lib/tuile/component/has_bad_input.rb', line 84 listener :on_bad_input_change |
#on_error_message_change ⇒ Listeners
Fired with an Tuile::Component::HasValidation::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.
11923 |
# File 'sig/tuile.rbs', line 11923
def on_error_message_change: () -> Listeners
|
#shown_message ⇒ StyledString, ...
The field's own report when it is showable, else whatever Tuile::Component::HasValidation#shown_message has — bad input outranks a verdict.
116 |
# File 'lib/tuile/component/has_bad_input.rb', line 116 def = ( if bad_input_settled?) || super |
#sync_bad_input ⇒ void
This method returns an undefined value.
Fires #on_bad_input_change when the showable report has really
changed — the sole writer of @last_bad_input, called from wherever an
input of that expression moves.
150 151 152 153 154 155 156 |
# File 'lib/tuile/component/has_bad_input.rb', line 150 def sync_bad_input showable = bad_input_settled? ? : nil return if showable == @last_bad_input @last_bad_input = showable on_bad_input_change.fire(BadInputChangeEvent.new(source: self, message: showable)) end |
#wears_bad_input_ink? ⇒ Boolean
Whether this component paints the well for its own report. true
here; a composite relaying a child's report answers false while that
child is the one holding it, so the fault reddens where it happened
rather than across the whole widget (DateTimeField).
131 |
# File 'lib/tuile/component/has_bad_input.rb', line 131 def wears_bad_input_ink? = true |