weft 0.1.0 → 0.2.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 +134 -28
- data/README.md +46 -23
- data/docs/app-patterns.md +8 -7
- data/docs/arbre.md +49 -18
- data/docs/configuration.md +42 -35
- data/docs/dsl.md +356 -105
- data/docs/error-handling.md +64 -24
- data/docs/examples/active-search.md +10 -10
- data/docs/examples/browser-dialogs.md +10 -10
- data/docs/examples/bulk-update.md +11 -11
- data/docs/examples/click-to-edit.md +14 -14
- data/docs/examples/click-to-load.md +8 -8
- data/docs/examples/delete-row.md +17 -19
- data/docs/examples/edit-row.md +17 -14
- data/docs/examples/file-upload.md +5 -5
- data/docs/examples/infinite-scroll.md +8 -8
- data/docs/examples/inline-expansion.md +8 -8
- data/docs/examples/inline-validation.md +17 -17
- data/docs/examples/lazy-loading.md +8 -8
- data/docs/examples/live-ticker.md +1 -1
- data/docs/examples/modal-dialog.md +3 -3
- data/docs/examples/progress-bar.md +1 -1
- data/docs/examples/reset-user-input.md +7 -7
- data/docs/examples/tabs.md +4 -4
- data/docs/examples/tooltip.md +8 -8
- data/docs/examples/updating-other-content.md +9 -9
- data/docs/examples/value-select.md +11 -11
- data/docs/params.md +112 -0
- data/docs/routing.md +13 -13
- data/docs/tutorial.md +46 -48
- data/lib/weft/action.rb +4 -2
- data/lib/weft/autoloading.rb +69 -0
- data/lib/weft/component.rb +97 -31
- data/lib/weft/configuration.rb +37 -5
- data/lib/weft/context/expansion.rb +184 -0
- data/lib/weft/context/interception.rb +22 -2
- data/lib/weft/context/modifiers.rb +78 -0
- data/lib/weft/context/traversal.rb +80 -0
- data/lib/weft/context/wiring.rb +85 -0
- data/lib/weft/context.rb +70 -164
- data/lib/weft/defaults/error_component.rb +57 -21
- data/lib/weft/defaults/error_page.rb +12 -10
- data/lib/weft/defaults/not_found_component.rb +14 -12
- data/lib/weft/defaults/not_found_page.rb +9 -8
- data/lib/weft/dsl/actions.rb +9 -9
- data/lib/weft/dsl/inclusions.rb +48 -11
- data/lib/weft/dsl/params.rb +265 -0
- data/lib/weft/dsl/recoveries.rb +36 -6
- data/lib/weft/dsl/sandbox.rb +26 -0
- data/lib/weft/dsl/triggers.rb +28 -8
- data/lib/weft/dsl/updates.rb +31 -8
- data/lib/weft/error.rb +12 -1
- data/lib/weft/page/assets.rb +222 -0
- data/lib/weft/page/head.rb +87 -0
- data/lib/weft/page.rb +55 -239
- data/lib/weft/params/assembly.rb +170 -0
- data/lib/weft/params.rb +138 -0
- data/lib/weft/presets.rb +96 -0
- data/lib/weft/redirect.rb +7 -7
- data/lib/weft/registry/eligibility.rb +5 -19
- data/lib/weft/registry.rb +58 -18
- data/lib/weft/resolver.rb +48 -20
- data/lib/weft/router/actions.rb +106 -22
- data/lib/weft/router/errors.rb +223 -83
- data/lib/weft/router/oob_includes.rb +202 -17
- data/lib/weft/router/streaming.rb +86 -20
- data/lib/weft/router.rb +33 -25
- data/lib/weft/version.rb +1 -1
- data/lib/weft.rb +37 -24
- metadata +32 -8
- data/lib/weft/attributes.rb +0 -65
- data/lib/weft/dsl/attributes.rb +0 -43
- data/lib/weft/shorthands.rb +0 -57
data/lib/weft/resolver.rb
CHANGED
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "bigdecimal/util"
|
|
4
|
+
|
|
3
5
|
module Weft
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Projects a wire params hash (string or symbol keys) onto a component
|
|
7
|
+
# class's declared schema, coercing values with declared types.
|
|
8
|
+
# Components self-resolve at build time (see DSL::Params); the Router
|
|
9
|
+
# also calls this directly for error-path bookkeeping. Future home of
|
|
10
|
+
# the reification step (wire primitives → rich objects).
|
|
7
11
|
class Resolver
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
# The declarable wire types, each self-describing: how to coerce a
|
|
13
|
+
# present wire value (permissive Ruby — never raises on malformed
|
|
14
|
+
# input), and the classes a declared default may already be an
|
|
15
|
+
# instance of (declaration-side validation lives in DSL::Params).
|
|
16
|
+
# Self-description is the seam a future registration API extends.
|
|
17
|
+
TYPES = {
|
|
18
|
+
string: { coerce: :to_s.to_proc, classes: [String] },
|
|
19
|
+
integer: { coerce: :to_i.to_proc, classes: [Integer] },
|
|
20
|
+
float: { coerce: :to_f.to_proc, classes: [Float] },
|
|
21
|
+
boolean: { coerce: ->(v) { [true, "true", "1"].include?(v) },
|
|
22
|
+
classes: [TrueClass, FalseClass] },
|
|
23
|
+
decimal: { coerce: :to_d.to_proc, classes: [BigDecimal] }
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def resolve(component_class, params)
|
|
28
|
+
component_class.params.each_with_object({}) do |(name, meta), result|
|
|
29
|
+
raw = fetch_raw(params, name)
|
|
30
|
+
result[name] = raw.nil? ? meta[:default] : coerce(raw, meta[:type])
|
|
31
|
+
end
|
|
12
32
|
end
|
|
13
|
-
end
|
|
14
33
|
|
|
15
|
-
|
|
34
|
+
# Coerce only the keys actually present on the wire — no default fill.
|
|
35
|
+
# The construction-time source stack uses this to tell wire-satisfied
|
|
36
|
+
# keys apart from keys that fall through to lower sources.
|
|
37
|
+
def resolve_present(component_class, params)
|
|
38
|
+
component_class.params.each_with_object({}) do |(name, meta), result|
|
|
39
|
+
raw = fetch_raw(params, name)
|
|
40
|
+
result[name] = coerce(raw, meta[:type]) unless raw.nil?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
16
45
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else value
|
|
46
|
+
# Try both key shapes without `||` — a literal false must read as present.
|
|
47
|
+
def fetch_raw(params, name)
|
|
48
|
+
key = name.to_s
|
|
49
|
+
key = name unless params.key?(key)
|
|
50
|
+
params[key]
|
|
23
51
|
end
|
|
24
|
-
end
|
|
25
52
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
# A declared type: coerces via its TYPES entry; untyped params accept
|
|
54
|
+
# any value as-is.
|
|
55
|
+
def coerce(value, type)
|
|
56
|
+
entry = TYPES[type]
|
|
57
|
+
entry ? entry[:coerce].call(value) : value
|
|
30
58
|
end
|
|
31
59
|
end
|
|
32
60
|
end
|
data/lib/weft/router/actions.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "weft/dsl/sandbox"
|
|
4
|
+
require "weft/params"
|
|
5
|
+
require "weft/params/assembly"
|
|
6
|
+
require "weft/redirect"
|
|
7
|
+
|
|
3
8
|
module Weft
|
|
4
9
|
class Router
|
|
5
10
|
# Action-dispatch slice of the Router. Resolves a request path to a
|
|
@@ -9,9 +14,10 @@ module Weft
|
|
|
9
14
|
# `render_error`; the small `render_action_error` wrapper sets the
|
|
10
15
|
# destructive-swap header before delegating.
|
|
11
16
|
#
|
|
12
|
-
# Depends on Router internals: `
|
|
13
|
-
# `
|
|
14
|
-
# `
|
|
17
|
+
# Depends on Router internals: `filtered_params`, `handle_redirect`,
|
|
18
|
+
# `apply_trigger_header`, `render_error`, `headers`, and the
|
|
19
|
+
# OOB-include slice's `render_companions` / `applicable_inclusions` /
|
|
20
|
+
# `explicitly_named_inclusions`.
|
|
15
21
|
module Actions
|
|
16
22
|
private
|
|
17
23
|
|
|
@@ -40,37 +46,115 @@ module Weft
|
|
|
40
46
|
nil
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
# State 1 — what the request composes from the wire before any of the
|
|
50
|
+
# component's own code runs — is the bag the first verb block sees, and
|
|
51
|
+
# it carries every door `build` reads: wire params, derivations (still
|
|
52
|
+
# lazy), and defines. Only `receives` is missing, and structurally so:
|
|
53
|
+
# an action request has no caller and no enclosing build to hand
|
|
54
|
+
# anything over.
|
|
55
|
+
#
|
|
56
|
+
# A failure is recovered from wherever the request had got to, walked
|
|
57
|
+
# from whoever was in charge at the time: the declarer's chain against
|
|
58
|
+
# state 1 while its callable runs, and — once the callable has returned
|
|
59
|
+
# and control has passed on — the rendering component's chain against
|
|
60
|
+
# state 2.
|
|
43
61
|
def handle_action(action, component_class)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
returned = action.callable&.call(attrs)
|
|
62
|
+
state = Weft::Params::Assembly.for_request(component_class, filtered_params)
|
|
63
|
+
returned = Weft::DSL::Sandbox.run(state, &action.callable) if action.callable
|
|
48
64
|
return handle_redirect(returned) if returned.is_a?(Weft::Redirect)
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
render_action_error(action, component_class, state || Weft::Params.new({}), e)
|
|
67
|
+
else
|
|
68
|
+
render_action_response(action, component_class, state, returned)
|
|
69
|
+
end
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
# One universe per request: the response renders against the request's
|
|
72
|
+
# own wire, with the callable's returned hash riding as an overlay —
|
|
73
|
+
# every component in the response (the rendered class, its nested
|
|
74
|
+
# children, OOB companions) resolves from the same substrate, and the
|
|
75
|
+
# delta overrides or clears wire values at any depth. The rendered
|
|
76
|
+
# class projects its own schema; the declaring class's resolution
|
|
77
|
+
# never crowns a new universe.
|
|
78
|
+
#
|
|
79
|
+
# Delete-swap actions skip the primary render entirely: htmx discards
|
|
80
|
+
# the body on a delete swap, and the component's record is typically
|
|
81
|
+
# gone by now. OOB includes still ride (a 200, never a 204 — htmx
|
|
82
|
+
# refuses to swap 204s, which would skip the delete itself).
|
|
83
|
+
def render_action_response(action, component_class, state, returned)
|
|
84
|
+
overlay = returned.is_a?(Hash) ? returned : {}
|
|
85
|
+
composed = state.overlay(overlay)
|
|
86
|
+
apply_trigger_header(component_class, action.name)
|
|
87
|
+
slots = Set.new
|
|
88
|
+
primary = build_action_primary(action, overlay, composed, slots)
|
|
89
|
+
(primary ? primary.to_s : "") +
|
|
90
|
+
render_companions(action_companions(action, component_class, primary, composed, overlay), slots)
|
|
51
91
|
rescue StandardError => e
|
|
52
|
-
render_action_error(action,
|
|
92
|
+
render_action_error(action, action.renders, composed, e)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Which companions ride this response, in precedence order: the
|
|
96
|
+
# rendered component's first, then — on a transfer — the declaring
|
|
97
|
+
# component's explicitly named ones. One list, so two companions
|
|
98
|
+
# claiming a single DOM id are caught across the two sources and the
|
|
99
|
+
# rendered component's declaration keeps the slot.
|
|
100
|
+
def action_companions(action, component_class, primary, composed, overlay)
|
|
101
|
+
target = target_companions(action, component_class, primary, composed, overlay)
|
|
102
|
+
return target if action.renders.equal?(component_class)
|
|
103
|
+
|
|
104
|
+
target + declarer_companions(component_class, action.name, composed, overlay)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The rendered component's companions: its block reads the primary's
|
|
108
|
+
# rendered bag (rich values included) and each one branches it.
|
|
109
|
+
def target_companions(action, component_class, primary, composed, overlay)
|
|
110
|
+
context = action.renders.equal?(component_class) ? :action : :transfer
|
|
111
|
+
env = { universe: filtered_params, overlays: overlay, branch_bag: primary&.params }
|
|
112
|
+
view = includes_view(primary, composed, overlay)
|
|
113
|
+
applicable_inclusions(action.renders, context, action.name).map { |inc| [inc, view, env] }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The declaring component's companions on a transfer. This branch
|
|
117
|
+
# forks before the hand-off, so the block reads the declarer's own
|
|
118
|
+
# params plus the callable's overlay — never the target's picture —
|
|
119
|
+
# and there is no primary bag to branch, because nothing rendered the
|
|
120
|
+
# declarer and so no rich values exist on this path.
|
|
121
|
+
def declarer_companions(component_class, action_name, composed, overlay)
|
|
122
|
+
env = { universe: filtered_params, overlays: overlay }
|
|
123
|
+
explicitly_named_inclusions(component_class, action_name).map { |inc| [inc, composed, env] }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Delete-swap actions skip the primary render: htmx discards the body
|
|
127
|
+
# on a delete swap, and the component's record is typically gone.
|
|
128
|
+
# The primary claims its DOM slot first, so a companion aimed at the
|
|
129
|
+
# same id is turned away rather than swapping over the very fragment
|
|
130
|
+
# the response is about.
|
|
131
|
+
#
|
|
132
|
+
# The primary branches the state the request has already composed, so a
|
|
133
|
+
# derivation the callable forced isn't paid for twice — and a transfer
|
|
134
|
+
# target inherits it exactly as a nested child inherits its parent's.
|
|
135
|
+
# Declared defaults don't ride a branch, so the target's own fallbacks
|
|
136
|
+
# stay its own; to override an inherited value, return the key (an
|
|
137
|
+
# explicit nil clears it).
|
|
138
|
+
def build_action_primary(action, overlay, composed, slots)
|
|
139
|
+
return nil if action.swap == :delete
|
|
140
|
+
|
|
141
|
+
build_component_with_wire(action.renders, filtered_params, overlays: overlay,
|
|
142
|
+
branch_bag: composed, slots: slots)
|
|
53
143
|
end
|
|
54
144
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# downstream OOB includes still see callable-returned attrs.
|
|
61
|
-
def render_action_response(action, component_class, resolved_attrs, returned)
|
|
62
|
-
bag = returned.is_a?(Hash) ? resolved_attrs.merge(returned) : resolved_attrs
|
|
63
|
-
apply_trigger_header(component_class)
|
|
64
|
-
html = action.renders.render(**resolver.resolve(action.renders, bag))
|
|
65
|
-
html + render_oob_includes(component_class, Weft::Attributes.new(bag), action_name: action.name)
|
|
145
|
+
# The params view an inclusion block receives: the rendered primary's
|
|
146
|
+
# bag with the overlay applied (undeclared delta keys stay readable).
|
|
147
|
+
# On a delete-swap there is no primary — the composed state stands in.
|
|
148
|
+
def includes_view(primary, composed, overlay)
|
|
149
|
+
primary ? primary.params.overlay(overlay) : composed
|
|
66
150
|
end
|
|
67
151
|
|
|
68
152
|
# Error handling for actions. Adds HX-Reswap header when the action's
|
|
69
153
|
# swap strategy is destructive (e.g., :delete) so the error fragment
|
|
70
154
|
# renders visibly instead of the element being silently removed.
|
|
71
|
-
def render_action_error(action, component_class,
|
|
155
|
+
def render_action_error(action, component_class, resolved_params, error)
|
|
72
156
|
headers["HX-Reswap"] = "outerHTML" if action.swap == :delete
|
|
73
|
-
render_error(component_class,
|
|
157
|
+
render_error(component_class, resolved_params, error)
|
|
74
158
|
end
|
|
75
159
|
end
|
|
76
160
|
end
|