phlex-reactive 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +246 -0
- data/README.md +127 -78
- data/app/controllers/phlex/reactive/actions_controller.rb +28 -8
- data/app/javascript/phlex/reactive/confirm_predicate.js +52 -0
- data/app/javascript/phlex/reactive/confirm_predicate.min.js +4 -0
- data/app/javascript/phlex/reactive/confirm_predicate.min.js.map +10 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +370 -179
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/component/dsl.rb +122 -34
- data/lib/phlex/reactive/component/helpers.rb +307 -165
- data/lib/phlex/reactive/component/registry.rb +6 -1
- data/lib/phlex/reactive/component.rb +3 -3
- data/lib/phlex/reactive/defer.rb +1 -1
- data/lib/phlex/reactive/deferred_render_job.rb +2 -2
- data/lib/phlex/reactive/engine.rb +14 -0
- data/lib/phlex/reactive/js.rb +29 -15
- data/lib/phlex/reactive/reply.rb +105 -44
- data/lib/phlex/reactive/response.rb +139 -48
- data/lib/phlex/reactive/streamable.rb +229 -199
- data/lib/phlex/reactive/test_helpers.rb +1 -1
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +137 -12
- metadata +4 -1
|
@@ -53,6 +53,8 @@ module Phlex
|
|
|
53
53
|
record_key: :@reactive_own_record_key,
|
|
54
54
|
# Form-field namespace — reactive_scope (issue #180).
|
|
55
55
|
scope: :@reactive_own_scope,
|
|
56
|
+
# Dirty-tracking config — reactive_dirty (issue #184).
|
|
57
|
+
dirty: :@reactive_own_dirty,
|
|
56
58
|
# Deferred reply segments — reactive_lazy (issue #165).
|
|
57
59
|
lazy: :@reactive_own_lazy,
|
|
58
60
|
# verify_authorized opt-out (issue #168): a scalar bare flag (skip the
|
|
@@ -124,7 +126,10 @@ module Phlex
|
|
|
124
126
|
fetch(klass, kind) do
|
|
125
127
|
inherited = inherited_value(klass, reader)
|
|
126
128
|
own = own(klass, kind)
|
|
127
|
-
|
|
129
|
+
# FROZEN (issue #186): the resolved hash is the public dispatch table
|
|
130
|
+
# (reactive_actions[:x] is the default-deny source of truth) — handing
|
|
131
|
+
# out a mutable memo would let a caller corrupt it. Mutation raises.
|
|
132
|
+
(inherited || EMPTY_HASH).merge(own || EMPTY_HASH).freeze
|
|
128
133
|
end
|
|
129
134
|
end
|
|
130
135
|
|
|
@@ -6,9 +6,9 @@ module Phlex
|
|
|
6
6
|
# reactive unit: declare actions in Ruby, and the generic `reactive`
|
|
7
7
|
# Stimulus controller wires clicks/inputs to an HTTP round trip that
|
|
8
8
|
# re-renders the component and applies it back into the DOM (a plain replace
|
|
9
|
-
# by default; return
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# by default; return reply.morph to morph in place and keep the focused
|
|
10
|
+
# input — issue #28). No per-feature Stimulus controllers, no hand-picked
|
|
11
|
+
# Turbo targets.
|
|
12
12
|
#
|
|
13
13
|
# Including Component pulls in Phlex::Reactive::Streamable automatically
|
|
14
14
|
# (Concern dependency — Streamable lands on the base first, exactly the old
|
data/lib/phlex/reactive/defer.rb
CHANGED
|
@@ -309,7 +309,7 @@ data-reactive-defer-pending="true" aria-busy="true">#{inner}</div>)
|
|
|
309
309
|
end
|
|
310
310
|
|
|
311
311
|
# Resolve the segment's placeholder to inner HTML — nil means "no
|
|
312
|
-
# shell". Same content contract as flash/
|
|
312
|
+
# shell". Same content contract as flash/also: a Phlex component
|
|
313
313
|
# renders through the configured renderer (auto-escaped), a plain
|
|
314
314
|
# String is DATA and gets escaped, an html_safe String is intentional
|
|
315
315
|
# markup and passes verbatim (ERB::Util.html_escape's own contract).
|
|
@@ -32,7 +32,7 @@ module Phlex
|
|
|
32
32
|
# — it raises loudly and broadcasts NOTHING, OUTSIDE the render rescue
|
|
33
33
|
# below (a component with actions can't be forged past the signed
|
|
34
34
|
# identity anyway, so this never fires for a real defer).
|
|
35
|
-
unless klass.respond_to?(:
|
|
35
|
+
unless klass.respond_to?(:reactive_actions) && klass.include?(Phlex::Reactive::Component)
|
|
36
36
|
raise ::ArgumentError,
|
|
37
37
|
"#{component_class_name} is not a reactive component — refusing the deferred render"
|
|
38
38
|
end
|
|
@@ -57,7 +57,7 @@ module Phlex
|
|
|
57
57
|
component = klass.from_identity(payload)
|
|
58
58
|
return broadcast_cleanup(stream_key, target) if component.respond_to?(:render?) && !component.render?
|
|
59
59
|
|
|
60
|
-
stream =
|
|
60
|
+
stream = component.to_stream_replace(morph:)
|
|
61
61
|
broadcast_payload(stream_key, stream.to_s + teardown_stream(target))
|
|
62
62
|
rescue ::StandardError => e
|
|
63
63
|
log_deferred_failure(e) unless e.is_a?(::ActiveRecord::RecordNotFound)
|
|
@@ -35,6 +35,8 @@ module Phlex
|
|
|
35
35
|
phlex/reactive/reactive_controller.min.js.map
|
|
36
36
|
phlex/reactive/confirm.min.js
|
|
37
37
|
phlex/reactive/confirm.min.js.map
|
|
38
|
+
phlex/reactive/confirm_predicate.min.js
|
|
39
|
+
phlex/reactive/confirm_predicate.min.js.map
|
|
38
40
|
phlex/reactive/compute.min.js
|
|
39
41
|
phlex/reactive/compute.min.js.map
|
|
40
42
|
phlex/reactive/inspect.min.js
|
|
@@ -76,6 +78,16 @@ module Phlex
|
|
|
76
78
|
to: "phlex/reactive/compute.min.js",
|
|
77
79
|
preload: true
|
|
78
80
|
)
|
|
81
|
+
# The client-side confirm-predicate registry (issue #179) — the
|
|
82
|
+
# multi-field escape hatch for conditional confirmation. Same bare-specifier
|
|
83
|
+
# rationale as confirm/compute: reactive_controller.js imports it, and an app
|
|
84
|
+
# registers predicates via `import { setConfirmPredicate } from
|
|
85
|
+
# "phlex/reactive/confirm_predicate"` — both resolve through this pin.
|
|
86
|
+
it.importmap.pin(
|
|
87
|
+
"phlex/reactive/confirm_predicate",
|
|
88
|
+
to: "phlex/reactive/confirm_predicate.min.js",
|
|
89
|
+
preload: true
|
|
90
|
+
)
|
|
79
91
|
# The on-demand client inspector (issue #168). NOT preloaded — it is a
|
|
80
92
|
# dev/debugging tool loaded only when you `import("phlex/reactive/inspect")`
|
|
81
93
|
# from the console, so no page pays for it. The pin makes that dynamic
|
|
@@ -116,6 +128,8 @@ module Phlex
|
|
|
116
128
|
# frozen registry makes runtime registration a loud error rather than a
|
|
117
129
|
# never-validated type. Idempotent.
|
|
118
130
|
Phlex::Reactive.freeze_param_types!
|
|
131
|
+
# Freeze the named-schema registry the same way (issue #184).
|
|
132
|
+
Phlex::Reactive.freeze_param_schemas!
|
|
119
133
|
end
|
|
120
134
|
end
|
|
121
135
|
end
|
data/lib/phlex/reactive/js.rb
CHANGED
|
@@ -54,6 +54,19 @@ module Phlex
|
|
|
54
54
|
# gets the SAME server-side default-deny as the JS chain (defense in depth;
|
|
55
55
|
# the client also enforces it). Non-attr ops and malformed entries pass
|
|
56
56
|
# through untouched — the client interpreter default-denies unknown ops.
|
|
57
|
+
# :root -> the sentinel; a String passes through as a CSS selector. Shared
|
|
58
|
+
# by the js op builder AND the on() pending-state hint normalizer (issue
|
|
59
|
+
# #181) so BOTH resolve a `to:` target through one code path. Anything else
|
|
60
|
+
# (a stray symbol, nil) is a bug at the call site — raise at render time
|
|
61
|
+
# instead of silently matching nothing in the browser.
|
|
62
|
+
def self.normalize_target(to)
|
|
63
|
+
return ROOT_SENTINEL if to == :root
|
|
64
|
+
return to if to.is_a?(String)
|
|
65
|
+
|
|
66
|
+
raise ArgumentError,
|
|
67
|
+
"target must be :root or a CSS selector string, got #{to.inspect}"
|
|
68
|
+
end
|
|
69
|
+
|
|
57
70
|
def self.assert_ops_allowed!(list)
|
|
58
71
|
Array(list).each do |op, args|
|
|
59
72
|
next unless ATTR_NAME_OPS.include?(op.to_s) && args.is_a?(::Hash)
|
|
@@ -227,15 +240,23 @@ module Phlex
|
|
|
227
240
|
self.class.assert_allowed_attr(name)
|
|
228
241
|
end
|
|
229
242
|
|
|
230
|
-
# A transition
|
|
243
|
+
# A transition is NAMED legs { during:, from:, to: } (issue #186), compiled to
|
|
244
|
+
# the [during, from, to] wire array (zero client change). The old positional
|
|
245
|
+
# Array form is removed — it raises with the caller's OWN values slotted into
|
|
246
|
+
# the named form, so the rewrite is copy-pasteable.
|
|
231
247
|
def normalize_transition(transition)
|
|
232
|
-
|
|
233
|
-
|
|
248
|
+
if transition.is_a?(Array)
|
|
249
|
+
d, f, t = transition
|
|
234
250
|
raise ArgumentError,
|
|
235
|
-
"#{self.class}: transition:
|
|
251
|
+
"#{self.class}: transition: takes named legs (issue #186) — " \
|
|
252
|
+
"transition: { during: #{d.inspect}, from: #{f.inspect}, to: #{t.inspect} }"
|
|
253
|
+
end
|
|
254
|
+
unless transition.is_a?(Hash) && %i[during from to].all? { transition.key?(it) }
|
|
255
|
+
raise ArgumentError,
|
|
256
|
+
"#{self.class}: transition: must name during:, from:, to: class lists, got #{transition.inspect}"
|
|
236
257
|
end
|
|
237
258
|
|
|
238
|
-
|
|
259
|
+
[transition[:during], transition[:from], transition[:to]].map(&:to_s).freeze
|
|
239
260
|
end
|
|
240
261
|
|
|
241
262
|
def class_args(to, classes, global:)
|
|
@@ -248,16 +269,9 @@ module Phlex
|
|
|
248
269
|
args.freeze
|
|
249
270
|
end
|
|
250
271
|
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
def normalize_target(to)
|
|
255
|
-
return ROOT_SENTINEL if to == :root
|
|
256
|
-
return to if to.is_a?(String)
|
|
257
|
-
|
|
258
|
-
raise ArgumentError,
|
|
259
|
-
"#{self.class}: target must be :root or a CSS selector string, got #{to.inspect}"
|
|
260
|
-
end
|
|
272
|
+
# Instance-side alias for the class method (used by the op builder). See
|
|
273
|
+
# JS.normalize_target — the shared :root/selector translation.
|
|
274
|
+
def normalize_target(to) = self.class.normalize_target(to)
|
|
261
275
|
end
|
|
262
276
|
end
|
|
263
277
|
end
|
data/lib/phlex/reactive/reply.rb
CHANGED
|
@@ -2,35 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module Phlex
|
|
4
4
|
module Reactive
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# The ONE door for building an action's reply (issue #182). Component#reply
|
|
6
|
+
# returns one, so an action writes
|
|
7
7
|
#
|
|
8
8
|
# reply.replace.flash(:error, msg)
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# There is no constant to qualify (reply is a method, resolved on the
|
|
11
|
+
# component — so a namespaced component needs no `Response = …` alias) and no
|
|
12
|
+
# `self` to thread (the component is bound).
|
|
11
13
|
#
|
|
12
|
-
#
|
|
14
|
+
# Reply is NOT a Response and does NOT subclass one: each verb calls the
|
|
15
|
+
# Response `build_*` class method, supplying the bound component as the
|
|
16
|
+
# subject, and returns the real, frozen Response value the endpoint honors. So
|
|
17
|
+
# immutability, chaining (.flash/.stream/.also/.js/.defer), and render_self?
|
|
18
|
+
# come from the Response value object untouched — the chain is plain Response
|
|
19
|
+
# all the way down.
|
|
13
20
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# `Response = …` alias) and no `self` to thread (the component is bound).
|
|
17
|
-
#
|
|
18
|
-
# Reply is NOT a Response and does NOT subclass one: each verb forwards to
|
|
19
|
-
# the Response class method, supplying the bound component as the subject, and
|
|
20
|
-
# returns the real, frozen Response value the endpoint already honors. So
|
|
21
|
-
# immutability, chaining (.flash/.stream/.also_update/.also_replace), and
|
|
22
|
-
# render_self? are inherited untouched — and there is no "verb after a chained
|
|
23
|
-
# Response" asymmetry, because the chain is plain Response all the way down.
|
|
24
|
-
#
|
|
25
|
-
# Response remains the public value object (it's what the endpoint reads); it
|
|
26
|
-
# is simply an internal detail you rarely name directly now.
|
|
21
|
+
# Response remains the value object the endpoint reads; you never construct it
|
|
22
|
+
# directly (its former public class verbs raise a guided rewrite).
|
|
27
23
|
class Reply
|
|
28
|
-
# Distinguishes `reply.remove` (no args — remove the bound component
|
|
29
|
-
# itself) from `reply.remove(:items, model)` (remove a collection row).
|
|
30
|
-
# A sentinel, not nil, so `reply.remove(:items, nil)` stays unambiguous.
|
|
31
|
-
UNSET = Object.new
|
|
32
|
-
private_constant :UNSET
|
|
33
|
-
|
|
34
24
|
def initialize(component)
|
|
35
25
|
@component = component
|
|
36
26
|
end
|
|
@@ -40,70 +30,86 @@ module Phlex
|
|
|
40
30
|
# Re-render in place. `morph: true` morphs the subtree (preserves the
|
|
41
31
|
# focused input + caret) instead of an outerHTML swap — see #morph.
|
|
42
32
|
def replace(morph: false)
|
|
43
|
-
Response.
|
|
33
|
+
Response.build_replace(@component, morph:)
|
|
44
34
|
end
|
|
45
35
|
|
|
46
36
|
# Re-render in place via Idiomorph (method="morph") — keeps focus + caret.
|
|
47
37
|
def morph
|
|
48
|
-
Response.
|
|
38
|
+
Response.build_morph(@component)
|
|
49
39
|
end
|
|
50
40
|
|
|
51
41
|
# Update only inner HTML (preserves the root element + its token attr).
|
|
52
42
|
# `morph: true` morphs the inner HTML in place (issue #113) so a
|
|
53
43
|
# cross-tab/per-field update keeps a focused input's caret.
|
|
54
44
|
def update(morph: false)
|
|
55
|
-
Response.
|
|
45
|
+
Response.build_update(@component, morph:)
|
|
56
46
|
end
|
|
57
47
|
|
|
58
48
|
# Reactive collections (issue #35) — add/remove a row in a declared
|
|
59
49
|
# reactive_collection, emitting the row stream + the count companion +
|
|
60
50
|
# the empty-state toggle as ONE Response. The bound component is the
|
|
61
|
-
# container (it carries the declaration + size resolver).
|
|
51
|
+
# container (it carries the declaration + size resolver). The collection is
|
|
52
|
+
# named by the `to:`/`from:` keyword (issue #182 — reads as Ruby, and a
|
|
53
|
+
# `to:` present unambiguously means "a collection row"):
|
|
62
54
|
#
|
|
63
|
-
# def add_item(...) = (item = @list.items.create!(...); reply.append(:items
|
|
64
|
-
# def remove_item(id:) = (@list.items.find(id).destroy!; reply.remove(:items
|
|
55
|
+
# def add_item(...) = (item = @list.items.create!(...); reply.append(item, to: :items))
|
|
56
|
+
# def remove_item(id:) = (@list.items.find(id).destroy!; reply.remove(id, from: :items))
|
|
65
57
|
#
|
|
66
58
|
# `model` is the row's record; remove also accepts the row's dom-id string.
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
# Extra kwargs (issue #186) thread to the row component's new(model:, **row_kwargs)
|
|
60
|
+
# — e.g. reply.append(item, to: :items, autofocus: true) → ItemRow.new(item:,
|
|
61
|
+
# autofocus: true). Additive: a no-kwarg call is byte-identical to before.
|
|
62
|
+
def append(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, **row_kwargs)
|
|
63
|
+
collection_build!(:build_collection_append, :append, model, legacy_model, to, row_kwargs)
|
|
69
64
|
end
|
|
70
65
|
|
|
71
|
-
def prepend(
|
|
72
|
-
|
|
66
|
+
def prepend(model = UNSET_ARG, legacy_model = UNSET_ARG, to: UNSET_ARG, **row_kwargs)
|
|
67
|
+
collection_build!(:build_collection_prepend, :prepend, model, legacy_model, to, row_kwargs)
|
|
73
68
|
end
|
|
74
69
|
|
|
75
|
-
# Two forms
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
# Two forms, dispatched on the PRESENCE of `from:` (issue #182 — no sentinel
|
|
71
|
+
# overload): bare `reply.remove` removes the bound component's own element;
|
|
72
|
+
# `reply.remove(model, from: :items)` removes a collection row + count +
|
|
73
|
+
# empty-state. `model` may be the record or the row's dom-id string.
|
|
74
|
+
#
|
|
75
|
+
# reply.remove # remove the bound component's element
|
|
76
|
+
# reply.remove(model, from: :items) # remove a collection row
|
|
77
|
+
def remove(model = UNSET_ARG, legacy_model = UNSET_ARG, from: UNSET_ARG)
|
|
78
|
+
reject_legacy_form!(:remove, model, legacy_model)
|
|
79
|
+
# Bare `reply.remove` (no positional, no from:) removes self.
|
|
80
|
+
return Response.build_remove(@component) if from.equal?(UNSET_ARG) && model.equal?(UNSET_ARG)
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
if from.equal?(UNSET_ARG)
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"reply.remove(model, …) needs a source collection — reply.remove(model, from: :name)"
|
|
85
|
+
end
|
|
86
|
+
reject_symbol_first!(:remove, model, :from, from)
|
|
87
|
+
Response.build_collection_remove(@component, from, resolve_row_arg(model))
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
# Subject-free builders — pass straight through so they read naturally.
|
|
85
91
|
|
|
86
92
|
# Client-side full navigation (Turbo.visit). Pass a *_url.
|
|
87
93
|
def redirect(url)
|
|
88
|
-
Response.
|
|
94
|
+
Response.build_redirect(url)
|
|
89
95
|
end
|
|
90
96
|
|
|
91
97
|
# Escape hatch / multi-stream root: zero or more raw turbo-stream strings.
|
|
92
98
|
def with(*strings)
|
|
93
|
-
Response.
|
|
99
|
+
Response.build_with(*strings)
|
|
94
100
|
end
|
|
95
101
|
|
|
96
102
|
# Self-targeting again: emit exactly these streams with a TOKEN-ONLY refresh
|
|
97
103
|
# (issue #30) — partial/per-field update, NO full-self replace, so the
|
|
98
104
|
# component's live inputs survive. The bound component supplies the token.
|
|
99
105
|
def streams(*strings)
|
|
100
|
-
Response.
|
|
106
|
+
Response.build_streams(@component, *strings)
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
# Defer an expensive segment (issue #165): `reply.defer(Totals.new(...))`.
|
|
104
110
|
#
|
|
105
111
|
# With NO prior verb, the bound component's token is refreshed via a tiny
|
|
106
|
-
# token-only stream (
|
|
112
|
+
# token-only stream (reply.streams), NOT a full self-replace: a full
|
|
107
113
|
# replace would re-render the acting component SYNCHRONOUSLY on the request
|
|
108
114
|
# thread — and if you defer your OWN subject (`reply.defer(self)`), that is
|
|
109
115
|
# the very render you meant to move OFF the critical path, so the defer
|
|
@@ -115,7 +121,62 @@ module Phlex
|
|
|
115
121
|
# reply.streams(volume_cell).defer(SessionTotals.new(workout: @workout))
|
|
116
122
|
# reply.replace.defer(Totals.new(order: @order)) # explicit self re-render
|
|
117
123
|
def defer(component, placeholder: nil, morph: false)
|
|
118
|
-
Response.
|
|
124
|
+
Response.build_streams(@component).defer(component, placeholder:, morph:)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Sentinel distinguishing "keyword omitted" from an explicit nil value, so
|
|
130
|
+
# `reply.remove(nil, from: :items)` (remove a row whose id resolves to nil)
|
|
131
|
+
# stays distinct from bare `reply.remove` (remove self). Reused for the
|
|
132
|
+
# positional model too, so the Symbol-first misuse can be caught precisely.
|
|
133
|
+
UNSET_ARG = Object.new
|
|
134
|
+
private_constant :UNSET_ARG
|
|
135
|
+
|
|
136
|
+
# Shared append/prepend build: catch the old two-positional form, require the
|
|
137
|
+
# `to:` keyword, reject a Symbol-first model, then delegate to the Response
|
|
138
|
+
# builder (name first, model second — its internal order).
|
|
139
|
+
def collection_build!(builder, verb, model, legacy_model, to, row_kwargs)
|
|
140
|
+
# Old form `reply.append(:name, model)` passes a SECOND positional — the
|
|
141
|
+
# arity would just error, so intercept it with the guided rewrite.
|
|
142
|
+
reject_legacy_form!(verb, model, legacy_model)
|
|
143
|
+
if to.equal?(UNSET_ARG)
|
|
144
|
+
raise ArgumentError,
|
|
145
|
+
"reply.#{verb} needs a target collection — reply.#{verb}(model, to: :name)"
|
|
146
|
+
end
|
|
147
|
+
reject_symbol_first!(verb, model, :to, to)
|
|
148
|
+
Response.public_send(builder, @component, to, resolve_row_arg(model), **row_kwargs)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The pre-#182 call `reply.<verb>(:name, model)` — a Symbol name first, the
|
|
152
|
+
# model second. Detect it by the presence of the second positional and print
|
|
153
|
+
# the exact keyword rewrite.
|
|
154
|
+
def reject_legacy_form!(verb, model, legacy_model)
|
|
155
|
+
return if legacy_model.equal?(UNSET_ARG)
|
|
156
|
+
|
|
157
|
+
keyword = verb == :remove ? :from : :to
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"reply.#{verb}(#{model.inspect}, …) — the collection name moved to a keyword " \
|
|
160
|
+
"(issue #182): reply.#{verb}(model, #{keyword}: #{model.inspect})"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Issue #182 removed the Symbol-first form (`reply.append(:items, model)`).
|
|
164
|
+
# A Symbol in the model position is almost certainly that old call shape, so
|
|
165
|
+
# raise the exact keyword rewrite instead of trying to build a row from a
|
|
166
|
+
# Symbol "record".
|
|
167
|
+
def reject_symbol_first!(verb, model, keyword, name)
|
|
168
|
+
return unless model.is_a?(Symbol)
|
|
169
|
+
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
"reply.#{verb}(#{model.inspect}, …) — the collection name moved to a keyword " \
|
|
172
|
+
"(issue #182): reply.#{verb}(model, #{keyword}: #{name.inspect})"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A row build needs a real model/dom-id — never the omitted sentinel.
|
|
176
|
+
def resolve_row_arg(model)
|
|
177
|
+
raise ArgumentError, "reply collection verbs need a model or dom-id as the first argument" if model.equal?(UNSET_ARG)
|
|
178
|
+
|
|
179
|
+
model
|
|
119
180
|
end
|
|
120
181
|
end
|
|
121
182
|
end
|