Class: StimulusGridRails::Grid
- Inherits:
-
Object
- Object
- StimulusGridRails::Grid
- 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
-
.columns_registry ⇒ Object
readonly
Returns the value of attribute columns_registry.
-
.model_class ⇒ Object
readonly
Returns the value of attribute model_class.
-
.resource_name ⇒ Object
readonly
Returns the value of attribute resource_name.
Instance Attribute Summary collapse
-
#user ⇒ Object
readonly
Returns the value of attribute user.
Class Method Summary collapse
- .column(name, **opts) ⇒ Object
- .model(klass) ⇒ Object
-
.resolve_column!(col_id) ⇒ Object
Used by the controller after deserializing the URL.
- .resource(name) ⇒ Object
Instance Method Summary collapse
-
#apply_cell!(row, column, value) ⇒ Object
Called from the controller after a successful coercion + permission check.
- #apply_filters(relation, filters) ⇒ Object
- #apply_search(relation, q) ⇒ Object
-
#apply_sort(relation, sort_model) ⇒ Object
Server-side sort (RAILS.md §21).
-
#build_new_row(overrides = {}) ⇒ Object
Build (unsaved) a new model instance merging defaults with caller overrides.
- #cell_value(row, column) ⇒ Object
- #columns ⇒ Object
-
#format_cell(row, column) ⇒ Object
Renders the value into the DOM.
-
#initialize(user: nil) ⇒ Grid
constructor
A new instance of Grid.
-
#new_row_defaults ⇒ Object
Default attributes for a freshly-created row.
- #row_id(row) ⇒ Object
-
#row_to_h(row) ⇒ Object
Serialize a row to the JSON shape the client grid expects: { id, <col>: <value>, … } including computed columns.
- #row_to_json(row) ⇒ Object
-
#scope(_user = user) ⇒ Object
The base relation a request may see.
-
#search_and_filter(relation, q: nil, filters: {}) ⇒ Object
Apply a global search term + per-column filters to a relation.
-
#serialize_value(v, column) ⇒ Object
JSON-friendly value for row_to_h — numbers stay numeric, dates become ISO strings, everything else stringifies sensibly.
- #visible_columns_for(_row) ⇒ Object
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_registry ⇒ Object (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_class ⇒ Object (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_name ⇒ Object (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
#user ⇒ Object (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.
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. : ["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 |
#columns ⇒ Object
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_defaults ⇒ Object
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 |