Class: Phlex::Reactive::Reply

Inherits:
Object
  • Object
show all
Defined in:
lib/phlex/reactive/reply.rb

Overview

The ONE door for building an action's reply (issue #182). Component#reply returns one, so an action writes

reply.replace.flash(:error, msg)

There is no constant to qualify (reply is a method, resolved on the component — so a namespaced component needs no Response = … alias) and no self to thread (the component is bound).

Reply is NOT a Response and does NOT subclass one: each verb calls the Response build_* class method, supplying the bound component as the subject, and returns the real, frozen Response value the endpoint honors. So immutability, chaining (.flash/.stream/.also/.js/.defer), and render_self? come from the Response value object untouched — the chain is plain Response all the way down.

Response remains the value object the endpoint reads; you never construct it directly (its former public class verbs raise a guided rewrite).

Instance Method Summary collapse

Constructor Details

#initialize(component) ⇒ Reply

Returns a new instance of Reply.



24
25
26
# File 'lib/phlex/reactive/reply.rb', line 24

def initialize(component)
  @component = component
end

Instance Method Details

#append(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, effect: nil, **row_kwargs) ⇒ Object

Reactive collections (issue #35) — add/remove a row in a declared reactive_collection, emitting the row stream + the count companion + the empty-state toggle as ONE Response. The bound component is the container (it carries the declaration + size resolver). The collection is named by the to:/from: keyword (issue #182 — reads as Ruby, and a to: present unambiguously means "a collection row"):

def add_item(...)    = (item = @list.items.create!(...); reply.append(item, to: :items))
def remove_item(id:) = (@list.items.find(id).destroy!;   reply.remove(id, from: :items))

model is the row's record; remove also accepts the row's dom-id string. Extra kwargs (issue #186) thread to the row component's new(model:, **row_kwargs) — e.g. reply.append(item, to: :items, autofocus: true) → ItemRow.new(item:, autofocus: true). Additive: a no-kwarg call is byte-identical to before. effect: (issue #215) is reserved as the row's per-call effect — it is NOT forwarded to the row component's init like the other row_kwargs.



67
68
69
# File 'lib/phlex/reactive/reply.rb', line 67

def append(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, effect: nil, **row_kwargs)
  collection_build!(:build_collection_append, :append, model, legacy_model, to, effect, row_kwargs)
end

#defer(component, placeholder: nil, morph: false) ⇒ Object

Defer an expensive segment (issue #165): reply.defer(Totals.new(...)).

With NO prior verb, the bound component's token is refreshed via a tiny token-only stream (reply.streams), NOT a full self-replace: a full replace would re-render the acting component SYNCHRONOUSLY on the request thread — and if you defer your OWN subject (reply.defer(self)), that is the very render you meant to move OFF the critical path, so the defer would be silently defeated (a double render). The token-only refresh rolls the signed identity forward without any synchronous render; the deferred segment's directive rides alongside. Chain off a verb when you DO want a synchronous piece too:

reply.streams(volume_cell).defer(SessionTotals.new(workout: @workout))
reply.replace.defer(Totals.new(order: @order))   # explicit self re-render


128
129
130
# File 'lib/phlex/reactive/reply.rb', line 128

def defer(component, placeholder: nil, morph: false)
  Response.build_streams(@component).defer(component, placeholder:, morph:)
end

#morph(effect: nil) ⇒ Object

Re-render in place via Idiomorph (method="morph") — keeps focus + caret.



40
41
42
# File 'lib/phlex/reactive/reply.rb', line 40

def morph(effect: nil)
  Response.build_morph(@component, effect:)
end

#prepend(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, effect: nil, **row_kwargs) ⇒ Object



71
72
73
# File 'lib/phlex/reactive/reply.rb', line 71

def prepend(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, effect: nil, **row_kwargs)
  collection_build!(:build_collection_prepend, :prepend, model, legacy_model, to, effect, row_kwargs)
end

#redirect(url) ⇒ Object

Client-side full navigation (Turbo.visit). Pass a *_url.



98
99
100
# File 'lib/phlex/reactive/reply.rb', line 98

def redirect(url)
  Response.build_redirect(url)
end

#remove(model = UNSET_ARG, legacy_model = UNSET_ARG, from: UNSET_ARG, effect: nil) ⇒ Object

Two forms, dispatched on the PRESENCE of from: (issue #182 — no sentinel overload): bare reply.remove removes the bound component's own element; reply.remove(model, from: :items) removes a collection row + count + empty-state. model may be the record or the row's dom-id string.

reply.remove                       # remove the bound component's element
reply.remove(model, from: :items)  # remove a collection row


82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/phlex/reactive/reply.rb', line 82

def remove(model = UNSET_ARG, legacy_model = UNSET_ARG, from: UNSET_ARG, effect: nil)
  reject_legacy_form!(:remove, model, legacy_model)
  # Bare `reply.remove` (no positional, no from:) removes self.
  return Response.build_remove(@component, effect:) if from.equal?(UNSET_ARG) && model.equal?(UNSET_ARG)

  if from.equal?(UNSET_ARG)
    raise ArgumentError,
      "reply.remove(model, …) needs a source collection — reply.remove(model, from: :name)"
  end
  reject_symbol_first!(:remove, model, :from, from)
  Response.build_collection_remove(@component, from, resolve_row_arg(model), effect:)
end

#replace(morph: false, effect: nil) ⇒ Object

Re-render in place. morph: true morphs the subtree (preserves the focused input + caret) instead of an outerHTML swap — see #morph. effect: (issue #215) — the per-call effect override for this one stream (a built-in name, :random, false to suppress, or #186 legs); same kwarg on every verb below.



35
36
37
# File 'lib/phlex/reactive/reply.rb', line 35

def replace(morph: false, effect: nil)
  Response.build_replace(@component, morph:, effect:)
end

#streams(*strings) ⇒ Object

Self-targeting again: emit exactly these streams with a TOKEN-ONLY refresh (issue #30) — partial/per-field update, NO full-self replace, so the component's live inputs survive. The bound component supplies the token.



110
111
112
# File 'lib/phlex/reactive/reply.rb', line 110

def streams(*strings)
  Response.build_streams(@component, *strings)
end

#update(morph: false, effect: nil) ⇒ Object

Update only inner HTML (preserves the root element + its token attr). morph: true morphs the inner HTML in place (issue #113) so a cross-tab/per-field update keeps a focused input's caret.



47
48
49
# File 'lib/phlex/reactive/reply.rb', line 47

def update(morph: false, effect: nil)
  Response.build_update(@component, morph:, effect:)
end

#with(*strings) ⇒ Object

Escape hatch / multi-stream root: zero or more raw turbo-stream strings.



103
104
105
# File 'lib/phlex/reactive/reply.rb', line 103

def with(*strings)
  Response.build_with(*strings)
end