Module: Tuile::Component::HasValue

Extended by:
Listeners::Declare
Includes:
HasValidation
Included in:
AbstractStringField, AbstractWrappingField, Checkbox, CheckboxGroup, ComboBox, DateTimeField, RadioGroup, Select
Defined in:
lib/tuile/component/has_value.rb,
sig/tuile.rbs

Overview

The value seam every input component shares: a settable/gettable #value of any type, an #on_value_change listener, #empty?, and #clear. A form (a future binder) drives a mix of field types uniformly through it, not caring that a TextField's value is a String while another field's is a domain object.

field.on_value_change { |e| puts "now: #{e.value.inspect}" }
field.value = "hello"   # fires the listener, e.from_user? false
field.clear             # value = empty_value, fires again

Who made the change

Every event tells typing from a programmatic write, so a listener keeps no guard flag of its own:

field.on_value_change { |e| open_palette if e.from_user? }
field.value = "x"                        # e.from_user? is false
field.set_value("x", from_user: true)    # an app writing for the user

The default #set_value/#value keep the value in @value and are enough for a component with nothing more natural — you get a repaint and the listener for free. An includer whose value lives elsewhere overrides both (AbstractStringField backs them with its text buffer); the override point is #set_value, never #value=, which is only its from_user: false spelling. Override #empty_value when the empty sentinel isn't nil (a text field's is "").

HasValidation comes with it, so every field carries the error_message a validator writes and paints its own error ink.

Implementation details

Deliberately smaller than Vaadin's HasValue: read-only, required-indicator and converters belong to the not-yet-built form layer, not here. Of Vaadin's event payload, isFromClient is carried (as ValueChangeEvent#from_user?) and getOldValue is not, until something reads it (D_from_user).

Defined Under Namespace

Classes: ValueChangeEvent

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)


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

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

Instance Method Details

#clear ⇒ void

This method returns an undefined value.

Resets #value to #empty_value.

An includer whose input can outrun its value (Tuile::Component::HasBadInput) must clear the input: a field holding bad input already reads empty_value, so inheriting this default — over a #set_value that returns early on a no-op set — is a clear that leaves the garbage on screen. A clear is programmatic: its event says from_user? == false.



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

def clear = (self.value = empty_value)

#empty? ⇒ Boolean

Empty of value: a field whose parse is partial reports true while the user is looking at glyphs it could not use, so ask Tuile::Component::HasBadInput#bad_input? first.

@return — true iff #value equals #empty_value.

Returns:

  • (Boolean)


106
# File 'lib/tuile/component/has_value.rb', line 106

def empty? = value == empty_value

#empty_value ⇒ Object

@return — the value #empty?/#clear treat as empty; nil unless an includer overrides it.

Returns:

  • (Object)


120
# File 'lib/tuile/component/has_value.rb', line 120

def empty_value = nil

#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:



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

def error_bg_color: () -> Color?

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


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

def error_ink?: () -> bool

#error_message ⇒ StyledString?

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

Returns:



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

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:



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

def error_message=: ((String | StyledString)? new_message) -> void

#focusable? ⇒ Boolean

Input fields are focusable by default (overrides Tuile::Component#focusable?); a read-only display field could override back to false. Only focusable? lives here — tab_stop? diverges between leaf fields and composing wrappers, so it stays per-class (design/decisions.md D_integer_field).

Returns:

  • (Boolean)


128
# File 'lib/tuile/component/has_value.rb', line 128

def focusable? = true

#inspect_details ⇒ ::Array[String]

Adds value=… to Tuile::Component#inspect, omitted while the value is nil.

Returns:

  • (::Array[String])


134
135
136
137
138
139
140
141
142
# File 'lib/tuile/component/has_value.rb', line 134

def inspect_details
  v = value
  return super if v.nil?

  # Truncate before #inspect, not after: a TextArea's value is its whole
  # buffer.
  v = "#{v[0, 40]}…" if v.is_a?(String) && v.length > 40
  super + ["value=#{v.inspect}"]
end

#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:



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

def on_error_message_change: () -> Listeners

#on_value_change ⇒ Listeners

Fired with a ValueChangeEvent whenever #value actually changes — never on a no-op set.

Returns:



65
# File 'lib/tuile/component/has_value.rb', line 65

listener :on_value_change

#set_value(new_value, from_user:) ⇒ void

This method returns an undefined value.

Writes the value and fires #on_value_change carrying from_user. No-op (no repaint, no listener) when equal to the current value.

field.set_value(Date.today, from_user: true)   # a "Today" button's click

The flag is the writer's claim, never derived from key dispatch and never checked: pass true when the write is a user's gesture — the gem's own key and mouse handlers do, as does Testing.set_value — and false for what the app decides alone. A wrong flag is silent, and false is the side that fails safe (D_from_user).

The override point for an includer: call super with from_user:.

@param new_value

@param from_user

Parameters:

  • new_value (Object)
  • from_user: (Boolean)


94
95
96
97
98
99
100
# File 'lib/tuile/component/has_value.rb', line 94

def set_value(new_value, from_user:)
  return if value == new_value

  @value = new_value
  invalidate
  on_value_change.fire(ValueChangeEvent.new(source: self, value: new_value, from_user:))
end

#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:



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

def shown_message: () -> (StyledString | String)?

#value ⇒ Object

@return — the current value; nil until first set.

Returns:

  • (Object)


68
# File 'lib/tuile/component/has_value.rb', line 68

def value = @value

#value=(new_value) ⇒ void

This method returns an undefined value.

A programmatic write: #set_value with from_user: false. Defined here once and never overridden — override #set_value instead, since a setter has no call syntax for the keyword.

@param new_value

Parameters:

  • new_value (Object)


75
76
77
# File 'lib/tuile/component/has_value.rb', line 75

def value=(new_value)
  set_value(new_value, from_user: false)
end