Module: StimulusGridRails::TurboStreams

Defined in:
lib/stimulus_grid_rails/turbo_streams_helper.rb

Overview

Custom Turbo Stream actions (RAILS.md §1) — generates <turbo-stream> tags whose ‘action=` is one of: cell, cell-attr, cell-confirm, cell-revert, cell-conflict, row-insert-sorted, row-remove, aggregate, bulk.

The matching client-side StreamActions are registered by app/assets/javascripts/stimulus_grid_rails.js (registerStreamActions).

Use directly:

render turbo_stream: StimulusGridRails::TurboStreams.cell(
  grid: "athletes", row_id: athlete.id, column: :age,
  value: athlete.age, optimistic_id: params[:optimistic_id],
)

Class Method Summary collapse

Class Method Details

.aggregate(grid:, column:, kind:, value:) ⇒ Object

Update a footer aggregate (sum/avg/count/etc.).



73
74
75
76
77
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 73

def aggregate(grid:, column:, kind:, value:)
  tag("aggregate", grid: grid, column: column, kind: kind) do
    ERB::Util.html_escape(value.to_s)
  end
end

.bulk(grid:, streams:) ⇒ Object

Atomic batched stream — wraps N inner streams so the client applies them in a single DOM reflow. Pass an array of already-built turbo-stream strings (other helpers return strings).



82
83
84
85
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 82

def bulk(grid:, streams:)
  inner = streams.join
  tag("bulk", grid: grid) { inner }
end

.cell(grid:, row_id:, column:, value:, optimistic_id: nil) ⇒ Object

Update one cell — value rendered inline as the cell’s textContent. ‘optimistic_id` is echoed back so the originating client can suppress its own broadcast.



20
21
22
23
24
25
26
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 20

def cell(grid:, row_id:, column:, value:, optimistic_id: nil)
  tag("cell", grid: grid, row_id: row_id, column: column,
              optimistic_id: optimistic_id) do
    # Plain HTML — the StreamAction handler reads this as the new cell content.
    ERB::Util.html_escape(value.to_s)
  end
end

.cell_attr(grid:, row_id:, column:, attr:, value:) ⇒ Object

Set an attribute on a cell (e.g. data-dirty=“false”).



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

def cell_attr(grid:, row_id:, column:, attr:, value:)
  tag("cell-attr", grid: grid, row_id: row_id, column: column,
                   attr: attr, value: value)
end

.cell_confirm(grid:, row_id:, column:, value:, optimistic_id:) ⇒ Object

Clear pending/optimistic state on a cell after a successful save.



35
36
37
38
39
40
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 35

def cell_confirm(grid:, row_id:, column:, value:, optimistic_id:)
  tag("cell-confirm", grid: grid, row_id: row_id, column: column,
                      optimistic_id: optimistic_id) do
    ERB::Util.html_escape(value.to_s)
  end
end

.cell_conflict(grid:, row_id:, column:, server_value:, client_value:, optimistic_id:) ⇒ Object

Conflict — server value differs from client base value.



52
53
54
55
56
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 52

def cell_conflict(grid:, row_id:, column:, server_value:, client_value:, optimistic_id:)
  tag("cell-conflict", grid: grid, row_id: row_id, column: column,
                       optimistic_id: optimistic_id,
                       server_value: server_value, client_value: client_value)
end

.cell_revert(grid:, row_id:, column:, value:, errors:, optimistic_id:) ⇒ Object

Restore prior server value + render inline error.



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

def cell_revert(grid:, row_id:, column:, value:, errors:, optimistic_id:)
  tag("cell-revert", grid: grid, row_id: row_id, column: column,
                     optimistic_id: optimistic_id,
                     errors: errors.to_json) do
    ERB::Util.html_escape(value.to_s)
  end
end

.presence(grid:, row_id:, column:, user_id:, user_label:, active:) ⇒ Object

Per-user editing indicator on a cell.



88
89
90
91
92
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 88

def presence(grid:, row_id:, column:, user_id:, user_label:, active:)
  tag("presence", grid: grid, row_id: row_id, column: column,
                  user_id: user_id, user_label: user_label,
                  active: active.to_s)
end

.row_insert_sorted(grid:, row_id:, payload:) ⇒ Object

Insert a row, respecting the client’s current sort order. ‘payload` is a JSON row object; it’s HTML-escaped here and decoded by the client’s textContent read, so names containing & or < stay valid JSON.



61
62
63
64
65
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 61

def row_insert_sorted(grid:, row_id:, payload:)
  tag("row-insert-sorted", grid: grid, row_id: row_id) do
    ERB::Util.html_escape(payload)
  end
end

.row_remove(grid:, row_id:) ⇒ Object

Remove a row by id.



68
69
70
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 68

def row_remove(grid:, row_id:)
  tag("row-remove", grid: grid, row_id: row_id)
end

.tag(action, **attrs) ⇒ Object

Internal — build a <turbo-stream> element. Keys with nil values are dropped. Block content becomes the <template> payload.



96
97
98
99
100
101
102
103
# File 'lib/stimulus_grid_rails/turbo_streams_helper.rb', line 96

def tag(action, **attrs)
  attrs[:action] = action
  kept = attrs.compact
  attr_str = kept.map { |k, v| %(#{k.to_s.tr("_", "-")}="#{ERB::Util.html_escape(v)}") }.join(" ")
  payload = block_given? ? yield : nil
  template = payload ? "<template>#{payload}</template>" : ""
  %(<turbo-stream #{attr_str}>#{template}</turbo-stream>)
end