phlex-forms 0.4.1 → 0.5.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 +67 -0
- data/README.md +27 -0
- data/lib/forms/base.rb +8 -2
- data/lib/forms/live.rb +17 -5
- data/lib/forms/rich_textarea.rb +5 -4
- data/lib/phlex_forms/configuration.rb +59 -11
- data/lib/phlex_forms/engine.rb +4 -0
- data/lib/phlex_forms/shareable.rb +33 -0
- data/lib/phlex_forms/theme.rb +39 -32
- data/lib/phlex_forms/version.rb +1 -1
- data/lib/phlex_forms.rb +35 -5
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b43b29cfcccda75c6a0af1c888d3edd50b824f3c393c9938890355143d787231
|
|
4
|
+
data.tar.gz: 8b231701048287d17a7e44b004f6389f5d760e2b5e36750c90f2416dcbfdca89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04f12370599fbae631ac6e88345a7e412eb8f0a143eaa20e76978d8e584aeab6d9657b182e8ac7a424b9624b9df7c362a4008a922c586d4669652fa3da67fa60
|
|
7
|
+
data.tar.gz: 802635cc20851fbade99760b38c05d6e3e8fb1a67433fedbcb59ff5318e2ef21fb66d202c7e6f0534c530005dfb468c9e4dc67a01fce5b69b3e5a6c8929db18d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0]
|
|
11
|
+
|
|
12
|
+
### Upgrading to 0.5
|
|
13
|
+
|
|
14
|
+
The configuration is frozen once the app has booted, so threads and fibers
|
|
15
|
+
read one immutable value, and so can Ractors when every setting can be shared
|
|
16
|
+
(#42, conformance-kit; see the unshareable-value note below).
|
|
17
|
+
|
|
18
|
+
- **It finalizes on its own.** Under Rails the engine calls
|
|
19
|
+
`PhlexForms.finalize!` in `after_initialize`, after every initializer has
|
|
20
|
+
run, with no app code. Without Rails, `PhlexForms.configure` finalizes when
|
|
21
|
+
its block returns; a host that never calls `configure` (or loads Rails
|
|
22
|
+
without booting an app) calls `PhlexForms.finalize!` at boot.
|
|
23
|
+
- **A direct write after boot raises `FrozenError`.** `PhlexForms.config.infer_from_model = false`
|
|
24
|
+
in a Rails initializer still works. After boot (a test's `before` block, or
|
|
25
|
+
after `configure` outside Rails), write through `PhlexForms.configure`
|
|
26
|
+
instead: it edits a copy and swaps it in.
|
|
27
|
+
- **Finalizing never raises over a value it cannot share.** The value is kept
|
|
28
|
+
as given and works on the main Ractor only: an icon renderer lambda at the
|
|
29
|
+
top level of an initializer (its `self` is `main`, and it may close over a
|
|
30
|
+
logger) is one. Define it in a class or module body (or with
|
|
31
|
+
`Ractor.shareable_lambda` on Ruby 4.0) to keep the whole configuration
|
|
32
|
+
shareable. A renderer that is not a Proc (an object with `#call`, a
|
|
33
|
+
`Method`) is never copied or frozen, so one that keeps a cache still works.
|
|
34
|
+
- **`form_options` and the `live` macros keep frozen copies** of what they are
|
|
35
|
+
given, so the values nested in `form_defaults` and the live settings are
|
|
36
|
+
frozen. Your own arrays and hashes are copied, never frozen.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`PhlexForms.finalize!`**: freezes the configuration and stores
|
|
41
|
+
Ractor-shareable copies of its settings. Idempotent; returns the
|
|
42
|
+
configuration. `PhlexForms::Theme::DAISY` / `PLAIN` are the built-in themes,
|
|
43
|
+
built once when the theme loads (`Theme.daisy` / `Theme.plain` return them),
|
|
44
|
+
and every `Theme` is frozen.
|
|
45
|
+
- **Ractor support for the non-rendering paths.** On Ruby 4.0, field
|
|
46
|
+
inference, option resolution, theme resolution, the icon renderer and the
|
|
47
|
+
`form_options` / `live` settings work inside a non-main Ractor. Rendering
|
|
48
|
+
there waits on Phlex
|
|
49
|
+
([yippee-fun/phlex#1055](https://github.com/yippee-fun/phlex/pull/1055)).
|
|
50
|
+
- **conformance-kit 0.4 gates in CI**: the thread-safety RuboCop cops, thread
|
|
51
|
+
and fiber contention specs, a Ractor canary and a strict
|
|
52
|
+
`conformance-audit` of `PhlexForms` and `Forms` with nothing allowlisted.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **The configuration is frozen after boot** (see Upgrading to 0.5).
|
|
57
|
+
`Configuration` sets every default in `initialize`; no reader memoizes. The
|
|
58
|
+
default icon renderer and `Configuration.glyphs_renderer` are shareable.
|
|
59
|
+
- `rich_textarea` renders `<lexxy-editor>` through Phlex's `tag` instead of
|
|
60
|
+
`register_element`; the markup is the same.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **The themes build without phlex-reactive.** `Theme.daisy` named
|
|
65
|
+
`Forms::TagField` before checking for the soft dependency, so a host without
|
|
66
|
+
phlex-reactive got `NameError: uninitialized constant Forms::TagField` on its
|
|
67
|
+
first form.
|
|
68
|
+
- **`Theme.daisy` / `Theme.plain` are built once.** Two threads resolving a
|
|
69
|
+
theme for the first time could each build their own.
|
|
70
|
+
- **phlex-forms loads without Rails with phlex-reactive bundled.** phlex-reactive's
|
|
71
|
+
`Sortable` calls `Hash#to_json` while it loads without requiring json, so a
|
|
72
|
+
plain Ruby host got `NoMethodError` on its first form (and, with finalizing,
|
|
73
|
+
from `configure`). phlex-forms requires json before phlex-reactive.
|
|
74
|
+
|
|
75
|
+
## [0.4.1] and earlier
|
|
76
|
+
|
|
10
77
|
### Upgrading to 0.4
|
|
11
78
|
|
|
12
79
|
phlex-forms now depends on **daisyui `~> 2.0`** and merges every default +
|
data/README.md
CHANGED
|
@@ -437,6 +437,33 @@ PhlexForms.configure do |c|
|
|
|
437
437
|
end
|
|
438
438
|
```
|
|
439
439
|
|
|
440
|
+
## Configuration is finalized at boot
|
|
441
|
+
|
|
442
|
+
`PhlexForms.configure` writes the configuration; finalizing freezes it and
|
|
443
|
+
stores Ractor-shareable copies of its settings, so threads and fibers read one
|
|
444
|
+
immutable value, and so can Ractors when every setting can be shared (see
|
|
445
|
+
below). It happens on its own:
|
|
446
|
+
|
|
447
|
+
- **Under Rails** the engine finalizes in `after_initialize`, once every
|
|
448
|
+
initializer has run, so initializers can still write (through `configure`
|
|
449
|
+
or directly).
|
|
450
|
+
- **Without Rails** (no `Rails::Engine` when phlex-forms loads), `configure`
|
|
451
|
+
finalizes when its block returns. A host that never calls `configure`, or
|
|
452
|
+
loads Rails without booting an app, calls `PhlexForms.finalize!` at boot.
|
|
453
|
+
|
|
454
|
+
After that, write through `configure` again (it edits a copy and swaps it in);
|
|
455
|
+
a direct `PhlexForms.config.theme = :plain` raises `FrozenError`. Your own
|
|
456
|
+
arrays and hashes are copied, never frozen.
|
|
457
|
+
|
|
458
|
+
Finalizing never raises over a value it cannot share: that value is kept as
|
|
459
|
+
given and works on the main Ractor only. An icon renderer lambda at the top
|
|
460
|
+
level of an initializer (its `self` is `main`, and it may close over a logger)
|
|
461
|
+
is one; define it in a class or module body (or with `Ractor.shareable_lambda`
|
|
462
|
+
on Ruby 4.0) to keep the whole configuration shareable. A renderer that is not
|
|
463
|
+
a Proc (an object with `#call`, a `Method`) is never copied or frozen, so one
|
|
464
|
+
that keeps a cache still works. `PhlexForms.reset_configuration!` restores the
|
|
465
|
+
open defaults (for test suites).
|
|
466
|
+
|
|
440
467
|
## RuboCop cops
|
|
441
468
|
|
|
442
469
|
Two cops nudge call sites toward the phlex-forms API. Enable them in your
|
data/lib/forms/base.rb
CHANGED
|
@@ -26,9 +26,14 @@ module Forms
|
|
|
26
26
|
# (instance args beat class defaults; subclass defaults beat parents'):
|
|
27
27
|
#
|
|
28
28
|
# form_options :spaced, url: "/signup", validate: true
|
|
29
|
+
#
|
|
30
|
+
# A class macro: it runs while the form class body loads (at boot, on the
|
|
31
|
+
# main Ractor) and stores Ractor-shareable copies of its arguments
|
|
32
|
+
# (PhlexForms::Shareable.copy), so the readers work in any Ractor.
|
|
33
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
29
34
|
def form_options(*modifiers, **defaults)
|
|
30
|
-
@form_modifiers = modifiers
|
|
31
|
-
@form_defaults = defaults
|
|
35
|
+
@form_modifiers = PhlexForms::Shareable.copy(modifiers)
|
|
36
|
+
@form_defaults = PhlexForms::Shareable.copy(defaults)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
def form_modifiers
|
|
@@ -40,6 +45,7 @@ module Forms
|
|
|
40
45
|
inherited = superclass.respond_to?(:form_defaults) ? superclass.form_defaults : {}
|
|
41
46
|
inherited.merge(@form_defaults || {})
|
|
42
47
|
end
|
|
48
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
43
49
|
|
|
44
50
|
# Server-truth live validation via phlex-reactive (a soft dependency):
|
|
45
51
|
#
|
data/lib/forms/live.rb
CHANGED
|
@@ -42,10 +42,15 @@ module Forms
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
class_methods do
|
|
45
|
+
# live (setup_live), live_permit, live_deny and live_tags are class macros:
|
|
46
|
+
# they run while the form class body loads (at boot, on the main Ractor)
|
|
47
|
+
# and store Ractor-shareable values (PhlexForms::Shareable.copy), so the
|
|
48
|
+
# readers work in any Ractor.
|
|
49
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
45
50
|
def setup_live(model_class:, scope:, debounce:)
|
|
46
51
|
@live_model_class = model_class
|
|
47
|
-
@live_scope = scope.to_s
|
|
48
|
-
@live_debounce = debounce
|
|
52
|
+
@live_scope = PhlexForms::Shareable.copy(scope.to_s)
|
|
53
|
+
@live_debounce = PhlexForms::Shareable.copy(debounce)
|
|
49
54
|
reactive_state :model_gid, :touched
|
|
50
55
|
action :validate, params: { scope.to_sym => :form_attributes, _touch: :string }
|
|
51
56
|
# phlex-reactive >= 0.11 defaults verify_authorized ON: an action that
|
|
@@ -64,8 +69,8 @@ module Forms
|
|
|
64
69
|
# Narrow (live_permit) or trim (live_deny) the attributes the :validate
|
|
65
70
|
# action may assign. Default: the model's column/attribute names plus
|
|
66
71
|
# every validated attribute (and its _confirmation twin).
|
|
67
|
-
def live_permit(*attrs) = @live_permit = attrs.map(&:to_s)
|
|
68
|
-
def live_deny(*attrs) = @live_deny = attrs.map(&:to_s)
|
|
72
|
+
def live_permit(*attrs) = @live_permit = PhlexForms::Shareable.copy(attrs.map(&:to_s))
|
|
73
|
+
def live_deny(*attrs) = @live_deny = PhlexForms::Shareable.copy(attrs.map(&:to_s))
|
|
69
74
|
|
|
70
75
|
# Lift a tag field onto the form's reactive root so live :validate sees its
|
|
71
76
|
# comma-joined value. The widget renders ROOTLESS (no nested reactive root),
|
|
@@ -90,7 +95,7 @@ module Forms
|
|
|
90
95
|
"non-live)."
|
|
91
96
|
end
|
|
92
97
|
|
|
93
|
-
@live_tags = { name: name.to_sym, suggestions: }
|
|
98
|
+
@live_tags = PhlexForms::Shareable.copy({ name: name.to_sym, suggestions: })
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
def live_tags_declaration = @live_tags || inherited_live(:live_tags_declaration)
|
|
@@ -101,6 +106,7 @@ module Forms
|
|
|
101
106
|
permitted |= [tag[:name].to_s] if tag
|
|
102
107
|
permitted - (@live_deny || [])
|
|
103
108
|
end
|
|
109
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
104
110
|
|
|
105
111
|
private
|
|
106
112
|
|
|
@@ -219,5 +225,11 @@ module Forms
|
|
|
219
225
|
@model.public_send(setter, value) if @model.respond_to?(setter)
|
|
220
226
|
end
|
|
221
227
|
end
|
|
228
|
+
|
|
229
|
+
# ActiveSupport::Concern's own list of the concerns this one includes
|
|
230
|
+
# (Phlex::Reactive::Component): filled while this module body loads and
|
|
231
|
+
# only read after, when a form class includes Forms::Live. Frozen, so the
|
|
232
|
+
# module holds no mutable state.
|
|
233
|
+
@_dependencies.freeze
|
|
222
234
|
end
|
|
223
235
|
end
|
data/lib/forms/rich_textarea.rb
CHANGED
|
@@ -6,8 +6,6 @@ module Forms
|
|
|
6
6
|
# Renders an editor bound to the field; when the value is an ActionText::RichText
|
|
7
7
|
# its HTML body is used as the initial content.
|
|
8
8
|
class RichTextarea < Phlex::HTML
|
|
9
|
-
register_element :lexxy_editor
|
|
10
|
-
|
|
11
9
|
def initialize(*modifiers, name:, id: nil, value: nil, **options)
|
|
12
10
|
@modifiers = modifiers
|
|
13
11
|
@name = name
|
|
@@ -20,9 +18,12 @@ module Forms
|
|
|
20
18
|
|
|
21
19
|
# Lexxy configures the editor through CHILD elements — `<lexxy-prompt>` for
|
|
22
20
|
# @mentions, `<lexxy-code-language-picker>` for code blocks — so the block
|
|
23
|
-
# has to reach the element, not be dropped.
|
|
21
|
+
# has to reach the element, not be dropped. Rendered through `tag`, not a
|
|
22
|
+
# `register_element` method, so the class keeps no element registry of its
|
|
23
|
+
# own.
|
|
24
24
|
def view_template(&)
|
|
25
|
-
|
|
25
|
+
tag(
|
|
26
|
+
:lexxy_editor,
|
|
26
27
|
name: @name,
|
|
27
28
|
id: @id,
|
|
28
29
|
value: formatted_value,
|
|
@@ -22,7 +22,23 @@ module PhlexForms
|
|
|
22
22
|
# glyphs is intentionally NOT the default: rails_icons reads SVGs from the host
|
|
23
23
|
# app's asset paths, which a minimal host (or the gem's own test suite) has not
|
|
24
24
|
# set up — so defaulting to glyphs would raise Icons::IconNotFound.
|
|
25
|
+
#
|
|
26
|
+
# Finalized at boot: `configure` finalizes when the Rails engine is absent, the
|
|
27
|
+
# engine's after_initialize does under Rails (or call `PhlexForms.finalize!`).
|
|
28
|
+
# Finalizing freezes the configuration and stores Ractor-shareable copies of
|
|
29
|
+
# its settings, so any Ractor can read it. A renderer that cannot be shared (a
|
|
30
|
+
# lambda written at the top level of an initializer has `main` as its self) is
|
|
31
|
+
# kept as given and still works on the main Ractor; build it in a class or
|
|
32
|
+
# module body, or with `Ractor.shareable_lambda` on Ruby 4.0, to keep the
|
|
33
|
+
# whole configuration shareable. A renderer that is not a Proc (an object with
|
|
34
|
+
# #call, a Method) is never copied or frozen: it is shared only if it already
|
|
35
|
+
# is. After finalizing, write through `configure`, which edits a copy and
|
|
36
|
+
# swaps it in.
|
|
25
37
|
class Configuration
|
|
38
|
+
# The bundled inline SVGs. Built in the class body (self is the class and the
|
|
39
|
+
# lambda captures nothing), so it can be made shareable once, here.
|
|
40
|
+
DEFAULT_ICON_RENDERER = Shareable.copy(->(name, **opts) { InlineIcons.render(name, **opts) })
|
|
41
|
+
|
|
26
42
|
# A ready-made renderer that delegates to glyphs' LucideIcon. Raises a clear
|
|
27
43
|
# error if glyphs is not loaded.
|
|
28
44
|
def self.glyphs_renderer
|
|
@@ -31,7 +47,7 @@ module PhlexForms
|
|
|
31
47
|
"PhlexForms glyphs_renderer requires the `glyphs` gem to be loaded"
|
|
32
48
|
end
|
|
33
49
|
|
|
34
|
-
->(name, **opts) { Glyphs::LucideIcon.new(name, **opts) }
|
|
50
|
+
Shareable.copy(->(name, **opts) { Glyphs::LucideIcon.new(name, **opts) })
|
|
35
51
|
end
|
|
36
52
|
|
|
37
53
|
attr_writer :icon_renderer
|
|
@@ -43,29 +59,48 @@ module PhlexForms
|
|
|
43
59
|
|
|
44
60
|
# Global daisyui variants prepended to every `field`'s inner input, e.g.
|
|
45
61
|
# `c.field_variants = [:primary]`. Form-level `field_variants:` and
|
|
46
|
-
# call-site modifiers stack after (later wins).
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@field_variants ||= []
|
|
62
|
+
# call-site modifiers stack after (later wins). nil means none, stored as an
|
|
63
|
+
# empty list so `c.field_variants << :sm` keeps working.
|
|
64
|
+
def field_variants=(variants)
|
|
65
|
+
@field_variants = variants || []
|
|
51
66
|
end
|
|
52
67
|
|
|
53
68
|
# The default theme: :daisy, :plain, or a PhlexForms::Theme instance.
|
|
54
69
|
# Defaults to daisy.
|
|
55
70
|
attr_writer :theme
|
|
56
71
|
|
|
72
|
+
# Every default is set here, never memoized by a reader, so concurrent
|
|
73
|
+
# readers never race to build one. The theme stays nil until set or
|
|
74
|
+
# finalized (the reader falls back to Theme.daisy), so building a
|
|
75
|
+
# configuration loads no component.
|
|
76
|
+
def initialize
|
|
77
|
+
@theme = nil
|
|
78
|
+
@infer_from_model = true
|
|
79
|
+
@field_variants = []
|
|
80
|
+
@icon_renderer = DEFAULT_ICON_RENDERER
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# `configure` on a finalized configuration edits a copy: give the copy its
|
|
84
|
+
# own variants list, so `c.field_variants << :sm` works on it.
|
|
85
|
+
def initialize_copy(source)
|
|
86
|
+
super
|
|
87
|
+
@field_variants = @field_variants.dup
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def field_variants
|
|
91
|
+
@field_variants || []
|
|
92
|
+
end
|
|
93
|
+
|
|
57
94
|
def theme
|
|
58
|
-
@theme
|
|
95
|
+
@theme || Theme.daisy
|
|
59
96
|
end
|
|
60
97
|
|
|
61
98
|
def infer_from_model
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
true
|
|
99
|
+
@infer_from_model.nil? || @infer_from_model
|
|
65
100
|
end
|
|
66
101
|
|
|
67
102
|
def icon_renderer
|
|
68
|
-
@icon_renderer
|
|
103
|
+
@icon_renderer || DEFAULT_ICON_RENDERER
|
|
69
104
|
end
|
|
70
105
|
|
|
71
106
|
# Returns a renderable for the named icon. Components pass the result to Phlex
|
|
@@ -73,5 +108,18 @@ module PhlexForms
|
|
|
73
108
|
def render_icon(name, **)
|
|
74
109
|
icon_renderer.call(name.to_s, **)
|
|
75
110
|
end
|
|
111
|
+
|
|
112
|
+
# Stores Ractor-shareable copies of every setting and freezes the
|
|
113
|
+
# configuration. Resolves the default theme, which builds both built-in
|
|
114
|
+
# themes at boot rather than on the first render. Idempotent; returns self.
|
|
115
|
+
def finalize!
|
|
116
|
+
return self if frozen?
|
|
117
|
+
|
|
118
|
+
@theme = Shareable.copy(theme)
|
|
119
|
+
@infer_from_model = Shareable.copy(@infer_from_model)
|
|
120
|
+
@field_variants = Shareable.copy(@field_variants)
|
|
121
|
+
@icon_renderer = Shareable.callable(@icon_renderer)
|
|
122
|
+
freeze
|
|
123
|
+
end
|
|
76
124
|
end
|
|
77
125
|
end
|
data/lib/phlex_forms/engine.rb
CHANGED
|
@@ -32,6 +32,10 @@ module PhlexForms
|
|
|
32
32
|
app.config.i18n.load_path.unshift(*locales)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# PhlexForms.configure leaves the configuration open under Rails; freeze it
|
|
36
|
+
# (with Ractor-shareable settings) once every initializer has run.
|
|
37
|
+
config.after_initialize { PhlexForms.finalize! }
|
|
38
|
+
|
|
35
39
|
# The raw-hash param type Forms::Live's :validate action schema declares.
|
|
36
40
|
# Registered from an initializer because phlex-reactive freezes its
|
|
37
41
|
# param-type registry after boot.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PhlexForms
|
|
4
|
+
# Ractor-shareable copies of boot-time settings (the configuration, the
|
|
5
|
+
# form_options / live class macros), so any Ractor can read them after boot.
|
|
6
|
+
module Shareable
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# A deep-frozen, Ractor-shareable copy of value; value itself is left as the
|
|
10
|
+
# caller has it. A Proc cannot be copied, so it is shared in place, which
|
|
11
|
+
# works only when its self and everything it captures are already shareable
|
|
12
|
+
# (it never freezes them). A value that cannot be shared (a Proc whose self
|
|
13
|
+
# is not shareable, an IO, ...) comes back unchanged: it still works on the
|
|
14
|
+
# main Ractor. Engines without Ractor get the value back as is.
|
|
15
|
+
def copy(value)
|
|
16
|
+
return value unless defined?(::Ractor)
|
|
17
|
+
return value if ::Ractor.shareable?(value)
|
|
18
|
+
return ::Ractor.make_shareable(value) if value.is_a?(::Proc)
|
|
19
|
+
|
|
20
|
+
::Ractor.make_shareable(value, copy: true)
|
|
21
|
+
rescue ::Ractor::Error, ::TypeError
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A callable (the icon renderer) shared only where that leaves it working: a
|
|
26
|
+
# Proc as in copy, any other object kept as given. A frozen copy of an
|
|
27
|
+
# object would break one that keeps state of its own (a cache, a buffer)
|
|
28
|
+
# on its first call, and a Method cannot be copied.
|
|
29
|
+
def callable(value)
|
|
30
|
+
value.is_a?(::Proc) ? copy(value) : value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/phlex_forms/theme.rb
CHANGED
|
@@ -19,8 +19,10 @@ module PhlexForms
|
|
|
19
19
|
#
|
|
20
20
|
# PhlexForms::Theme.resolve(:plain).with(input: MyInput)
|
|
21
21
|
class Theme
|
|
22
|
+
# Frozen, so a theme of component classes is Ractor-shareable.
|
|
22
23
|
def initialize(components)
|
|
23
24
|
@components = components.freeze
|
|
25
|
+
freeze
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def [](role)
|
|
@@ -45,48 +47,53 @@ module PhlexForms
|
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
48
|
-
def daisy
|
|
49
|
-
@daisy ||= new({
|
|
50
|
-
input: Forms::Input, select: Forms::Select, choices_select: Forms::ChoicesSelect,
|
|
51
|
-
textarea: Forms::Textarea, rich_textarea: Forms::RichTextarea,
|
|
52
|
-
checkbox: Forms::Checkbox, toggle: Forms::Toggle, radio: Forms::Radio,
|
|
53
|
-
checkbox_group: Forms::CheckboxGroup,
|
|
54
|
-
file: Forms::FileInput, wrapped_input: Forms::WrappedInput, hidden: Forms::Hidden,
|
|
55
|
-
control: Forms::FormControl, label: Forms::Label,
|
|
56
|
-
field_error: Forms::FieldError, field_hint: Forms::FieldHint,
|
|
57
|
-
submit: Forms::Submit, row: Forms::Row, group: Forms::Group,
|
|
58
|
-
# tag_field is phlex-reactive-gated (ClientBindings); only registered
|
|
59
|
-
# when the soft dep is present, else :tag_field raises a clear KeyError.
|
|
60
|
-
**reactive_roles(Forms::TagField, Forms::RootlessTagField)
|
|
61
|
-
})
|
|
62
|
-
end
|
|
50
|
+
def daisy = DAISY
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
# rich_textarea to a plain textarea; toggle renders as a checkbox.
|
|
66
|
-
def plain
|
|
67
|
-
@plain ||= new({
|
|
68
|
-
input: Forms::Plain::Input, select: Forms::Plain::Select, choices_select: Forms::Plain::Select,
|
|
69
|
-
textarea: Forms::Plain::Textarea, rich_textarea: Forms::Plain::Textarea,
|
|
70
|
-
checkbox: Forms::Plain::Checkbox, toggle: Forms::Plain::Checkbox, radio: Forms::Plain::Radio,
|
|
71
|
-
checkbox_group: Forms::Plain::CheckboxGroup,
|
|
72
|
-
file: Forms::Plain::FileInput, wrapped_input: Forms::Plain::WrappedInput, hidden: Forms::Hidden,
|
|
73
|
-
control: Forms::Plain::Control, label: Forms::Plain::Label,
|
|
74
|
-
field_error: Forms::Plain::FieldError, field_hint: Forms::Plain::FieldHint,
|
|
75
|
-
submit: Forms::Plain::Submit, row: Forms::Plain::Row, group: Forms::Plain::Group,
|
|
76
|
-
**reactive_roles(Forms::Plain::TagField, Forms::Plain::RootlessTagField)
|
|
77
|
-
})
|
|
78
|
-
end
|
|
52
|
+
def plain = PLAIN
|
|
79
53
|
|
|
80
54
|
private
|
|
81
55
|
|
|
82
56
|
# phlex-reactive-gated roles: mapped only when the soft dep is present. When
|
|
83
57
|
# absent, the leaf classes aren't autoloaded, so these roles are simply not
|
|
84
58
|
# registered (fetching one raises the theme's own KeyError with the list).
|
|
85
|
-
|
|
59
|
+
# Takes the namespace, not the classes: naming Forms::TagField without
|
|
60
|
+
# phlex-reactive would raise NameError before the guard ran.
|
|
61
|
+
def reactive_roles(namespace)
|
|
86
62
|
return {} unless defined?(Phlex::Reactive)
|
|
87
63
|
|
|
88
|
-
{ tag_field:
|
|
64
|
+
{ tag_field: namespace::TagField, rootless_tag_field: namespace::RootlessTagField }
|
|
89
65
|
end
|
|
90
66
|
end
|
|
67
|
+
|
|
68
|
+
# Both built-ins are built once, when this file loads (never memoized by a
|
|
69
|
+
# reader, so no two threads race to build one; PhlexForms.finalize! loads
|
|
70
|
+
# it at boot), and the phlex-reactive probe is evaluated once, here.
|
|
71
|
+
DAISY = new({
|
|
72
|
+
input: Forms::Input, select: Forms::Select, choices_select: Forms::ChoicesSelect,
|
|
73
|
+
textarea: Forms::Textarea, rich_textarea: Forms::RichTextarea,
|
|
74
|
+
checkbox: Forms::Checkbox, toggle: Forms::Toggle, radio: Forms::Radio,
|
|
75
|
+
checkbox_group: Forms::CheckboxGroup,
|
|
76
|
+
file: Forms::FileInput, wrapped_input: Forms::WrappedInput, hidden: Forms::Hidden,
|
|
77
|
+
control: Forms::FormControl, label: Forms::Label,
|
|
78
|
+
field_error: Forms::FieldError, field_hint: Forms::FieldHint,
|
|
79
|
+
submit: Forms::Submit, row: Forms::Row, group: Forms::Group,
|
|
80
|
+
# tag_field is phlex-reactive-gated (ClientBindings); only registered
|
|
81
|
+
# when the soft dep is present, else :tag_field raises a clear KeyError.
|
|
82
|
+
**reactive_roles(Forms)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
# choices_select degrades to the native plain select (no choices.js) and
|
|
86
|
+
# rich_textarea to a plain textarea; toggle renders as a checkbox.
|
|
87
|
+
PLAIN = new({
|
|
88
|
+
input: Forms::Plain::Input, select: Forms::Plain::Select, choices_select: Forms::Plain::Select,
|
|
89
|
+
textarea: Forms::Plain::Textarea, rich_textarea: Forms::Plain::Textarea,
|
|
90
|
+
checkbox: Forms::Plain::Checkbox, toggle: Forms::Plain::Checkbox, radio: Forms::Plain::Radio,
|
|
91
|
+
checkbox_group: Forms::Plain::CheckboxGroup,
|
|
92
|
+
file: Forms::Plain::FileInput, wrapped_input: Forms::Plain::WrappedInput, hidden: Forms::Hidden,
|
|
93
|
+
control: Forms::Plain::Control, label: Forms::Plain::Label,
|
|
94
|
+
field_error: Forms::Plain::FieldError, field_hint: Forms::Plain::FieldHint,
|
|
95
|
+
submit: Forms::Plain::Submit, row: Forms::Plain::Row, group: Forms::Plain::Group,
|
|
96
|
+
**reactive_roles(Forms::Plain)
|
|
97
|
+
})
|
|
91
98
|
end
|
|
92
99
|
end
|
data/lib/phlex_forms/version.rb
CHANGED
data/lib/phlex_forms.rb
CHANGED
|
@@ -10,6 +10,11 @@ require "zeitwerk"
|
|
|
10
10
|
require "daisy_ui"
|
|
11
11
|
|
|
12
12
|
begin
|
|
13
|
+
# phlex-reactive's Sortable calls Hash#to_json while it loads without
|
|
14
|
+
# requiring json (a Rails host has it loaded already). Finalizing builds the
|
|
15
|
+
# themes, which load the tag-field leaves and with them Sortable, so without
|
|
16
|
+
# Rails `configure` would raise NoMethodError.
|
|
17
|
+
require "json"
|
|
13
18
|
require "phlex/reactive"
|
|
14
19
|
rescue LoadError
|
|
15
20
|
# phlex-reactive is a soft dependency: without it the `live` server-truth
|
|
@@ -32,17 +37,38 @@ module PhlexForms
|
|
|
32
37
|
# used but its backing dependency is not available.
|
|
33
38
|
class FeatureUnavailable < Error; end
|
|
34
39
|
|
|
40
|
+
# @configuration is built when this file loads (see the bottom) and replaced
|
|
41
|
+
# only by configure and reset_configuration!, on the main Ractor at boot or
|
|
42
|
+
# between tests (a non-main Ractor cannot write a module ivar). Once finalized
|
|
43
|
+
# it is frozen and Ractor-shareable, so any Ractor may read it.
|
|
44
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
35
45
|
class << self
|
|
36
|
-
|
|
37
|
-
@configuration ||= Configuration.new
|
|
38
|
-
end
|
|
46
|
+
attr_reader :configuration
|
|
39
47
|
alias config configuration
|
|
40
48
|
|
|
49
|
+
# Yields the configuration, then finalizes it unless the Rails engine is
|
|
50
|
+
# loaded (its after_initialize finalizes once every initializer has run). A
|
|
51
|
+
# finalized configuration is never edited in place: the block gets a copy,
|
|
52
|
+
# which is finalized and swapped in whole. Returns the block's value.
|
|
41
53
|
def configure
|
|
42
|
-
|
|
54
|
+
if @configuration.frozen?
|
|
55
|
+
draft = @configuration.dup
|
|
56
|
+
result = yield(draft)
|
|
57
|
+
@configuration = draft.finalize!
|
|
58
|
+
else
|
|
59
|
+
result = yield(@configuration)
|
|
60
|
+
finalize! unless defined?(PhlexForms::Engine)
|
|
61
|
+
end
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Freezes the configuration with Ractor-shareable settings (see
|
|
66
|
+
# Configuration#finalize!). Idempotent; returns the configuration.
|
|
67
|
+
def finalize!
|
|
68
|
+
@configuration.finalize!
|
|
43
69
|
end
|
|
44
70
|
|
|
45
|
-
# Reset configuration to defaults. Intended for test isolation.
|
|
71
|
+
# Reset configuration to defaults, open again. Intended for test isolation.
|
|
46
72
|
def reset_configuration!
|
|
47
73
|
@configuration = Configuration.new
|
|
48
74
|
end
|
|
@@ -97,6 +123,10 @@ end
|
|
|
97
123
|
|
|
98
124
|
loader.setup
|
|
99
125
|
|
|
126
|
+
# The default configuration, built at load (never lazily, so no two readers
|
|
127
|
+
# race to create it).
|
|
128
|
+
PhlexForms.reset_configuration!
|
|
129
|
+
|
|
100
130
|
# Optional Rails integration (Stimulus controllers via importmap/assets, and
|
|
101
131
|
# the gem's default locale files). Loaded only under Rails; the gem stays pure
|
|
102
132
|
# Phlex otherwise.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-forms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -189,6 +189,7 @@ files:
|
|
|
189
189
|
- lib/phlex_forms/inference.rb
|
|
190
190
|
- lib/phlex_forms/inline_icons.rb
|
|
191
191
|
- lib/phlex_forms/rubocop.rb
|
|
192
|
+
- lib/phlex_forms/shareable.rb
|
|
192
193
|
- lib/phlex_forms/theme.rb
|
|
193
194
|
- lib/phlex_forms/version.rb
|
|
194
195
|
- lib/rubocop/phlex_forms/cop/legacy_form_method.rb
|