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
|
@@ -76,7 +76,50 @@ module Phlex
|
|
|
76
76
|
def reactive_scope(name = nil)
|
|
77
77
|
return Registry.resolve_scalar(self, :scope, :reactive_scope) if name.nil?
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
scope = name.to_sym
|
|
80
|
+
# Issue #184: catch an action already declared with a schema nested under
|
|
81
|
+
# THIS scope key (the action-first declaration order) — the endpoint
|
|
82
|
+
# unwraps ONE scope level, so a schema pre-nested under it would be dead.
|
|
83
|
+
reactive_actions.each_value { assert_no_scope_double_nesting!(scope, it.params, it.name) }
|
|
84
|
+
Registry.write_scalar(self, :scope, scope)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Raise a guided ArgumentError when a param schema is ALREADY nested under
|
|
88
|
+
# the scope key (issue #184): the endpoint peels one scope level, so
|
|
89
|
+
# params: { <scope>: { … } } would double-unwrap to nothing. Shared by both
|
|
90
|
+
# macros so neither declaration order can dodge the guard. A nested key that
|
|
91
|
+
# is NOT the scope (a real nested_attributes param) is fine.
|
|
92
|
+
def assert_no_scope_double_nesting!(scope, params, action_name)
|
|
93
|
+
return unless params.is_a?(Hash) && params[scope].is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
raise ArgumentError,
|
|
96
|
+
"#{self}: action #{action_name.inspect} nests its schema under the reactive_scope " \
|
|
97
|
+
"key #{scope.inspect} (params: { #{scope}: { … } }). The endpoint unwraps one scope " \
|
|
98
|
+
"level already — declare the schema FLAT: params: { #{params[scope].keys.first || :field}: … }."
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Issue #184: ONE dirty-tracking declaration, class-level. Replaces
|
|
102
|
+
# reactive_root(track_dirty:, warn_unsaved:) + reactive_field(dirty:).
|
|
103
|
+
#
|
|
104
|
+
# reactive_dirty # track every field via the root
|
|
105
|
+
# reactive_dirty warn_unsaved: true # + warn before navigating away
|
|
106
|
+
# reactive_dirty only: %i[title] # track only these fields (per-field)
|
|
107
|
+
#
|
|
108
|
+
# Stored as a frozen config the render helpers read; the emitted DOM is
|
|
109
|
+
# byte-identical to the old kwargs, so the client is unchanged. `only:`
|
|
110
|
+
# switches to per-field descriptors (no root-level input delegation);
|
|
111
|
+
# otherwise the root delegates for the whole subtree.
|
|
112
|
+
def reactive_dirty(warn_unsaved: false, only: nil)
|
|
113
|
+
Registry.write_scalar(self, :dirty, {
|
|
114
|
+
warn_unsaved: warn_unsaved ? true : false,
|
|
115
|
+
only: only && Array(only).map(&:to_sym).freeze
|
|
116
|
+
}.freeze)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The resolved dirty config (or nil when reactive_dirty was never
|
|
120
|
+
# declared), inherited through the Registry like every other scalar.
|
|
121
|
+
def reactive_dirty_config
|
|
122
|
+
Registry.resolve_scalar(self, :dirty, :reactive_dirty_config)
|
|
80
123
|
end
|
|
81
124
|
|
|
82
125
|
# Opt into signed STATE for record-less components only.
|
|
@@ -149,6 +192,13 @@ module Phlex
|
|
|
149
192
|
# ({ "0" => ..., "1" => ... }), so a fields_for collection works either way.
|
|
150
193
|
def action(name, params: {})
|
|
151
194
|
require_server_actions!(:action)
|
|
195
|
+
# Issue #184: params: :symbol resolves a registered named schema.
|
|
196
|
+
params = Phlex::Reactive.param_schema(params) if params.is_a?(Symbol)
|
|
197
|
+
# If a scope is already declared, reject a schema nested under the scope
|
|
198
|
+
# key here (the scope-first declaration order). The action-first order is
|
|
199
|
+
# caught in reactive_scope.
|
|
200
|
+
scope = reactive_scope
|
|
201
|
+
assert_no_scope_double_nesting!(scope, params, name.to_sym) if scope
|
|
152
202
|
Registry.write_entry(
|
|
153
203
|
self, :actions, name.to_sym,
|
|
154
204
|
Action.new(name: name.to_sym, params: params, schema: Phlex::Reactive::ParamSchema.compile(params))
|
|
@@ -164,12 +214,17 @@ module Phlex
|
|
|
164
214
|
Registry.resolve_hash(self, :actions, :reactive_actions)
|
|
165
215
|
end
|
|
166
216
|
|
|
217
|
+
# Issue #186: the fetch-one readers are removed — the plural
|
|
218
|
+
# reactive_actions hash IS the fetch-one. Guided errors name the rewrite.
|
|
167
219
|
def reactive_action(name)
|
|
168
|
-
|
|
220
|
+
raise NoMethodError,
|
|
221
|
+
"reactive_action(#{name.inspect}) was removed in issue #186 — use reactive_actions[#{name.inspect}]"
|
|
169
222
|
end
|
|
170
223
|
|
|
171
224
|
def reactive_action?(name)
|
|
172
|
-
|
|
225
|
+
raise NoMethodError,
|
|
226
|
+
"reactive_action?(#{name.inspect}) was removed in issue #186 — " \
|
|
227
|
+
"use reactive_actions.key?(#{name.inspect})"
|
|
173
228
|
end
|
|
174
229
|
|
|
175
230
|
# Opt out of the default-ON verify_authorized guard (issue #168).
|
|
@@ -218,9 +273,10 @@ module Phlex
|
|
|
218
273
|
# empty: ItemsEmptyComponent, # optional empty-state component
|
|
219
274
|
# size: -> { @record.items.size } # resolves the live size
|
|
220
275
|
#
|
|
221
|
-
# An action then governs the reply with one call
|
|
222
|
-
#
|
|
223
|
-
# reply.
|
|
276
|
+
# An action then governs the reply with one call (the collection is
|
|
277
|
+
# named by the to:/from: keyword, issue #182):
|
|
278
|
+
# reply.append(item, to: :items) # row append + count + clear empty-state
|
|
279
|
+
# reply.remove(id, from: :items) # row remove + count + restore empty-state
|
|
224
280
|
#
|
|
225
281
|
# count/empty/size are optional: a list with just rows omits them and the
|
|
226
282
|
# corresponding stream isn't emitted. See Phlex::Reactive::Reply and the
|
|
@@ -236,12 +292,17 @@ module Phlex
|
|
|
236
292
|
Registry.resolve_hash(self, :collections, :reactive_collections)
|
|
237
293
|
end
|
|
238
294
|
|
|
295
|
+
# Issue #186: use the plural reactive_collections hash directly.
|
|
239
296
|
def reactive_collection_def(name)
|
|
240
|
-
|
|
297
|
+
raise NoMethodError,
|
|
298
|
+
"reactive_collection_def(#{name.inspect}) was removed in issue #186 — " \
|
|
299
|
+
"use reactive_collections[#{name.inspect}]"
|
|
241
300
|
end
|
|
242
301
|
|
|
243
302
|
def reactive_collection?(name)
|
|
244
|
-
|
|
303
|
+
raise NoMethodError,
|
|
304
|
+
"reactive_collection?(#{name.inspect}) was removed in issue #186 — " \
|
|
305
|
+
"use reactive_collections.key?(#{name.inspect})"
|
|
245
306
|
end
|
|
246
307
|
|
|
247
308
|
# Declare a client-side computation, OR (called with just a name) read
|
|
@@ -299,17 +360,13 @@ module Phlex
|
|
|
299
360
|
# the client interpreter re-checks the same shape).
|
|
300
361
|
def reactive_compute(name, inputs: nil, outputs: nil, reducer: nil, mirror: nil)
|
|
301
362
|
if inputs.nil? && outputs.nil?
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"alongside inputs:/outputs:."
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
return reactive_compute_def(name)
|
|
363
|
+
# Issue #186: the bare getter form is removed — reactive_compute is
|
|
364
|
+
# the SETTER now; read one compute via the plural hash. (Raising here
|
|
365
|
+
# keeps the getter/setter overload from silently misfiring.)
|
|
366
|
+
raise ArgumentError,
|
|
367
|
+
"reactive_compute(#{name.inspect}) getter was removed in issue #186 — " \
|
|
368
|
+
"use reactive_computes[#{name.inspect}]. reactive_compute is the declarative " \
|
|
369
|
+
"SETTER now: reactive_compute(:name, inputs:, outputs:)."
|
|
313
370
|
end
|
|
314
371
|
|
|
315
372
|
input_names, input_types = normalize_compute_inputs(inputs)
|
|
@@ -331,28 +388,59 @@ module Phlex
|
|
|
331
388
|
# reactive_collection_def (issue #115). The bare reactive_compute(name)
|
|
332
389
|
# getter above is a PERMANENT documented alias (it shipped in the same
|
|
333
390
|
# minor series, #73); both stay, no deprecation.
|
|
391
|
+
# Issue #186: use the plural reactive_computes hash directly.
|
|
334
392
|
def reactive_compute_def(name)
|
|
335
|
-
|
|
393
|
+
raise NoMethodError,
|
|
394
|
+
"reactive_compute_def(#{name.inspect}) was removed in issue #186 — " \
|
|
395
|
+
"use reactive_computes[#{name.inspect}]"
|
|
336
396
|
end
|
|
337
397
|
|
|
338
398
|
def reactive_compute?(name)
|
|
339
|
-
|
|
399
|
+
raise NoMethodError,
|
|
400
|
+
"reactive_compute?(#{name.inspect}) was removed in issue #186 — " \
|
|
401
|
+
"use reactive_computes.key?(#{name.inspect})"
|
|
340
402
|
end
|
|
341
403
|
|
|
342
|
-
# Split the `inputs:` argument into [ordered names, types-or-nil]
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
404
|
+
# Split the `inputs:` argument into [ordered names, types-or-nil].
|
|
405
|
+
# THREE shapes, all degenerate cases of the issue-#183 PERMIT form
|
|
406
|
+
# (bare symbols default to :number; a trailing Hash types the exceptions):
|
|
407
|
+
#
|
|
408
|
+
# * a pure Hash ({ title: :string, qty: :number }) — the issue-#104 typed
|
|
409
|
+
# form. Ordered keys + a name→type map.
|
|
410
|
+
# * a permit Array with a trailing Hash ([:qty, { title: :string }]) —
|
|
411
|
+
# bare symbols typed :number, the Hash keys typed as declared. This is
|
|
412
|
+
# the one form the issue-#183 docs show.
|
|
413
|
+
# * a bare Array/symbol ([:price, :qty]) — the issue-#73 UNTYPED form.
|
|
414
|
+
# nil types, so the array-form wire stays byte-identical and the client
|
|
415
|
+
# keeps its numeric coercion.
|
|
416
|
+
#
|
|
417
|
+
# Named after the shape it normalizes.
|
|
348
418
|
def normalize_compute_inputs(inputs)
|
|
349
|
-
if inputs.is_a?(Hash)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
419
|
+
return normalize_typed_compute_inputs(inputs) if inputs.is_a?(Hash)
|
|
420
|
+
|
|
421
|
+
# dup before popping: Array(inputs) returns the SAME object for an array
|
|
422
|
+
# arg, so an unguarded pop would mutate (or FrozenError on) a shared or
|
|
423
|
+
# frozen inputs array the caller still holds.
|
|
424
|
+
list = Array(inputs).dup
|
|
425
|
+
trailing = list.last.is_a?(Hash) ? list.pop : nil
|
|
426
|
+
names = list.map(&:to_sym)
|
|
427
|
+
|
|
428
|
+
# No trailing type Hash → the untyped array form (nil types).
|
|
429
|
+
return [names, nil] if trailing.nil?
|
|
430
|
+
|
|
431
|
+
# Permit form: bare names default to :number; the trailing Hash types
|
|
432
|
+
# the exceptions. Declaration order is bare names, then the Hash keys.
|
|
433
|
+
typed = trailing.transform_keys(&:to_sym).transform_values(&:to_sym)
|
|
434
|
+
ordered = names + typed.keys
|
|
435
|
+
types = names.to_h { [it, :number] }.merge(typed)
|
|
436
|
+
[ordered, types]
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# A pure Hash of name => type (issue #104): ordered keys + the type map.
|
|
440
|
+
def normalize_typed_compute_inputs(inputs)
|
|
441
|
+
names = inputs.keys.map(&:to_sym)
|
|
442
|
+
types = inputs.transform_keys(&:to_sym).transform_values(&:to_sym)
|
|
443
|
+
[names, types]
|
|
356
444
|
end
|
|
357
445
|
|
|
358
446
|
# Normalize `mirror:` to { name => [id selectors] } (nil passes
|