weft 0.1.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 +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/docs/app-patterns.md +268 -0
- data/docs/arbre.md +298 -0
- data/docs/configuration.md +219 -0
- data/docs/dsl.md +329 -0
- data/docs/error-handling.md +136 -0
- data/docs/examples/README.md +70 -0
- data/docs/examples/active-search.md +125 -0
- data/docs/examples/browser-dialogs.md +71 -0
- data/docs/examples/bulk-update.md +122 -0
- data/docs/examples/click-to-edit.md +113 -0
- data/docs/examples/click-to-load.md +77 -0
- data/docs/examples/delete-row.md +101 -0
- data/docs/examples/edit-row.md +146 -0
- data/docs/examples/file-upload.md +96 -0
- data/docs/examples/infinite-scroll.md +98 -0
- data/docs/examples/inline-expansion.md +98 -0
- data/docs/examples/inline-validation.md +101 -0
- data/docs/examples/keyboard-shortcuts.md +71 -0
- data/docs/examples/lazy-loading.md +96 -0
- data/docs/examples/live-ticker.md +91 -0
- data/docs/examples/modal-dialog.md +88 -0
- data/docs/examples/progress-bar.md +138 -0
- data/docs/examples/reset-user-input.md +101 -0
- data/docs/examples/tabs.md +102 -0
- data/docs/examples/tooltip.md +88 -0
- data/docs/examples/updating-other-content.md +169 -0
- data/docs/examples/value-select.md +109 -0
- data/docs/routing.md +131 -0
- data/docs/tutorial.md +372 -0
- data/lib/weft/action.rb +83 -0
- data/lib/weft/attributes.rb +65 -0
- data/lib/weft/component.rb +176 -0
- data/lib/weft/configuration.rb +177 -0
- data/lib/weft/context/interception.rb +22 -0
- data/lib/weft/context.rb +184 -0
- data/lib/weft/defaults/error_component.rb +63 -0
- data/lib/weft/defaults/error_page.rb +27 -0
- data/lib/weft/defaults/not_found_component.rb +44 -0
- data/lib/weft/defaults/not_found_page.rb +25 -0
- data/lib/weft/defaults.rb +9 -0
- data/lib/weft/dsl/actions.rb +72 -0
- data/lib/weft/dsl/attributes.rb +43 -0
- data/lib/weft/dsl/containers.rb +77 -0
- data/lib/weft/dsl/inclusions.rb +49 -0
- data/lib/weft/dsl/recoveries.rb +89 -0
- data/lib/weft/dsl/triggers.rb +40 -0
- data/lib/weft/dsl/updates.rb +86 -0
- data/lib/weft/error.rb +64 -0
- data/lib/weft/page.rb +371 -0
- data/lib/weft/redirect.rb +45 -0
- data/lib/weft/registry/eligibility.rb +58 -0
- data/lib/weft/registry.rb +202 -0
- data/lib/weft/resolver.rb +33 -0
- data/lib/weft/router/actions.rb +77 -0
- data/lib/weft/router/errors.rb +246 -0
- data/lib/weft/router/oob_includes.rb +34 -0
- data/lib/weft/router/streaming.rb +63 -0
- data/lib/weft/router.rb +191 -0
- data/lib/weft/shorthands.rb +57 -0
- data/lib/weft/version.rb +5 -0
- data/lib/weft.rb +124 -0
- metadata +169 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module Defaults
|
|
5
|
+
# Gem-default full-document page rendered for traditional (non-htmx)
|
|
6
|
+
# responses when a NotFound error falls through to the Page recovers
|
|
7
|
+
# chain. Thin wrapper around NotFoundComponent.
|
|
8
|
+
class NotFoundPage < Weft::Page
|
|
9
|
+
self.page_path = "/_weft/not_found"
|
|
10
|
+
|
|
11
|
+
attribute :request_path
|
|
12
|
+
attribute :status_code
|
|
13
|
+
|
|
14
|
+
def build(attributes = {})
|
|
15
|
+
attributes[:title] ||= "Not found"
|
|
16
|
+
super
|
|
17
|
+
insert_tag(
|
|
18
|
+
Weft::Defaults::NotFoundComponent,
|
|
19
|
+
request_path: @attrs.request_path,
|
|
20
|
+
status_code: @attrs.status_code
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Weft::Defaults namespace — gem-provided Page/Component classes used as
|
|
4
|
+
# fallback targets when no user-declared recovers entry matches. Pointed at
|
|
5
|
+
# by Weft::Configuration#error_component / #error_page / #not_found_page.
|
|
6
|
+
require "weft/defaults/error_component"
|
|
7
|
+
require "weft/defaults/error_page"
|
|
8
|
+
require "weft/defaults/not_found_component"
|
|
9
|
+
require "weft/defaults/not_found_page"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare user-initiated actions: `performs`,
|
|
6
|
+
# `transfers`, and the `dismisses` sugar. Included into Weft::Component.
|
|
7
|
+
module Actions
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.extend(ClassMethods)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
# Declare a user-initiated action on this component.
|
|
14
|
+
#
|
|
15
|
+
# performs :advance do |attrs|
|
|
16
|
+
# order = Order.find(attrs.order_id)
|
|
17
|
+
# Oms::PrepareOrder.call(order)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# performs method: :delete, swap: :delete do |attrs| ... end
|
|
21
|
+
def performs(name = nil, method: :post, swap: :outer_html, target: nil, &block)
|
|
22
|
+
action = Weft::Action.new(name: name, method: method, swap: swap,
|
|
23
|
+
target: target, renders: self, callable: block)
|
|
24
|
+
own_actions[[name, method]] = action
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Sugar for performs with swap: :delete. Removes the component from
|
|
28
|
+
# the DOM on success. The callable (if given) runs for side effects;
|
|
29
|
+
# the return value is rendered but htmx ignores the response body.
|
|
30
|
+
#
|
|
31
|
+
# dismisses :close # no side effects
|
|
32
|
+
# dismisses :remove do |attrs| # with side effects
|
|
33
|
+
# Item.find(attrs.item_id).archive!
|
|
34
|
+
# end
|
|
35
|
+
def dismisses(name = nil, method: :delete, &)
|
|
36
|
+
performs(name, method: method, swap: :delete, &)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Declare a transfer — an action that renders a different component.
|
|
40
|
+
#
|
|
41
|
+
# transfers :edit, to: EditableOrderHeader do |attrs|
|
|
42
|
+
# { mode: "full" }
|
|
43
|
+
# end
|
|
44
|
+
def transfers(name = nil, to:, method: :post, swap: :outer_html, target: nil, &block)
|
|
45
|
+
action = Weft::Action.new(name: name, method: method, swap: swap,
|
|
46
|
+
target: target, renders: to, callable: block)
|
|
47
|
+
own_actions[[name, method]] = action
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# All declared actions (own + inherited), keyed by [name, method].
|
|
51
|
+
def actions
|
|
52
|
+
if superclass.respond_to?(:actions)
|
|
53
|
+
superclass.actions.merge(own_actions)
|
|
54
|
+
else
|
|
55
|
+
own_actions.dup
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Look up an action by name (for Context interception).
|
|
60
|
+
def action_for(name)
|
|
61
|
+
actions.values.find { |a| a.name == name }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def own_actions
|
|
67
|
+
@own_actions ||= {}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare wire-state attributes.
|
|
6
|
+
# Provides the `attribute` class DSL and `attrs` instance reader.
|
|
7
|
+
# Used by Component (for partial route params) and Page (for page route params).
|
|
8
|
+
module Attributes
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
# Declare a wire attribute. Accepts optional default: and type: kwargs.
|
|
15
|
+
# attribute :status, default: "active", type: :string
|
|
16
|
+
def attribute(name, default: nil, **options)
|
|
17
|
+
meta = { default: default }
|
|
18
|
+
meta[:type] = options[:type] if options.key?(:type)
|
|
19
|
+
own_attributes[name] = meta
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns all declared attributes (own + inherited), preserving declaration order.
|
|
23
|
+
def attributes
|
|
24
|
+
if superclass.respond_to?(:attributes)
|
|
25
|
+
superclass.attributes.merge(own_attributes)
|
|
26
|
+
else
|
|
27
|
+
own_attributes.dup
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def own_attributes
|
|
34
|
+
@own_attributes ||= {}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Instance access to resolved wire attribute values.
|
|
39
|
+
# Returns a Weft::Attributes object with method-style and hash access.
|
|
40
|
+
attr_reader :attrs
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for the `adds_children_to :@ivar` macro — generates the standard
|
|
6
|
+
# Arbre container override that redirects user-block children into a
|
|
7
|
+
# nested element while letting structural children (added during `build`)
|
|
8
|
+
# pass through to the wrapper.
|
|
9
|
+
#
|
|
10
|
+
# Mixed into both Weft::Component and Weft::Page by default. Usable on
|
|
11
|
+
# any class that inherits from Arbre::Component.
|
|
12
|
+
#
|
|
13
|
+
# class Card < Weft::Component
|
|
14
|
+
# adds_children_to :@body
|
|
15
|
+
#
|
|
16
|
+
# def build(attributes = {})
|
|
17
|
+
# super
|
|
18
|
+
# h2 "Header" # structural — goes to wrapper
|
|
19
|
+
# @body = div(class: "card-body")
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# The `:@body` form (with the leading `@`) is required — a pedagogical
|
|
24
|
+
# cue that the macro depends on the user assigning the ivar somewhere
|
|
25
|
+
# in `build`. If `build` returns without setting the ivar and a child
|
|
26
|
+
# is then added, the macro raises with a pointed error message.
|
|
27
|
+
module Containers
|
|
28
|
+
def self.included(base)
|
|
29
|
+
base.extend(ClassMethods)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
# Declare that block-style children should be added to the named
|
|
34
|
+
# instance variable instead of the wrapper element itself.
|
|
35
|
+
def adds_children_to(ivar)
|
|
36
|
+
unless ivar.is_a?(Symbol)
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"adds_children_to expects a Symbol (e.g., :@body), got #{ivar.inspect}"
|
|
39
|
+
end
|
|
40
|
+
unless ivar.to_s.start_with?("@")
|
|
41
|
+
raise Weft::InvalidDefinition,
|
|
42
|
+
"adds_children_to expects a Symbol that must start with @ (e.g., :@body), got #{ivar.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
prepend(Containers.behavior_for(ivar))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build the prepended Module that implements both the `build` wrap
|
|
50
|
+
# (to detect "build returned without assigning the ivar") and the
|
|
51
|
+
# `add_child` redirect. Each call returns a fresh anonymous module
|
|
52
|
+
# — declaring the macro on multiple classes is safe.
|
|
53
|
+
def self.behavior_for(ivar)
|
|
54
|
+
Module.new do
|
|
55
|
+
define_method(:build) do |*args, **kwargs|
|
|
56
|
+
super(*args, **kwargs)
|
|
57
|
+
@_weft_container_built = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
define_method(:add_child) do |child|
|
|
61
|
+
target = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
|
|
62
|
+
if target
|
|
63
|
+
target << child
|
|
64
|
+
elsif @_weft_container_built
|
|
65
|
+
raise Weft::MissingContainerIvar,
|
|
66
|
+
"#{self.class} declared `adds_children_to #{ivar.inspect}` " \
|
|
67
|
+
"but never assigned #{ivar} in build " \
|
|
68
|
+
"(e.g., #{ivar} = div(class: 'body') inside build)"
|
|
69
|
+
else
|
|
70
|
+
super(child)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare OOB-swapped sibling components.
|
|
6
|
+
# Included into Weft::Component.
|
|
7
|
+
#
|
|
8
|
+
# See Weft::Component#includes for the DSL surface.
|
|
9
|
+
module Inclusions
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
# Declare that another component should be OOB-swapped alongside
|
|
16
|
+
# this component's action responses and SSE pushes.
|
|
17
|
+
#
|
|
18
|
+
# includes OrderHeader # pass-through attrs
|
|
19
|
+
# includes OrderHeader, on: :advance # only on :advance action
|
|
20
|
+
# includes OrderHeader do |attrs| # explicit attr mapping
|
|
21
|
+
# { order_id: attrs.order_id, compact: true }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Without a block, the included component resolves from the same
|
|
25
|
+
# request params as the primary component. With a block, the block
|
|
26
|
+
# receives the primary component's resolved attrs and returns wire
|
|
27
|
+
# attrs for the included component's Resolver.
|
|
28
|
+
def includes(component_class, on: nil, &block)
|
|
29
|
+
own_inclusions << { component_class: component_class, on: on, block: block }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# All declared inclusions (own + inherited).
|
|
33
|
+
def inclusions
|
|
34
|
+
if superclass.respond_to?(:inclusions)
|
|
35
|
+
superclass.inclusions + own_inclusions
|
|
36
|
+
else
|
|
37
|
+
own_inclusions.dup
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def own_inclusions
|
|
44
|
+
@own_inclusions ||= []
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare recovery edges via `recovers`.
|
|
6
|
+
# Used by both Component (action and partial-render failures) and Page
|
|
7
|
+
# (full-document render failures, routing misses).
|
|
8
|
+
#
|
|
9
|
+
# See Weft::Component#recovers for the DSL surface.
|
|
10
|
+
module Recoveries
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.extend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Declare a recovery edge: how this class handles a specific error.
|
|
17
|
+
#
|
|
18
|
+
# recovers from: Weft::Unprocessable do |attrs, error|
|
|
19
|
+
# { error_messages: error.messages }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# recovers from: Weft::Unauthorized, with: LoginPage
|
|
23
|
+
#
|
|
24
|
+
# `from:` accepts a Class (subclass-inclusive), Integer (matched against
|
|
25
|
+
# HTTPError#status), Range, or Array of any of the above.
|
|
26
|
+
# `with:` accepts a Class (Page or Component) or Symbol (resolved against
|
|
27
|
+
# Weft.configuration at error-handling time). Default: self.
|
|
28
|
+
# The optional block receives `|attrs, error|` and returns a hash of
|
|
29
|
+
# additional attrs that merge with the original on the recovery edge.
|
|
30
|
+
# Symmetric with performs/transfers contracts (attrs first; error is the
|
|
31
|
+
# recovery-specific extra). The block never returns HTML.
|
|
32
|
+
def recovers(from:, with: nil, &block)
|
|
33
|
+
own_recoveries << { from: from, with: with, block: block }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# All declared recovery entries (own + inherited), in resolution order.
|
|
37
|
+
# Own entries precede inherited entries so subclass declarations take
|
|
38
|
+
# precedence over ancestor declarations. Within a class, declaration
|
|
39
|
+
# order is preserved — first-declared wins on ties.
|
|
40
|
+
def recoveries
|
|
41
|
+
if superclass.respond_to?(:recoveries)
|
|
42
|
+
own_recoveries + superclass.recoveries
|
|
43
|
+
else
|
|
44
|
+
own_recoveries.dup
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Find the first recovery entry whose `from:` matches the given exception.
|
|
49
|
+
# Returns nil if nothing matches. `from:` accepts Class (subclass-inclusive),
|
|
50
|
+
# Integer (status equality — HTTPError carries .status; non-HTTPError = 500),
|
|
51
|
+
# Range (status in range), or Array of any of the above (any element matches).
|
|
52
|
+
def recovery_for(exception)
|
|
53
|
+
recoveries.find { |entry| recovery_matches?(entry[:from], exception) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolve the recovery entry's `with:` value to a concrete target class.
|
|
57
|
+
# Symbol values look up `Weft.configuration.<sym>` (resolved at error-handling
|
|
58
|
+
# time so config reassignment propagates). Nil falls back to self.
|
|
59
|
+
def resolve_recovery_target(entry)
|
|
60
|
+
case entry[:with]
|
|
61
|
+
when Symbol then Weft.configuration.public_send(entry[:with])
|
|
62
|
+
when nil then self
|
|
63
|
+
else entry[:with]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def own_recoveries
|
|
70
|
+
@own_recoveries ||= []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def recovery_matches?(from_clause, exception)
|
|
74
|
+
case from_clause
|
|
75
|
+
when Array then from_clause.any? { |f| recovery_matches?(f, exception) }
|
|
76
|
+
when Class then exception.is_a?(from_clause)
|
|
77
|
+
when Integer then recovery_status_of(exception) == from_clause
|
|
78
|
+
when Range then from_clause.cover?(recovery_status_of(exception))
|
|
79
|
+
else false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def recovery_status_of(exception)
|
|
84
|
+
exception.is_a?(Weft::HTTPError) ? exception.status : 500
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare HX-Trigger response events.
|
|
6
|
+
# Included into Weft::Component.
|
|
7
|
+
#
|
|
8
|
+
# See Weft::Component#triggers for the DSL surface.
|
|
9
|
+
module Triggers
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
# Declare an event to trigger on all action responses for this component.
|
|
16
|
+
# Sets the HX-Trigger response header.
|
|
17
|
+
#
|
|
18
|
+
# triggers "order-updated"
|
|
19
|
+
def triggers(event_name)
|
|
20
|
+
own_triggers << event_name.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# All declared trigger events (own + inherited).
|
|
24
|
+
def trigger_events
|
|
25
|
+
if superclass.respond_to?(:trigger_events)
|
|
26
|
+
superclass.trigger_events | own_triggers
|
|
27
|
+
else
|
|
28
|
+
own_triggers.dup
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def own_triggers
|
|
35
|
+
@own_triggers ||= []
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
module DSL
|
|
5
|
+
# Mixin for classes that declare live-update behavior — polling and
|
|
6
|
+
# event-driven refreshes (`refreshes`) and SSE push streams (`pushes`).
|
|
7
|
+
# Included into Weft::Component.
|
|
8
|
+
module Updates
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
# Declare that this component refreshes automatically.
|
|
15
|
+
#
|
|
16
|
+
# refreshes every: 10.seconds # polling
|
|
17
|
+
# refreshes on: "delivery-completed" # event-driven
|
|
18
|
+
# refreshes every: 10, on: "updated" # both
|
|
19
|
+
#
|
|
20
|
+
# Generates hx-get, hx-trigger, hx-swap on the wrapper element.
|
|
21
|
+
# Multiple calls accumulate into a single hx-trigger value.
|
|
22
|
+
# Fractional intervals render in htmx's millisecond syntax.
|
|
23
|
+
def refreshes(every: nil, on: nil)
|
|
24
|
+
if every
|
|
25
|
+
ms = interval_in_ms(every, :refreshes)
|
|
26
|
+
own_refresh_triggers << ((ms % 1000).zero? ? "every #{ms / 1000}s" : "every #{ms}ms")
|
|
27
|
+
end
|
|
28
|
+
own_refresh_triggers << "#{on} from:body" if on
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Declare that this component pushes updates via SSE.
|
|
32
|
+
#
|
|
33
|
+
# pushes every: 5.seconds # server pushes on interval
|
|
34
|
+
#
|
|
35
|
+
# Generates hx-ext="sse", sse-connect, sse-swap on the wrapper element.
|
|
36
|
+
# The Router auto-generates a streaming endpoint at
|
|
37
|
+
# /component_path/_stream (the suffix is the stream_suffix config knob).
|
|
38
|
+
#
|
|
39
|
+
# Future: pushes on: "event-name" for event-driven server push (v1.0).
|
|
40
|
+
def pushes(every: nil)
|
|
41
|
+
@push_config = {}
|
|
42
|
+
return unless every
|
|
43
|
+
|
|
44
|
+
ms = interval_in_ms(every, :pushes)
|
|
45
|
+
@push_config[:every] = (ms % 1000).zero? ? ms / 1000 : ms / 1000.0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Push configuration (own or inherited). Returns nil if no pushes declared.
|
|
49
|
+
def push_config
|
|
50
|
+
if instance_variable_defined?(:@push_config)
|
|
51
|
+
@push_config
|
|
52
|
+
elsif superclass.respond_to?(:push_config)
|
|
53
|
+
superclass.push_config
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# All declared refresh triggers (own + inherited).
|
|
58
|
+
def refresh_triggers
|
|
59
|
+
if superclass.respond_to?(:refresh_triggers)
|
|
60
|
+
superclass.refresh_triggers | own_refresh_triggers
|
|
61
|
+
else
|
|
62
|
+
own_refresh_triggers.dup
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def own_refresh_triggers
|
|
69
|
+
@own_refresh_triggers ||= []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# htmx's smallest expressible interval is 1ms; rather than emit an
|
|
73
|
+
# "every 0s" that polls flat-out, round up and say so.
|
|
74
|
+
def interval_in_ms(every, verb)
|
|
75
|
+
ms = (every.to_f * 1000).round
|
|
76
|
+
return ms if ms.positive?
|
|
77
|
+
|
|
78
|
+
Weft.logger.warn(
|
|
79
|
+
"#{name} declares `#{verb} every: #{every.inspect}`, below the 1ms floor; using 1ms"
|
|
80
|
+
)
|
|
81
|
+
1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/weft/error.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Weft
|
|
4
|
+
# Abstract base — never raise directly; use a semantic subclass.
|
|
5
|
+
# Exists for `rescue Weft::Error` to catch the whole gem-error family.
|
|
6
|
+
Error = Class.new(StandardError)
|
|
7
|
+
|
|
8
|
+
# Intermediate carrying an HTTP status. Subclass for status-bearing semantics.
|
|
9
|
+
# `recovers from: Weft::HTTPError` catches the whole status-bearing family.
|
|
10
|
+
class HTTPError < Error
|
|
11
|
+
def self.status
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def status
|
|
16
|
+
self.class.status
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class NotFound < HTTPError
|
|
21
|
+
def self.status = 404
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Unauthorized < HTTPError
|
|
25
|
+
def self.status = 401
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Forbidden < HTTPError
|
|
29
|
+
def self.status = 403
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Unprocessable < HTTPError
|
|
33
|
+
def self.status = 422
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class InternalError < HTTPError
|
|
37
|
+
def self.status = 500
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Below this comment, maintain semantically-named errors (not ending in "Error") in alphabetical order, e.g.:
|
|
41
|
+
#
|
|
42
|
+
# OrderAlreadyFulfilled = Class.new(Error)
|
|
43
|
+
# InsufficientFunds = Class.new(Error)
|
|
44
|
+
|
|
45
|
+
# Raised for semantic mistakes inside `Weft.configure { |c| ... }` — values
|
|
46
|
+
# of the right kind that nonetheless violate a constraint (e.g. an asset
|
|
47
|
+
# root not starting with `/`), or state conflicts (duplicate asset bundles).
|
|
48
|
+
InvalidConfiguration = Class.new(Error)
|
|
49
|
+
|
|
50
|
+
# Raised for semantic mistakes in class-body DSL declarations — e.g. a page
|
|
51
|
+
# declares attributes but no `page_path`, or `adds_children_to` receives a
|
|
52
|
+
# Symbol that does not start with `@`.
|
|
53
|
+
InvalidDefinition = Class.new(Error)
|
|
54
|
+
|
|
55
|
+
# Raised for semantic mistakes at render / action time — invalid input
|
|
56
|
+
# combinations or references to state that isn't there (e.g. an unknown
|
|
57
|
+
# assets bundle named at `register_stylesheet`).
|
|
58
|
+
InvalidUsage = Class.new(Error)
|
|
59
|
+
|
|
60
|
+
# Raised by the `adds_children_to :@ivar` macro when build returns without
|
|
61
|
+
# ever assigning the named ivar and then a child is added — almost always
|
|
62
|
+
# means the developer declared the macro but forgot the matching assignment.
|
|
63
|
+
MissingContainerIvar = Class.new(InvalidDefinition)
|
|
64
|
+
end
|