Module: Phlex::Reactive::Component

Extended by:
ActiveSupport::Concern
Defined in:
lib/phlex/reactive/component.rb

Overview

Component turns a self-contained Phlex component into a Livewire-style reactive unit: declare actions in Ruby, and the generic ‘reactive` Stimulus controller wires clicks/inputs to an HTTP round trip that re-renders the component and morphs it back into the DOM. No per-feature Stimulus controllers, no hand-picked Turbo targets.

Include alongside Phlex::Reactive::Streamable (which provides #id and the re-render machinery).

Security model (the decisive design choice) ===

We do NOT ship component STATE to the browser (no snapshot). The DOM carries a signed IDENTITY:

* Record-backed (the common case): reactive_record :todo signs the
  record's GlobalID. The server re-finds it via GlobalID — the client
  can neither forge the component class nor swap the record. State =
  the database.
* State-backed (record-less, e.g. a counter): reactive_state :count
  signs the listed instance variables. Use only when there is genuinely
  no record to re-find.

Actions are DEFAULT-DENY: only methods declared with ‘action :name` may be invoked. The signature proves the token is ours, NOT that this user may act — your action must still authorize the record. Action params pass through a declared schema; nothing else reaches the method.

Usage (record-backed):

class Todos::Item < ApplicationComponent
  include Phlex::Reactive::Streamable
  include Phlex::Reactive::Component

  reactive_record :todo
  action :toggle
  action :rename, params: { title: :string }

  def initialize(todo:) = @todo = todo
  def id = dom_id(@todo)

  def toggle  = (authorize!(@todo, :update?); @todo.toggle!(:done))
  def rename(title:) = (authorize!(@todo, :update?); @todo.update!(title:))

  def view_template
    li(id:, **reactive_attrs) do
      button(**on(:toggle)) { @todo.done? ? "" : "" }
      span { @todo.title }
    end
  end
end

Defined Under Namespace

Classes: Action

Instance Method Summary collapse

Instance Method Details

#on(action_name, event: "click", **params) ⇒ Object

Attributes for an element that triggers an action.

button(**on(:toggle)) { "○" }
form(**on(:save, event: "submit")) { ... }

Extra keyword args become explicit params merged over collected form fields. For click triggers we force type=“button” so a bare button inside a <form> can’t submit it and cause a full-page navigation.



138
139
140
141
142
143
144
145
146
147
148
# File 'lib/phlex/reactive/component.rb', line 138

def on(action_name, event: "click", **params)
  attrs = {
    data: {
      action: "#{event}->reactive#dispatch",
      reactive_action_param: action_name.to_s,
      reactive_params_param: params.to_json
    }
  }
  attrs[:type] = "button" if event == "click"
  attrs
end

#reactive_attrsObject

Root-element attributes: marks the element reactive and carries the signed identity token. Spread onto the root:

div(id:, **reactive_attrs) { ... }


122
123
124
125
126
127
128
129
# File 'lib/phlex/reactive/component.rb', line 122

def reactive_attrs
  {
    data: {
      controller: "reactive",
      reactive_token_value: reactive_token
    }
  }
end