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.
@@ -279,6 +279,82 @@ module Phlex
279
279
  @param_types_frozen = false
280
280
  end
281
281
 
282
+ # Register (with a schema Hash) OR read (bare name) a NAMED param schema
283
+ # (issue #184) — a reusable, boot-declared schema so sibling components stop
284
+ # duplicating verbatim constants that drift. Follows the param_type contract:
285
+ # register in an INITIALIZER (the registry freezes after boot).
286
+ #
287
+ # # config/initializers/phlex_reactive.rb
288
+ # Phlex::Reactive.param_schema :todo, title: :string, done: :boolean
289
+ # # then: action :save, params: :todo
290
+ # # compose: action :bulk, params: { **Phlex::Reactive.param_schema(:todo), note: :string }
291
+ #
292
+ # Reading an unknown name raises, listing the registered ones. The stored
293
+ # schema is frozen; the reader returns it (compose with ** for a new Hash).
294
+ def param_schema(name, schema = nil)
295
+ return fetch_param_schema(name) if schema.nil?
296
+
297
+ if param_schemas_frozen?
298
+ raise Error, "Phlex::Reactive.param_schema(#{name.inspect}) called after boot — the param-schema " \
299
+ "registry is frozen once the app is initialized. Register named schemas in an " \
300
+ "initializer (config/initializers/phlex_reactive.rb)."
301
+ end
302
+
303
+ param_schemas[name.to_sym] = deep_freeze_schema(schema.transform_keys(&:to_sym))
304
+ end
305
+
306
+ def param_schemas
307
+ @param_schemas ||= {}
308
+ end
309
+
310
+ def param_schema?(name)
311
+ param_schemas.key?(name.to_sym)
312
+ end
313
+
314
+ def freeze_param_schemas!
315
+ param_schemas.freeze
316
+ @param_schemas_frozen = true
317
+ end
318
+
319
+ def param_schemas_frozen?
320
+ @param_schemas_frozen ||= false
321
+ end
322
+
323
+ # Drop the named-schema registry and unfreeze it (tests only).
324
+ def reset_param_schemas!
325
+ @param_schemas = {}
326
+ @param_schemas_frozen = false
327
+ end
328
+
329
+ private
330
+
331
+ # Deep-freeze a registered schema so a nested Hash/Array (a nested-param
332
+ # schema like { address: { street: :string } }) can't be mutated through the
333
+ # memoized object fetch_param_schema returns — a top-level freeze alone leaves
334
+ # inner structures writable, poisoning the shared registry entry. Symbolizes
335
+ # nested keys too so composed-in nested schemas stay canonical.
336
+ def deep_freeze_schema(value)
337
+ case value
338
+ when Hash then value.to_h { |k, v| [k.to_sym, deep_freeze_schema(v)] }.freeze
339
+ when Array then value.map { deep_freeze_schema(it) }.freeze
340
+ else value
341
+ end
342
+ end
343
+
344
+ # Resolve a registered named schema, raising a guided error (listing the
345
+ # registered names) for an unknown one — the action macro calls this to turn
346
+ # `params: :todo` into the schema Hash.
347
+ def fetch_param_schema(name)
348
+ param_schemas.fetch(name.to_sym) do
349
+ known = param_schemas.keys.sort.join(", ")
350
+ raise ::ArgumentError,
351
+ "Phlex::Reactive.param_schema(#{name.inspect}) is not registered. " \
352
+ "Registered schemas: #{known.empty? ? "(none)" : known}."
353
+ end
354
+ end
355
+
356
+ public
357
+
282
358
  # Emit an `<event>.phlex_reactive` ActiveSupport::Notifications event around
283
359
  # a block, yielding the mutable payload so a rescue can finalize the outcome
284
360
  # (issue #107). ASN.instrument is cheap when nothing is subscribed (the hot
@@ -560,13 +636,29 @@ module Phlex
560
636
  # diagnostic still goes to the log.
561
637
  attr_accessor :error_flash
562
638
 
563
- # Component class used to render STRING flash content (issue #77).
564
- # Instantiated as flash_component.new(level:, content:) and rendered
565
- # through the existing render path, replacing the built-in
566
- # <div class="reactive-flash reactive-flash--{level}"> wrapper. Default
567
- # nil (the built-in wrapper). Phlex component content passed to
639
+ # A CALLABLE that builds a flash component from STRING flash content (issue
640
+ # #182). Given (level, content), it returns a Phlex component the gem renders
641
+ # in place of the built-in <div class="reactive-flash reactive-flash--{level}">
642
+ # wrapper so the APP owns the kwarg mapping (no hardcoded new(level:,
643
+ # content:) contract that collides with a real app's flash component):
644
+ #
645
+ # Phlex::Reactive.flash_component = ->(level, content) { MyFlash.new(level:, message: content) }
646
+ #
647
+ # Default nil (the built-in wrapper). Phlex component content passed to
568
648
  # reply.flash always renders verbatim and bypasses this.
569
- attr_accessor :flash_component
649
+ attr_reader :flash_component
650
+
651
+ # Reject a bare Class (the pre-#182 contract) with the exact lambda rewrite —
652
+ # the gem no longer guesses the component's kwargs.
653
+ def flash_component=(callable)
654
+ if callable.is_a?(Class)
655
+ raise ArgumentError,
656
+ "Phlex::Reactive.flash_component is now a callable (issue #182) — " \
657
+ "flash_component = ->(level, content) { #{callable}.new(level:, content:) }"
658
+ end
659
+
660
+ @flash_component = callable
661
+ end
570
662
 
571
663
  # Render a Phlex component to HTML with a full (off-request) view context.
572
664
  # Uses phlex-rails' #render_in against the memoized view context — a direct
@@ -581,10 +673,36 @@ module Phlex
581
673
  Phlex::Reactive::Defer.with_real_render { component.render_in(off_request_view_context) }
582
674
  end
583
675
 
676
+ # Module-level broadcast (issue #185) — the same broadcast_to as the class
677
+ # form, for a BUILT component payload, so a NON-Streamable target (a count
678
+ # badge, any plain Phlex component) broadcasts WITHOUT hand-rolling the raw
679
+ # channel + render, and IS instrumented:
680
+ #
681
+ # Phlex::Reactive.broadcast_to(@list, :todos, update: TodoCount.new(list: @list), target: "todos-count")
682
+ # Phlex::Reactive.broadcast_to(user, :alerts, replace: NotificationsBadge.new(user:))
683
+ #
684
+ # Container verbs (update:/append:/prepend:) take any component; self-targeting
685
+ # verbs (replace:/remove:) require a Streamable payload (its #id is the target)
686
+ # — a plain component gets a guided error steering to update:. Shares the one
687
+ # broadcast implementation with the class-level form.
688
+ def broadcast_to(*streamables, morph: false, target: nil, exclude: nil, visible_to: nil, each: nil, **verb)
689
+ name, payload = Phlex::Reactive::Streamable.extract_module_broadcast_verb(verb)
690
+ component = name == :js ? nil : payload
691
+ keys = each ? each.map { Array(it) } : [streamables]
692
+ # The owner is the payload's class when Streamable (for js ops + instrument),
693
+ # else Streamable itself (a plain component still instruments via the shared
694
+ # path). A nil payload (a bare state-backed default) isn't supported here —
695
+ # the module form is for BUILT component payloads.
696
+ owner = payload.is_a?(Phlex::Reactive::Streamable) ? payload.class : Phlex::Reactive::Streamable
697
+ Phlex::Reactive::Streamable.broadcast_component(
698
+ owner, name, payload, component, keys, morph:, target:, exclude:, visible_to:
699
+ )
700
+ end
701
+
584
702
  # A Turbo::Streams::TagBuilder bound to an off-request view context, used
585
703
  # to build standalone streams not tied to a specific component's id — a
586
704
  # Response flash append, a reactive_collection row removal, a count
587
- # companion update, an also_update companion. Cached PER THREAD alongside
705
+ # companion update, an also() companion. Cached PER THREAD alongside
588
706
  # the context it's bound to (see off_request_view_context for why
589
707
  # per-thread). Renamed from flash_builder (issue #113): the builder does
590
708
  # far more than flashes, so the name misled. flash_builder stays a
@@ -615,11 +733,18 @@ module Phlex
615
733
  @off_request_view_context_generation = off_request_view_context_generation + 1
616
734
  end
617
735
 
618
- # Permanent aliases for the pre-#113 names. Kept forever (no deprecation)
619
- # so the engine's to_prepare hook and any app code calling the old names
620
- # keep working unchanged.
621
- alias flash_builder stream_builder
622
- alias reset_flash_builder! reset_stream_builder!
736
+ # Issue #182: the pre-#113 aliases are removed (they contradicted the
737
+ # clean-break rule two names for one thing). Each raises a guided error
738
+ # naming the real method. The engine (to_prepare) already uses the new names.
739
+ def flash_builder
740
+ raise NoMethodError,
741
+ "Phlex::Reactive.flash_builder was removed in issue #182 — use stream_builder"
742
+ end
743
+
744
+ def reset_flash_builder!
745
+ raise NoMethodError,
746
+ "Phlex::Reactive.reset_flash_builder! was removed in issue #182 — use reset_stream_builder!"
747
+ end
623
748
 
624
749
  def off_request_view_context_generation
625
750
  @off_request_view_context_generation ||= 0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -123,6 +123,9 @@ files:
123
123
  - app/javascript/phlex/reactive/confirm.js
124
124
  - app/javascript/phlex/reactive/confirm.min.js
125
125
  - app/javascript/phlex/reactive/confirm.min.js.map
126
+ - app/javascript/phlex/reactive/confirm_predicate.js
127
+ - app/javascript/phlex/reactive/confirm_predicate.min.js
128
+ - app/javascript/phlex/reactive/confirm_predicate.min.js.map
126
129
  - app/javascript/phlex/reactive/inspect.js
127
130
  - app/javascript/phlex/reactive/inspect.min.js
128
131
  - app/javascript/phlex/reactive/inspect.min.js.map