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.bad_input_message     # => "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| message_label.caption = e.message.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 nil for 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

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)


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.

Returns:

  • (Boolean)


95
# File 'lib/tuile/component/has_bad_input.rb', line 95

def bad_input? = !bad_input_message.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).

Returns:

  • (String, nil)


91
# File 'lib/tuile/component/has_bad_input.rb', line 91

def bad_input_message = 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.

Returns:

  • (Boolean)


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).

Returns:



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.

Returns:

  • (Boolean)


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.

Returns:



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

Parameters:



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.

Returns:

  • (::Array[String])


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.

Returns:



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.

Returns:



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.

Returns:



116
# File 'lib/tuile/component/has_bad_input.rb', line 116

def shown_message = (bad_input_message 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? ? bad_input_message : 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).

Returns:

  • (Boolean)


131
# File 'lib/tuile/component/has_bad_input.rb', line 131

def wears_bad_input_ink? = true