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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "arbre"
|
|
4
|
+
|
|
5
|
+
require "weft/error"
|
|
6
|
+
require "weft/registry"
|
|
7
|
+
|
|
8
|
+
module Weft
|
|
9
|
+
class Page < Arbre::Component
|
|
10
|
+
# The asset surface of the page head: the register_* family (class-level
|
|
11
|
+
# DSL, accumulate semantics — subclasses add atop their parents) plus the
|
|
12
|
+
# rendering and URL-resolution machinery that emits them. Mixed into
|
|
13
|
+
# Weft::Page alongside Head, whose build_head drives render_assets.
|
|
14
|
+
module Assets
|
|
15
|
+
def self.included(base) = base.extend(ClassMethods)
|
|
16
|
+
|
|
17
|
+
HTMX_SRC = "https://unpkg.com/htmx.org@2.0.4"
|
|
18
|
+
HTMX_ATTRS = {
|
|
19
|
+
integrity: "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+",
|
|
20
|
+
crossorigin: "anonymous"
|
|
21
|
+
}.freeze
|
|
22
|
+
HTMX_SSE_SRC = "https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"
|
|
23
|
+
HTMX_SSE_ATTRS = {
|
|
24
|
+
integrity: "sha384-fw+eTlCc7suMV/1w/7fr2/PmwElUIt5i82bi+qTiLXvjRXZ2/FkiTNA/w0MhXnGI",
|
|
25
|
+
crossorigin: "anonymous"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Class-level registration DSL. Each register_* call accumulates; the
|
|
29
|
+
# matching reader returns own + inherited entries in ancestor order.
|
|
30
|
+
module ClassMethods
|
|
31
|
+
# Register a stylesheet to include in the page head.
|
|
32
|
+
#
|
|
33
|
+
# Absolute URLs (http(s)://, protocol-relative //, leading /) render as-is.
|
|
34
|
+
# Bare-relative paths resolve against a static_assets bundle: either the
|
|
35
|
+
# one named via `assets:`, or the :default bundle if one is configured.
|
|
36
|
+
# When neither applies, the path passes through unchanged and the browser
|
|
37
|
+
# resolves it relative to the current page URL.
|
|
38
|
+
#
|
|
39
|
+
# register_stylesheet "https://cdn.example.com/bootstrap.css"
|
|
40
|
+
# register_stylesheet "css/app.css" # resolves against :default
|
|
41
|
+
# register_stylesheet "tailwind/tw.css", assets: :vendor
|
|
42
|
+
def register_stylesheet(href, assets: nil)
|
|
43
|
+
own_stylesheets << { href: href, assets: assets }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register a script to include in the page head. Same resolution rules
|
|
47
|
+
# as register_stylesheet. Additional kwargs become HTML attributes on
|
|
48
|
+
# the <script> tag.
|
|
49
|
+
#
|
|
50
|
+
# register_script "https://cdn.example.com/app.js", defer: "defer"
|
|
51
|
+
# register_script "js/app.js" # resolves against :default
|
|
52
|
+
# register_script "vendor/x.js", assets: :vendor, defer: "defer"
|
|
53
|
+
def register_script(src, assets: nil, **html_attrs)
|
|
54
|
+
own_scripts << { src: src, attrs: html_attrs, assets: assets }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Register a block of inline CSS to include in the page head.
|
|
58
|
+
# Each registered string emits as its own <style> tag (subclasses
|
|
59
|
+
# add on top of their parent's contributions; nothing replaces).
|
|
60
|
+
#
|
|
61
|
+
# register_inline_css <<~CSS
|
|
62
|
+
# .card { padding: 1rem; }
|
|
63
|
+
# CSS
|
|
64
|
+
def register_inline_css(css)
|
|
65
|
+
own_inline_css << css
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Register a block of inline JavaScript to include in the page head.
|
|
69
|
+
# Same accumulate semantics as register_inline_css; emitted after the
|
|
70
|
+
# registered external scripts, so registered libraries are in reach.
|
|
71
|
+
#
|
|
72
|
+
# register_inline_js "htmx.logAll();"
|
|
73
|
+
def register_inline_js(javascript)
|
|
74
|
+
own_inline_js << javascript
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# All registered stylesheets (own + inherited).
|
|
78
|
+
def stylesheets
|
|
79
|
+
if superclass.respond_to?(:stylesheets)
|
|
80
|
+
superclass.stylesheets + own_stylesheets
|
|
81
|
+
else
|
|
82
|
+
own_stylesheets.dup
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# All registered scripts (own + inherited).
|
|
87
|
+
def scripts
|
|
88
|
+
if superclass.respond_to?(:scripts)
|
|
89
|
+
superclass.scripts + own_scripts
|
|
90
|
+
else
|
|
91
|
+
own_scripts.dup
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# All registered inline CSS blocks (own + inherited).
|
|
96
|
+
def inline_css
|
|
97
|
+
if superclass.respond_to?(:inline_css)
|
|
98
|
+
superclass.inline_css + own_inline_css
|
|
99
|
+
else
|
|
100
|
+
own_inline_css.dup
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# All registered inline JS blocks (own + inherited).
|
|
105
|
+
def inline_js
|
|
106
|
+
if superclass.respond_to?(:inline_js)
|
|
107
|
+
superclass.inline_js + own_inline_js
|
|
108
|
+
else
|
|
109
|
+
own_inline_js.dup
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def own_stylesheets = @own_stylesheets ||= []
|
|
116
|
+
def own_scripts = @own_scripts ||= []
|
|
117
|
+
def own_inline_css = @own_inline_css ||= []
|
|
118
|
+
def own_inline_js = @own_inline_js ||= []
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def render_assets
|
|
124
|
+
render_stylesheets
|
|
125
|
+
render_htmx_script if Weft.configuration.include_htmx
|
|
126
|
+
render_sse_script if include_sse_ext?
|
|
127
|
+
render_scripts
|
|
128
|
+
render_inline_js
|
|
129
|
+
render_inline_css
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_stylesheets
|
|
133
|
+
self.class.stylesheets.each do |entry|
|
|
134
|
+
link href: resolve_asset_url(entry[:href], assets: entry[:assets]), rel: "stylesheet"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def render_scripts
|
|
139
|
+
self.class.scripts.each do |entry|
|
|
140
|
+
script(src: resolve_asset_url(entry[:src], assets: entry[:assets]), **entry[:attrs])
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def render_inline_js
|
|
145
|
+
self.class.inline_js.each { |js| script { text_node js.html_safe } }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render_inline_css
|
|
149
|
+
self.class.inline_css.each { |css| style { text_node css.html_safe } }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def render_htmx_script
|
|
153
|
+
script src: HTMX_SRC, **HTMX_ATTRS
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def render_sse_script
|
|
157
|
+
script src: HTMX_SSE_SRC, **HTMX_SSE_ATTRS
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Resolve the include_sse_ext configuration into a boolean. :auto defers
|
|
161
|
+
# to the Registry; true/false short-circuit it.
|
|
162
|
+
def include_sse_ext?
|
|
163
|
+
case Weft.configuration.include_sse_ext
|
|
164
|
+
when true then true
|
|
165
|
+
when false then false
|
|
166
|
+
else Weft.registry.any_sse_components?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Resolve a registered asset path to a final URL.
|
|
171
|
+
#
|
|
172
|
+
# - Absolute URLs (http(s)://, //, /) pass through unchanged. Passing an
|
|
173
|
+
# `assets:` kwarg alongside an absolute URL raises — the kwarg is only
|
|
174
|
+
# meaningful for relative paths, and accepting it silently would train
|
|
175
|
+
# callers to attach it everywhere "just in case."
|
|
176
|
+
# - Bare-relative paths with an explicit `assets: :name` resolve against
|
|
177
|
+
# that bundle's root. Unknown bundle names raise with a list of
|
|
178
|
+
# configured names.
|
|
179
|
+
# - Bare-relative paths without an `assets:` kwarg resolve against the
|
|
180
|
+
# :default bundle if one is configured; otherwise they pass through and
|
|
181
|
+
# the browser interprets them relative to the current page URL.
|
|
182
|
+
def resolve_asset_url(path, assets: nil)
|
|
183
|
+
return resolve_absolute_asset_url(path, assets) if absolute_asset_url?(path)
|
|
184
|
+
|
|
185
|
+
resolve_relative_asset_url(path, assets)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def resolve_absolute_asset_url(path, assets)
|
|
189
|
+
raise_absolute_with_assets_kwarg!(path, assets) if assets
|
|
190
|
+
|
|
191
|
+
path
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_relative_asset_url(path, assets)
|
|
195
|
+
bundles = Weft.configuration.static_assets
|
|
196
|
+
target = assets&.to_sym || (bundles.key?(:default) ? :default : nil)
|
|
197
|
+
return path unless target
|
|
198
|
+
|
|
199
|
+
bundle = bundles[target]
|
|
200
|
+
raise_unknown_assets_bundle!(path, target, bundles) unless bundle
|
|
201
|
+
|
|
202
|
+
"#{bundle[:root]}/#{path}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def absolute_asset_url?(path)
|
|
206
|
+
path.start_with?("http://", "https://", "//", "/")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def raise_absolute_with_assets_kwarg!(path, assets)
|
|
210
|
+
raise Weft::InvalidUsage,
|
|
211
|
+
"static asset #{path.inspect}: assets: #{assets.inspect} " \
|
|
212
|
+
"is only meaningful for relative paths (absolute URLs render as-is)"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def raise_unknown_assets_bundle!(path, target, bundles)
|
|
216
|
+
raise Weft::InvalidUsage,
|
|
217
|
+
"static asset #{path.inspect} references assets bundle #{target.inspect}, " \
|
|
218
|
+
"but no such bundle is configured. Configured bundles: #{bundles.keys.inspect}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "arbre"
|
|
4
|
+
|
|
5
|
+
require "weft/dsl/sandbox"
|
|
6
|
+
require "weft/error"
|
|
7
|
+
require "weft/params"
|
|
8
|
+
|
|
9
|
+
module Weft
|
|
10
|
+
class Page < Arbre::Component
|
|
11
|
+
# The document-head surface: the `title` verb (class-level DSL, override
|
|
12
|
+
# semantics — nearest declaration in the ancestry wins) and the head
|
|
13
|
+
# assembly that consumes it. Mixed into Weft::Page alongside Assets,
|
|
14
|
+
# whose render_assets this module's build_head drives.
|
|
15
|
+
module Head
|
|
16
|
+
def self.included(base) = base.extend(ClassMethods)
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
# Declare the page's <title>. A static value, or a block computed from
|
|
20
|
+
# the page's params at render time — a `(params) → value` function run
|
|
21
|
+
# in the verb sandbox, like every other verb block.
|
|
22
|
+
#
|
|
23
|
+
# title "Orders"
|
|
24
|
+
# title { |params| "Order ##{params.order_id}" }
|
|
25
|
+
#
|
|
26
|
+
# The nearest declaration in the class ancestry wins; without one
|
|
27
|
+
# anywhere, the title falls back to "Weft".
|
|
28
|
+
def title(value = nil, &block)
|
|
29
|
+
if value && block
|
|
30
|
+
raise Weft::InvalidDefinition, "#{name}: title takes a value or a block, not both"
|
|
31
|
+
elsif value.nil? && block.nil?
|
|
32
|
+
raise Weft::InvalidDefinition, "#{name}: title needs a value or a block"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@title_declaration = block || value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The declared title (own or nearest inherited); nil when no ancestor declares.
|
|
39
|
+
def title_declaration
|
|
40
|
+
if instance_variable_defined?(:@title_declaration)
|
|
41
|
+
@title_declaration
|
|
42
|
+
elsif superclass.respond_to?(:title_declaration)
|
|
43
|
+
superclass.title_declaration
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def build_head
|
|
51
|
+
insert_tag(Arbre::HTML::Head) do
|
|
52
|
+
meta charset: "utf-8"
|
|
53
|
+
meta name: "viewport", content: "width=device-width, initial-scale=1"
|
|
54
|
+
title { text_node resolved_page_title }
|
|
55
|
+
render_assets
|
|
56
|
+
render_htmx_config
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The page's <title> text: the declared title (static, or block-computed
|
|
61
|
+
# from params in the verb sandbox), else "Weft".
|
|
62
|
+
def resolved_page_title
|
|
63
|
+
case (declared = self.class.title_declaration)
|
|
64
|
+
when Proc then Weft::DSL::Sandbox.run(@params || Weft::Params.new({}), &declared)
|
|
65
|
+
when nil then "Weft"
|
|
66
|
+
else declared
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_htmx_config
|
|
71
|
+
script do
|
|
72
|
+
text_node htmx_config_js.html_safe
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def htmx_config_js
|
|
77
|
+
<<~JS
|
|
78
|
+
htmx.config.responseHandling = [
|
|
79
|
+
{code: "204", swap: false},
|
|
80
|
+
{code: "[23]..", swap: true},
|
|
81
|
+
{code: "[45]..", swap: true, error: true}
|
|
82
|
+
];
|
|
83
|
+
JS
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/weft/page.rb
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "arbre"
|
|
3
4
|
require "uri"
|
|
4
5
|
|
|
6
|
+
require "weft/context"
|
|
7
|
+
require "weft/context/interception"
|
|
8
|
+
require "weft/context/traversal"
|
|
9
|
+
require "weft/dsl/containers"
|
|
10
|
+
require "weft/dsl/params"
|
|
11
|
+
require "weft/dsl/recoveries"
|
|
12
|
+
require "weft/error"
|
|
13
|
+
require "weft/page/assets"
|
|
14
|
+
require "weft/page/head"
|
|
15
|
+
require "weft/registry"
|
|
16
|
+
require "weft/registry/eligibility"
|
|
17
|
+
|
|
5
18
|
module Weft
|
|
6
19
|
# Document shell component. Renders the full HTML skeleton (doctype,
|
|
7
|
-
# head, body)
|
|
8
|
-
#
|
|
20
|
+
# head, body); the head surface — the `title` verb and the registered
|
|
21
|
+
# scripts and stylesheets — lives in Page::Head and Page::Assets.
|
|
22
|
+
# Subclass to add application-specific assets and CSS.
|
|
9
23
|
#
|
|
10
24
|
# Pages auto-route via page_path declarations or class-name inference.
|
|
11
25
|
# The Router serves them as full documents at the resolved URL patterns.
|
|
12
26
|
#
|
|
13
27
|
# class OrderDetailPage < Weft::Page
|
|
14
28
|
# self.page_path = "/orders/:order_id"
|
|
15
|
-
#
|
|
29
|
+
# param :order_id
|
|
16
30
|
# end
|
|
17
31
|
#
|
|
18
32
|
# Subclasses without an explicit page_path auto-infer one from the class
|
|
@@ -20,13 +34,16 @@ module Weft
|
|
|
20
34
|
# stripped (DashboardPage and Dashboard both route at "/dashboard"). Use
|
|
21
35
|
# abstract! to opt out — typical for an intermediate base class that hosts
|
|
22
36
|
# shared assets and helpers but isn't itself a destination.
|
|
23
|
-
class Page < Arbre::Component
|
|
37
|
+
class Page < Arbre::Component
|
|
24
38
|
extend Weft::Registry::Eligibility
|
|
25
39
|
|
|
26
40
|
include Weft::Context::Interception
|
|
27
|
-
include Weft::DSL::
|
|
41
|
+
include Weft::DSL::Params
|
|
28
42
|
include Weft::DSL::Recoveries
|
|
29
43
|
include Weft::DSL::Containers
|
|
44
|
+
include Weft::Context::Traversal
|
|
45
|
+
include Assets
|
|
46
|
+
include Head
|
|
30
47
|
|
|
31
48
|
# @!method weft_page(*args, &block)
|
|
32
49
|
# @api private
|
|
@@ -34,20 +51,9 @@ module Weft
|
|
|
34
51
|
# subclass Weft::Page and render via the Router; they do not call this.
|
|
35
52
|
builder_method :weft_page
|
|
36
53
|
|
|
37
|
-
HTMX_SRC = "https://unpkg.com/htmx.org@2.0.4"
|
|
38
|
-
HTMX_ATTRS = {
|
|
39
|
-
integrity: "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+",
|
|
40
|
-
crossorigin: "anonymous"
|
|
41
|
-
}.freeze
|
|
42
|
-
HTMX_SSE_SRC = "https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"
|
|
43
|
-
HTMX_SSE_ATTRS = {
|
|
44
|
-
integrity: "sha384-fw+eTlCc7suMV/1w/7fr2/PmwElUIt5i82bi+qTiLXvjRXZ2/FkiTNA/w0MhXnGI",
|
|
45
|
-
crossorigin: "anonymous"
|
|
46
|
-
}.freeze
|
|
47
|
-
|
|
48
54
|
class << self
|
|
49
55
|
# Class-level page path pattern. Sinatra-style string with :param segments.
|
|
50
|
-
# Bidirectional: forward (interpolate
|
|
56
|
+
# Bidirectional: forward (interpolate params → URL) and reverse (match request → params).
|
|
51
57
|
#
|
|
52
58
|
# self.page_path = "/orders/:order_id"
|
|
53
59
|
def page_path
|
|
@@ -60,28 +66,28 @@ module Weft
|
|
|
60
66
|
|
|
61
67
|
attr_writer :page_path
|
|
62
68
|
|
|
63
|
-
# Resolve the page path by interpolating
|
|
69
|
+
# Resolve the page path by interpolating params into the pattern.
|
|
64
70
|
# OrderDetailPage.resolve_page_path(order_id: "42") # => "/orders/42"
|
|
65
|
-
def resolve_page_path(
|
|
71
|
+
def resolve_page_path(params = {})
|
|
66
72
|
pattern = page_path || default_page_path
|
|
67
|
-
pattern.gsub(/:(\w+)/) {
|
|
73
|
+
pattern.gsub(/:(\w+)/) { params[::Regexp.last_match(1).to_sym] || ":#{::Regexp.last_match(1)}" }
|
|
68
74
|
end
|
|
69
75
|
|
|
70
|
-
# Build a redirect URL targeting this page with the given
|
|
71
|
-
# Path :param segments interpolate from
|
|
72
|
-
#
|
|
76
|
+
# Build a redirect URL targeting this page with the given params.
|
|
77
|
+
# Path :param segments interpolate from params; declared-but-not-path
|
|
78
|
+
# params become query string entries. Anything not in the page's
|
|
73
79
|
# declared schema is discarded — never leaks into the URL.
|
|
74
80
|
#
|
|
75
81
|
# class OrderDetailPage < Weft::Page
|
|
76
82
|
# self.page_path = "/orders/:order_id"
|
|
77
|
-
#
|
|
78
|
-
#
|
|
83
|
+
# param :order_id
|
|
84
|
+
# param :highlight_section
|
|
79
85
|
# end
|
|
80
86
|
# OrderDetailPage.redirect_url(order_id: 42, highlight_section: "items", junk: "x")
|
|
81
87
|
# # => "/orders/42?highlight_section=items"
|
|
82
|
-
def redirect_url(
|
|
83
|
-
path = resolve_page_path(
|
|
84
|
-
query =
|
|
88
|
+
def redirect_url(params = {})
|
|
89
|
+
path = resolve_page_path(params)
|
|
90
|
+
query = params.slice(*(self.params.keys - path_param_keys)).compact
|
|
85
91
|
query.empty? ? path : "#{path}?#{::URI.encode_www_form(query)}"
|
|
86
92
|
end
|
|
87
93
|
|
|
@@ -97,12 +103,12 @@ module Weft
|
|
|
97
103
|
# A page is inferred-routable if it has an explicit page_path, or if its
|
|
98
104
|
# class name yields a usable default — i.e. the demodulized name has a
|
|
99
105
|
# non-empty stem after stripping any trailing "Page" suffix. The suffix
|
|
100
|
-
# is optional: FooBarPage and BazBar both route. Pages with
|
|
106
|
+
# is optional: FooBarPage and BazBar both route. Pages with params
|
|
101
107
|
# are not inferred-routable; they require an explicit page_path (a
|
|
102
108
|
# parameterized route can't be derived from the name; see default_page_path).
|
|
103
109
|
def inferred_routable?
|
|
104
110
|
return true if instance_variable_defined?(:@page_path)
|
|
105
|
-
return false if
|
|
111
|
+
return false if params.any?
|
|
106
112
|
|
|
107
113
|
!name.to_s.delete_suffix("Page").demodulize.empty?
|
|
108
114
|
end
|
|
@@ -113,98 +119,23 @@ module Weft
|
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
# Render this page as a full HTML document outside any Arbre DSL context.
|
|
116
|
-
#
|
|
117
|
-
# for
|
|
118
|
-
|
|
122
|
+
# The kwargs are pseudo-wire: exactly what a request's query/path params
|
|
123
|
+
# would carry. Used by the Router for full-document responses, and
|
|
124
|
+
# available to users for testing or standalone rendering.
|
|
125
|
+
def render(**wire_params)
|
|
119
126
|
klass = self
|
|
120
|
-
Weft::Context.new({}, nil) do
|
|
121
|
-
insert_tag(klass
|
|
127
|
+
Weft::Context.new({}, nil, wire_params: wire_params) do
|
|
128
|
+
insert_tag(klass)
|
|
122
129
|
end.to_s
|
|
123
130
|
end
|
|
124
131
|
|
|
125
|
-
# Register a stylesheet to include in the page head.
|
|
126
|
-
#
|
|
127
|
-
# Absolute URLs (http(s)://, protocol-relative //, leading /) render as-is.
|
|
128
|
-
# Bare-relative paths resolve against a static_assets bundle: either the
|
|
129
|
-
# one named via `assets:`, or the :default bundle if one is configured.
|
|
130
|
-
# When neither applies, the path passes through unchanged and the browser
|
|
131
|
-
# resolves it relative to the current page URL.
|
|
132
|
-
#
|
|
133
|
-
# register_stylesheet "https://cdn.example.com/bootstrap.css"
|
|
134
|
-
# register_stylesheet "css/app.css" # resolves against :default
|
|
135
|
-
# register_stylesheet "tailwind/tw.css", assets: :vendor
|
|
136
|
-
def register_stylesheet(href, assets: nil)
|
|
137
|
-
own_stylesheets << { href: href, assets: assets }
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Register a script to include in the page head. Same resolution rules
|
|
141
|
-
# as register_stylesheet. Additional kwargs become HTML attributes on
|
|
142
|
-
# the <script> tag.
|
|
143
|
-
#
|
|
144
|
-
# register_script "https://cdn.example.com/app.js", defer: "defer"
|
|
145
|
-
# register_script "js/app.js" # resolves against :default
|
|
146
|
-
# register_script "vendor/x.js", assets: :vendor, defer: "defer"
|
|
147
|
-
def register_script(src, assets: nil, **html_attrs)
|
|
148
|
-
own_scripts << { src: src, attrs: html_attrs, assets: assets }
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Register a block of inline CSS to include in the page head.
|
|
152
|
-
# Each registered string emits as its own <style> tag (subclasses
|
|
153
|
-
# add on top of their parent's contributions; nothing replaces).
|
|
154
|
-
#
|
|
155
|
-
# register_css <<~CSS
|
|
156
|
-
# .card { padding: 1rem; }
|
|
157
|
-
# CSS
|
|
158
|
-
def register_css(css)
|
|
159
|
-
own_inline_css << css
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# All registered stylesheets (own + inherited).
|
|
163
|
-
def stylesheets
|
|
164
|
-
if superclass.respond_to?(:stylesheets)
|
|
165
|
-
superclass.stylesheets + own_stylesheets
|
|
166
|
-
else
|
|
167
|
-
own_stylesheets.dup
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# All registered scripts (own + inherited).
|
|
172
|
-
def scripts
|
|
173
|
-
if superclass.respond_to?(:scripts)
|
|
174
|
-
superclass.scripts + own_scripts
|
|
175
|
-
else
|
|
176
|
-
own_scripts.dup
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# All registered inline CSS blocks (own + inherited).
|
|
181
|
-
def inline_css
|
|
182
|
-
if superclass.respond_to?(:inline_css)
|
|
183
|
-
superclass.inline_css + own_inline_css
|
|
184
|
-
else
|
|
185
|
-
own_inline_css.dup
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
132
|
private
|
|
190
133
|
|
|
191
|
-
def own_stylesheets
|
|
192
|
-
@own_stylesheets ||= []
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def own_scripts
|
|
196
|
-
@own_scripts ||= []
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def own_inline_css
|
|
200
|
-
@own_inline_css ||= []
|
|
201
|
-
end
|
|
202
|
-
|
|
203
134
|
def default_page_path
|
|
204
|
-
if
|
|
135
|
+
if params.any?
|
|
205
136
|
raise Weft::InvalidDefinition,
|
|
206
|
-
"#{name} declares
|
|
207
|
-
"Set self.page_path = \"/your/path/:#{
|
|
137
|
+
"#{name} declares params but no explicit page_path. " \
|
|
138
|
+
"Set self.page_path = \"/your/path/:#{params.keys.first}\""
|
|
208
139
|
end
|
|
209
140
|
|
|
210
141
|
stem = name.to_s.delete_suffix("Page")
|
|
@@ -220,11 +151,17 @@ module Weft
|
|
|
220
151
|
end
|
|
221
152
|
end
|
|
222
153
|
|
|
154
|
+
# Params resolve at construction (see Weft::Component#initialize) so
|
|
155
|
+
# user build bodies can read them before super — e.g. computing body
|
|
156
|
+
# chrome from a record looked up by param.
|
|
157
|
+
def initialize(*)
|
|
158
|
+
super
|
|
159
|
+
@params = assembled_params if self.class.declared_keys.any?
|
|
160
|
+
end
|
|
161
|
+
|
|
223
162
|
def build(attributes = {})
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@page_title = attributes.delete(:title) || "Weft"
|
|
227
|
-
super(attributes.except(*schema.keys))
|
|
163
|
+
warn_declared_chrome_collisions(attributes)
|
|
164
|
+
super
|
|
228
165
|
build_head
|
|
229
166
|
@body_el = insert_tag(Arbre::HTML::Body)
|
|
230
167
|
end
|
|
@@ -241,127 +178,6 @@ module Weft
|
|
|
241
178
|
"<!DOCTYPE html>\n#{super}"
|
|
242
179
|
end
|
|
243
180
|
|
|
244
|
-
private
|
|
245
|
-
|
|
246
|
-
def build_head
|
|
247
|
-
insert_tag(Arbre::HTML::Head) do
|
|
248
|
-
meta charset: "utf-8"
|
|
249
|
-
meta name: "viewport", content: "width=device-width, initial-scale=1"
|
|
250
|
-
title { text_node @page_title }
|
|
251
|
-
render_assets
|
|
252
|
-
render_htmx_config
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def render_assets
|
|
257
|
-
render_stylesheets
|
|
258
|
-
render_htmx_script if Weft.configuration.include_htmx
|
|
259
|
-
render_sse_script if include_sse_ext?
|
|
260
|
-
render_scripts
|
|
261
|
-
render_inline_css
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def render_stylesheets
|
|
265
|
-
self.class.stylesheets.each do |entry|
|
|
266
|
-
link href: resolve_asset_url(entry[:href], assets: entry[:assets]), rel: "stylesheet"
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def render_scripts
|
|
271
|
-
self.class.scripts.each do |entry|
|
|
272
|
-
script(src: resolve_asset_url(entry[:src], assets: entry[:assets]), **entry[:attrs])
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Resolve a registered asset path to a final URL.
|
|
277
|
-
#
|
|
278
|
-
# - Absolute URLs (http(s)://, //, /) pass through unchanged. Passing an
|
|
279
|
-
# `assets:` kwarg alongside an absolute URL raises — the kwarg is only
|
|
280
|
-
# meaningful for relative paths, and accepting it silently would train
|
|
281
|
-
# callers to attach it everywhere "just in case."
|
|
282
|
-
# - Bare-relative paths with an explicit `assets: :name` resolve against
|
|
283
|
-
# that bundle's root. Unknown bundle names raise with a list of
|
|
284
|
-
# configured names.
|
|
285
|
-
# - Bare-relative paths without an `assets:` kwarg resolve against the
|
|
286
|
-
# :default bundle if one is configured; otherwise they pass through and
|
|
287
|
-
# the browser interprets them relative to the current page URL.
|
|
288
|
-
def resolve_asset_url(path, assets: nil)
|
|
289
|
-
return resolve_absolute_asset_url(path, assets) if absolute_asset_url?(path)
|
|
290
|
-
|
|
291
|
-
resolve_relative_asset_url(path, assets)
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def resolve_absolute_asset_url(path, assets)
|
|
295
|
-
raise_absolute_with_assets_kwarg!(path, assets) if assets
|
|
296
|
-
|
|
297
|
-
path
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def resolve_relative_asset_url(path, assets)
|
|
301
|
-
bundles = Weft.configuration.static_assets
|
|
302
|
-
target = assets&.to_sym || (bundles.key?(:default) ? :default : nil)
|
|
303
|
-
return path unless target
|
|
304
|
-
|
|
305
|
-
bundle = bundles[target]
|
|
306
|
-
raise_unknown_assets_bundle!(path, target, bundles) unless bundle
|
|
307
|
-
|
|
308
|
-
"#{bundle[:root]}/#{path}"
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def absolute_asset_url?(path)
|
|
312
|
-
path.start_with?("http://", "https://", "//", "/")
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def raise_absolute_with_assets_kwarg!(path, assets)
|
|
316
|
-
raise Weft::InvalidUsage,
|
|
317
|
-
"static asset #{path.inspect}: assets: #{assets.inspect} " \
|
|
318
|
-
"is only meaningful for relative paths (absolute URLs render as-is)"
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def raise_unknown_assets_bundle!(path, target, bundles)
|
|
322
|
-
raise Weft::InvalidUsage,
|
|
323
|
-
"static asset #{path.inspect} references assets bundle #{target.inspect}, " \
|
|
324
|
-
"but no such bundle is configured. Configured bundles: #{bundles.keys.inspect}"
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
def render_inline_css
|
|
328
|
-
self.class.inline_css.each { |css| style { text_node css.html_safe } }
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def render_htmx_script
|
|
332
|
-
script src: HTMX_SRC, **HTMX_ATTRS
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def render_sse_script
|
|
336
|
-
script src: HTMX_SSE_SRC, **HTMX_SSE_ATTRS
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
# Resolve the include_sse_ext configuration into a boolean. :auto defers
|
|
340
|
-
# to the Registry; true/false short-circuit it.
|
|
341
|
-
def include_sse_ext?
|
|
342
|
-
case Weft.configuration.include_sse_ext
|
|
343
|
-
when true then true
|
|
344
|
-
when false then false
|
|
345
|
-
else Weft.registry.any_sse_components?
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def render_htmx_config
|
|
350
|
-
script do
|
|
351
|
-
text_node htmx_config_js.html_safe
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
def htmx_config_js
|
|
356
|
-
<<~JS
|
|
357
|
-
htmx.config.responseHandling = [
|
|
358
|
-
{code: "204", swap: false},
|
|
359
|
-
{code: "[23]..", swap: true},
|
|
360
|
-
{code: "[45]..", swap: true, error: true}
|
|
361
|
-
];
|
|
362
|
-
JS
|
|
363
|
-
end
|
|
364
|
-
|
|
365
181
|
# Gem-default recovery edges. Symbol form defers resolution until
|
|
366
182
|
# error-handling time so reassigning the configuration knob propagates.
|
|
367
183
|
# NotFound declared first so its more-specific match wins over StandardError.
|