Class: StimulusGridRails::Grid

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

Overview

Base class for declaring a server-side grid. RAILS.md §7 — one source of truth per resource. All editor selection, auth, coercion, validation, broadcasting flows through here.

Subclass and declare:

class AthleteGrid < StimulusGridRails::Grid
  resource :athletes
  model    Athlete

  column :athlete,  type: :string,  editable: true, width: 200, pinned: :left
  column :country,  type: :string,  editable: ->(row, user) { user.admin? }
  column :age,      type: :integer, editable: true, validate: ->(v, _r) { "must be > 0" if v <= 0 }
  column :sport,    type: :enum,    editable: true, enum_values: %w[Swimming Cycling Gymnastics]
  column :date,     type: :date,    editable: true
  column :gold,     type: :integer, editable: true
  column :silver,   type: :integer, editable: true
  column :bronze,   type: :integer, editable: true
  column :total,    type: :integer, computed: true, depends_on: %i[gold silver bronze]

  def compute_total(row) = row.gold + row.silver + row.bronze
end

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user: nil) ⇒ Grid

Returns a new instance of Grid.



55
56
57
# File 'lib/stimulus_grid_rails/grid.rb', line 55

def initialize(user: nil)
  @user = user
end

Class Attribute Details

.columns_registryObject (readonly)

Returns the value of attribute columns_registry.



29
30
31
# File 'lib/stimulus_grid_rails/grid.rb', line 29

def columns_registry
  @columns_registry
end

.model_classObject (readonly)

Returns the value of attribute model_class.



29
30
31
# File 'lib/stimulus_grid_rails/grid.rb', line 29

def model_class
  @model_class
end

.resource_nameObject (readonly)

Returns the value of attribute resource_name.



29
30
31
# File 'lib/stimulus_grid_rails/grid.rb', line 29

def resource_name
  @resource_name
end

Instance Attribute Details

#userObject (readonly)

Returns the value of attribute user.



53
54
55
# File 'lib/stimulus_grid_rails/grid.rb', line 53

def user
  @user
end

Class Method Details

.column(name, **opts) ⇒ Object



40
41
42
43
# File 'lib/stimulus_grid_rails/grid.rb', line 40

def column(name, **opts)
  @columns_registry ||= {}
  @columns_registry[name.to_sym] = Column.new(name, **opts)
end

.model(klass) ⇒ Object



36
37
38
# File 'lib/stimulus_grid_rails/grid.rb', line 36

def model(klass)
  @model_class = klass
end

.resolve_column!(col_id) ⇒ Object

Used by the controller after deserializing the URL.

Raises:

  • (ArgumentError)


46
47
48
49
50
# File 'lib/stimulus_grid_rails/grid.rb', line 46

def resolve_column!(col_id)
  column = columns_registry[col_id.to_sym]
  raise ArgumentError, "Unknown column #{col_id} on #{name}" unless column
  column
end

.resource(name) ⇒ Object



31
32
33
34
# File 'lib/stimulus_grid_rails/grid.rb', line 31

def resource(name)
  @resource_name = name.to_s
  StimulusGridRails.register_grid(@resource_name, self)
end

Instance Method Details

#apply_cell!(row, column, value) ⇒ Object

Called from the controller after a successful coercion + permission check. Returns [success?, mutations_to_broadcast] where mutations is an array of [row_id, col_id, value, opts]. For computed cascade, runs the dependent column’s compute_X methods and includes those too.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/stimulus_grid_rails/grid.rb', line 71

def apply_cell!(row, column, value)
  errors = column.validate(value, row)
  return [false, errors, []] if errors.any?

  old_value = row.send(column.name)
  row.send("#{column.name}=", value)
  mutations = [[row_id(row), column.name.to_s, value, {}]]

  # Cascade — recompute every column declared as depending on this one.
  self.class.columns_registry.each_value do |c|
    next unless c.computed? && c.depends_on.include?(column.name)
    compute_method = "compute_#{c.name}"
    if respond_to?(compute_method)
      new_val = public_send(compute_method, row)
      row.send("#{c.name}=", new_val) if row.respond_to?("#{c.name}=")
      mutations << [row_id(row), c.name.to_s, new_val, {}]
    end
  end

  saved = row.respond_to?(:save) ? row.save : true
  if saved
    [true, [], mutations]
  else
    # Restore old value so the in-memory row doesn't carry the failed write.
    row.send("#{column.name}=", old_value)
    [false, Array(row.respond_to?(:errors) ? row.errors.full_messages : ["save failed"]), []]
  end
end

#apply_filters(relation, filters) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/stimulus_grid_rails/grid.rb', line 157

def apply_filters(relation, filters)
  return relation if filters.blank?
  table = self.class.model_class.arel_table
  filters.each do |col_name, criteria|
    next if criteria.blank?
    col = self.class.columns_registry[col_name.to_sym]
    next unless col
    pred = col.filter_predicate(table, criteria)
    relation = relation.where(pred) if pred
  end
  relation
end

#apply_search(relation, q) ⇒ Object



149
150
151
152
153
154
155
# File 'lib/stimulus_grid_rails/grid.rb', line 149

def apply_search(relation, q)
  return relation if q.blank?
  table = self.class.model_class.arel_table
  preds = columns.filter_map { |c| c.search_predicate(table, q) }
  return relation if preds.empty?
  relation.where(preds.reduce(:or))
end

#apply_sort(relation, sort_model) ⇒ Object

Server-side sort (RAILS.md §21). ‘sort_model` is the client shape: [{ “colId” =>, “sort” => “asc”|“desc” }, …]. Only real (non-computed, non-underscore) columns that exist on the model are honored.



173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/stimulus_grid_rails/grid.rb', line 173

def apply_sort(relation, sort_model)
  return relation if sort_model.blank?
  table  = self.class.model_class.arel_table
  names  = self.class.model_class.column_names
  orders = Array(sort_model).filter_map do |entry|
    col_id = (entry["colId"] || entry[:colId]).to_s
    col    = self.class.columns_registry[col_id.to_sym]
    next unless col && !col.computed? && !col_id.start_with?("_") && names.include?(col_id)
    dir = (entry["sort"] || entry[:sort]).to_s.downcase == "desc" ? :desc : :asc
    table[col_id.to_sym].public_send(dir)
  end
  orders.empty? ? relation : relation.reorder(*orders)
end

#build_new_row(overrides = {}) ⇒ Object

Build (unsaved) a new model instance merging defaults with caller overrides.



112
113
114
115
# File 'lib/stimulus_grid_rails/grid.rb', line 112

def build_new_row(overrides = {})
  attrs = new_row_defaults.merge((overrides || {}).symbolize_keys)
  self.class.model_class.new(attrs)
end

#cell_value(row, column) ⇒ Object



187
188
189
190
191
192
193
# File 'lib/stimulus_grid_rails/grid.rb', line 187

def cell_value(row, column)
  if column.computed?
    method = "compute_#{column.name}"
    return respond_to?(method) ? public_send(method, row) : nil
  end
  row.respond_to?(column.name) ? row.send(column.name) : row[column.name]
end

#columnsObject



59
60
61
# File 'lib/stimulus_grid_rails/grid.rb', line 59

def columns
  self.class.columns_registry.values
end

#format_cell(row, column) ⇒ Object

Renders the value into the DOM. Override per-column or per-type in subclasses for richer renderers.



197
198
199
200
201
202
203
204
205
206
# File 'lib/stimulus_grid_rails/grid.rb', line 197

def format_cell(row, column)
  v = cell_value(row, column)
  case column.type
  when :money   then ActiveSupport::NumberHelper.number_to_currency(v) rescue v.to_s
  when :date    then v.respond_to?(:to_date) ? v.to_date.iso8601 : v.to_s
  when :datetime then v.respond_to?(:iso8601) ? v.iso8601 : v.to_s
  when :boolean then v ? "" : ""
  else v.to_s
  end
end

#new_row_defaultsObject

Default attributes for a freshly-created row. Override in subclasses.



107
108
109
# File 'lib/stimulus_grid_rails/grid.rb', line 107

def new_row_defaults
  {}
end

#row_id(row) ⇒ Object



100
101
102
# File 'lib/stimulus_grid_rails/grid.rb', line 100

def row_id(row)
  row.respond_to?(:id) ? row.id : row[:id]
end

#row_to_h(row) ⇒ Object

Serialize a row to the JSON shape the client grid expects: { id, <col>: <value>, … } including computed columns. Used as the row-insert-sorted payload.



119
120
121
122
123
124
125
126
# File 'lib/stimulus_grid_rails/grid.rb', line 119

def row_to_h(row)
  h = { "id" => row_id(row) }
  self.class.columns_registry.each_value do |col|
    next if col.name.to_s.start_with?("_")   # skip action/renderer-only columns
    h[col.name.to_s] = serialize_value(cell_value(row, col), col)
  end
  h
end

#row_to_json(row) ⇒ Object



128
129
130
# File 'lib/stimulus_grid_rails/grid.rb', line 128

def row_to_json(row)
  JSON.generate(row_to_h(row))
end

#scope(_user = user) ⇒ Object

The base relation a request may see. Override for per-user authorization scoping (e.g. ‘model_class.where(team: user.team)`).



136
137
138
# File 'lib/stimulus_grid_rails/grid.rb', line 136

def scope(_user = user)
  self.class.model_class.all
end

#search_and_filter(relation, q: nil, filters: {}) ⇒ Object

Apply a global search term + per-column filters to a relation. ‘filters` is { col_name => { “type” =>, “value” =>, “value2” => } } (the client filterModel shape). Unknown columns and unparseable values are ignored.



143
144
145
146
147
# File 'lib/stimulus_grid_rails/grid.rb', line 143

def search_and_filter(relation, q: nil, filters: {})
  relation = apply_search(relation, q)
  relation = apply_filters(relation, filters)
  relation
end

#serialize_value(v, column) ⇒ Object

JSON-friendly value for row_to_h — numbers stay numeric, dates become ISO strings, everything else stringifies sensibly.



210
211
212
213
214
215
216
217
218
219
# File 'lib/stimulus_grid_rails/grid.rb', line 210

def serialize_value(v, column)
  case column.type
  when :integer, :bigint then v.to_i
  when :decimal, :money then v.to_f
  when :boolean        then !!v
  when :date           then v.respond_to?(:to_date) ? v.to_date.iso8601 : v
  when :datetime       then v.respond_to?(:iso8601) ? v.iso8601 : v
  else v
  end
end

#visible_columns_for(_row) ⇒ Object



63
64
65
# File 'lib/stimulus_grid_rails/grid.rb', line 63

def visible_columns_for(_row)
  columns
end