Class: StimulusGridRails::Column

Inherits:
Object
  • Object
show all
Defined in:
lib/stimulus_grid_rails/column.rb

Overview

One column on a Grid. Captures everything the server needs to authorise, coerce, validate, and broadcast changes for this field — plus everything the client needs to choose an editor and render a cell.

Created via the ‘column` DSL inside an ApplicationGrid subclass; not instantiated directly. See StimulusGridRails::Grid for usage.

Constant Summary collapse

TYPES =
%i[string text integer bigint decimal money boolean enum date datetime reference].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, type:, editable: false, editor: nil, editor_config: {}, enum_values: nil, concurrency: :last_write_wins, computed: false, depends_on: [], validate: nil, header: nil, width: nil, pinned: nil, cell_renderer: nil, cell_editor: nil, sortable: true, filterable: true, searchable: nil) ⇒ Column

Returns a new instance of Column.

Raises:

  • (ArgumentError)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/stimulus_grid_rails/column.rb', line 15

def initialize(name, type:, editable: false, editor: nil, editor_config: {},
               enum_values: nil, concurrency: :last_write_wins,
               computed: false, depends_on: [], validate: nil,
               header: nil, width: nil, pinned: nil, cell_renderer: nil,
               cell_editor: nil, sortable: true, filterable: true, searchable: nil)
  raise ArgumentError, "Unknown column type #{type.inspect}" unless TYPES.include?(type)
  @name          = name.to_sym
  @type          = type
  @editable      = editable                       # may be `true`, `false`, or a Proc(row, user)
  @editor        = editor || default_editor_for(type)
  @editor_config = editor_config
  @enum_values   = enum_values
  @concurrency   = concurrency
  @computed      = computed
  @depends_on    = Array(depends_on)
  @validators    = Array(validate)
  @header        = header || name.to_s.humanize
  @width         = width
  @pinned        = pinned
  @cell_renderer = cell_renderer
  @cell_editor   = cell_editor
  @sortable      = sortable
  @filterable    = filterable
  # Global search defaults to text-ish columns; numeric/date/boolean opt in.
  @searchable    = searchable.nil? ? %i[string text enum reference].include?(type) : searchable
end

Instance Attribute Details

#cell_editorObject (readonly)

Returns the value of attribute cell_editor.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def cell_editor
  @cell_editor
end

#cell_rendererObject (readonly)

Returns the value of attribute cell_renderer.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def cell_renderer
  @cell_renderer
end

#concurrencyObject (readonly)

Returns the value of attribute concurrency.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def concurrency
  @concurrency
end

#depends_onObject (readonly)

Returns the value of attribute depends_on.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def depends_on
  @depends_on
end

#editorObject (readonly)

Returns the value of attribute editor.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def editor
  @editor
end

#editor_configObject (readonly)

Returns the value of attribute editor_config.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def editor_config
  @editor_config
end

#enum_valuesObject (readonly)

Returns the value of attribute enum_values.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def enum_values
  @enum_values
end

#headerObject (readonly)

Returns the value of attribute header.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def header
  @header
end

#nameObject (readonly)

Returns the value of attribute name.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def name
  @name
end

#pinnedObject (readonly)

Returns the value of attribute pinned.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def pinned
  @pinned
end

#typeObject (readonly)

Returns the value of attribute type.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def type
  @type
end

#validatorsObject (readonly)

Returns the value of attribute validators.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def validators
  @validators
end

#widthObject (readonly)

Returns the value of attribute width.



11
12
13
# File 'lib/stimulus_grid_rails/column.rb', line 11

def width
  @width
end

Instance Method Details

#cascades?Boolean

Returns:

  • (Boolean)


57
# File 'lib/stimulus_grid_rails/column.rb', line 57

def cascades?  = !@depends_on.empty?

#client_data_attrs(row, user) ⇒ Object

Serialized into the rendered cell’s data-* attributes so the client-side editor controller can mount the right editor with the right config.



130
131
132
133
134
135
136
137
138
139
# File 'lib/stimulus_grid_rails/column.rb', line 130

def client_data_attrs(row, user)
  attrs = {
    "data-col-id" => name,
    "data-editor" => @editor,
  }
  attrs["data-editable"] = "true" if editable_for?(row, user)
  attrs["data-enum-values"] = JSON.generate(@enum_values) if @enum_values
  attrs["data-editor-config"] = JSON.generate(@editor_config) unless @editor_config.empty?
  attrs
end

#coerce(raw) ⇒ Object

Coerce a string value (from a form submission) to this column’s Ruby type. Returns [value, error] — error is a string if coercion failed.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/stimulus_grid_rails/column.rb', line 61

def coerce(raw)
  case @type
  when :string, :text, :enum, :reference then [raw.to_s, nil]
  when :integer, :bigint
    Integer(raw.to_s, 10).then { |i| [i, nil] }
  when :decimal, :money
    [BigDecimal(raw.to_s), nil]
  when :boolean
    [%w[1 true yes on t].include?(raw.to_s.downcase), nil]
  when :date
    [Date.parse(raw.to_s), nil]
  when :datetime
    [Time.zone.parse(raw.to_s), nil]
  else
    [raw, nil]
  end
rescue ArgumentError, TypeError => e
  [nil, "invalid #{@type}: #{e.message}"]
end

#computed?Boolean

Returns:

  • (Boolean)


56
# File 'lib/stimulus_grid_rails/column.rb', line 56

def computed?  = @computed

#editable_for?(row, user) ⇒ Boolean

Per-row, per-user editable check. RAILS.md §17 — server re-evaluates on every PATCH (never trust the client’s data-editable attribute).

Returns:

  • (Boolean)


44
45
46
47
48
49
50
# File 'lib/stimulus_grid_rails/column.rb', line 44

def editable_for?(row, user)
  case @editable
  when true, false then @editable
  when Proc then !!@editable.call(row, user)
  else !!@editable
  end
end

#editable_static?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/stimulus_grid_rails/column.rb', line 52

def editable_static?
  @editable == true
end

#filter_predicate(arel_table, criteria) ⇒ Object

Arel predicate for a per-column filter. ‘criteria` mirrors the client filterModel shape: { “type” => op, “value” => v, “value2” => v2 }. Returns nil for computed/unknown columns or unparseable values.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/stimulus_grid_rails/column.rb', line 95

def filter_predicate(arel_table, criteria)
  return nil if @computed || name.to_s.start_with?("_")
  col = arel_table[@name]
  op  = (criteria["type"] || criteria[:type]).to_s
  raw = criteria["value"]  || criteria[:value]
  raw2 = criteria["value2"] || criteria[:value2]

  case @type
  when :string, :text, :enum, :reference
    text_predicate(col, op, raw.to_s)
  when :integer, :bigint, :decimal, :money
    numeric_predicate(col, op, raw, raw2)
  when :date, :datetime
    date_predicate(col, op, raw, raw2)
  when :boolean
    v, err = coerce(raw)
    err ? nil : col.eq(v)
  end
end

#header_data_attrsObject

Serialized into the column’s <th> so header_cell_controller picks it up.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/stimulus_grid_rails/column.rb', line 142

def header_data_attrs
  {
    "data-controller" => "header-cell",
    "data-header-cell-field-value" => name,
    "data-header-cell-type-value" => header_cell_type,
    "data-header-cell-sortable-value" => @sortable.to_s,
    "data-header-cell-filter-value" => (@filterable ? filter_type_for_client : nil),
    "data-header-cell-editable-value" => editable_static?.to_s,
    "data-header-cell-pinned-value" => @pinned,
    "data-header-cell-width-value" => @width,
    "data-header-cell-cell-renderer-value" => @cell_renderer,
    "data-header-cell-cell-editor-value" => @cell_editor,
  }.compact
end

#search_predicate(arel_table, term) ⇒ Object

Arel predicate for the global search term, or nil if this column doesn’t participate. Case-insensitive contains over the column’s text. Computed and action columns never match (no DB column to query).



86
87
88
89
90
# File 'lib/stimulus_grid_rails/column.rb', line 86

def search_predicate(arel_table, term)
  return nil unless searchable?
  pattern = "%#{like_escape(term.to_s.downcase)}%"
  arel_table[@name].lower.matches(pattern)
end

#searchable?Boolean

Returns:

  • (Boolean)


81
# File 'lib/stimulus_grid_rails/column.rb', line 81

def searchable? = @searchable && !@computed && !name.to_s.start_with?("_")

#validate(value, row) ⇒ Object

Run client-defined validators. Returns Array of error strings.



116
117
118
119
120
121
122
123
124
125
126
# File 'lib/stimulus_grid_rails/column.rb', line 116

def validate(value, row)
  @validators.flat_map do |v|
    result = v.call(value, row)
    case result
    when nil, true then []
    when String    then [result]
    when Array     then result
    else [result.to_s]
    end
  end
end