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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9159a7fb47cb6e20e124522b7c3d4d704678dc5c39464039d2e5d0e06243ecd9
|
|
4
|
+
data.tar.gz: eb64c59d4c7468349b6dfb3b2eaf081e71b2412d27b4504adb3b2991d7bca603
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f4a553ba0e98279dd39d704b8b30b21d0d4e4dd6d5cf58f6eb89d79cd0825217bb0ae5ee141910d664c609e5c9560d04de2d346c73444c73a47d4e412962ae8
|
|
7
|
+
data.tar.gz: f336d96dd78e65e14058c934a879f57c85278e0665267d5ff3c854d52bc8ce86af915fb49b4f11333a7e40e032458d3b80023bdf8f5c680a9a4e9d49e8bb789a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,37 +1,143 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.0 (2026-08-11)
|
|
4
|
+
|
|
5
|
+
Weft's inputs model grows up: four declared ways to get a component what it needs, values that flow down the render tree, and one universe of state per request. Also typed wire params, one-call app loading, self-healing streams, and 404s you can brand.
|
|
6
|
+
|
|
7
|
+
### New Features:
|
|
8
|
+
|
|
9
|
+
- **Four Doors Into `params`** – Declare what a component needs; read it back the same way whatever the source. Full model in [the DSL reference](docs/dsl.md#params).
|
|
10
|
+
- `param` – wire state from the query string, path, or body
|
|
11
|
+
- `receives` – rich objects the call site hands over; required unless you declare a default
|
|
12
|
+
- `derives` – a block that runs at most once per render, and only if something reads it
|
|
13
|
+
- `defines` – static values a subclass pins; sugar over `derives`
|
|
14
|
+
- Two doors on one key resolve either way: handed over when embedded, self-fetching when standalone
|
|
15
|
+
- `dependent!` marks a component that only makes sense inside a parent
|
|
16
|
+
|
|
17
|
+
- **Typed Wire Params** (`type:`) – `param :page, type: :integer` reads `?page=2` as an Integer, with `:string`, `:float`, `:boolean`, and `:decimal` completing the vocabulary. Bad declarations fail at class-load time, not mid-request.
|
|
18
|
+
|
|
19
|
+
- **Ancestor Navigation** (`closest` / `enclosing`) – A nested component finds an ancestor and reads its identity, so it can aim at that ancestor instead of being hand-fed a target at every call site. Bang variants raise `Weft::AncestorNotFound`.
|
|
20
|
+
|
|
21
|
+
- **One-Call App Loading** (`Weft.configure_autoloading`) – Point Weft at your app directories and [Zeitwerk](https://github.com/fxn/zeitwerk) takes over; every component and page is registered and routable before the first request.
|
|
22
|
+
- `reload: true` applies edits, new files, and deletions on the next request, route registry in sync
|
|
23
|
+
- `Weft.registry.evict` and `Weft.configuration.refresh_stale_classes!` are public, for hand-rolled reloaders
|
|
24
|
+
|
|
25
|
+
- **Self-Healing Live Streams** – `recovers` now protects SSE pushes like any other render, and a stream that keeps failing no longer errors forever.
|
|
26
|
+
- Error budget set gem-wide or per-component
|
|
27
|
+
- Recovery components can declare `param :attempts_remaining` to tell "still retrying" apart from "gave up"
|
|
28
|
+
- A new `reopen_stream:` preset, providing one-click resume for a failed stream
|
|
29
|
+
- `pushes immediate: false` holds the first frame one interval, for snapshots that only mean something after a push cycle
|
|
30
|
+
|
|
31
|
+
- **Branded Not-Found Pages** – `recovers from: Weft::NotFound, with: YourNotFoundPage` now works the way it was always documented, on every 404 pathway; mounted as middleware, a downstream app's own 404s pass through untouched.
|
|
32
|
+
- **Map your own errors** (`status:`) – `recovers from: ActiveRecord::RecordNotFound, with: NotFoundPage, status: 404` gives your exceptions honest wire semantics
|
|
33
|
+
|
|
34
|
+
- **One Universe Per Request** – A response resolves against the request's own wire params end to end — action re-render, `transfers` hand-off, or recovery — so a nested component keeps reading what it declares with no encloser relaying it. Hashes returned from verb blocks overlay that universe for everything the response renders.
|
|
35
|
+
|
|
36
|
+
- **Actions That Read What Your Component Knows** – Every verb block now sees the same `params` that `build` does, so a lookup a component already declares isn't written again inside each action. (`receives` stays out: a request over the wire has no call site.)
|
|
37
|
+
- One query per response — a record the callable loads is already loaded for the re-render, its companions, and any transfer target
|
|
38
|
+
- A `recovers` block is handed the state the request had reached when it broke
|
|
39
|
+
- Two one-time warnings name a `derives` block that can never run
|
|
40
|
+
|
|
41
|
+
- **Companions Ride Along Smarter** (`includes`) – A companion renders as an out-of-band child of the component it accompanies, inheriting its params, with its block returning a delta for that companion alone.
|
|
42
|
+
- `on:` takes arrays, and matches your own `transfers` actions
|
|
43
|
+
- `when: :transferred` scopes a companion to transfer arrivals; declare both for a union
|
|
44
|
+
- A companion that raises walks its **own** `recovers` chain into its **own** slot while the primary's render, status, and announcement stand — streams included, and it never spends their attempts budget
|
|
45
|
+
|
|
46
|
+
- **Announcements That Name Their Action** (`triggers ..., on:`) – Events are no longer welded to every action a component has, so subscribers stop refetching for an action that changed nothing. Arrays accepted.
|
|
47
|
+
|
|
48
|
+
- **Deletions Without Ceremony** (`dismisses`) – A successful delete-swap responds with an empty body, retiring the guard clause against the just-deleted record. Companions still ride along, and `dismisses` now takes `target:`.
|
|
49
|
+
- A recovery fragment adopts the failing component's wrapper tag, so a failed row delete yields an error `<tr>`, not a `<div>` wedged into a table
|
|
50
|
+
|
|
51
|
+
- **Call-Site Wiring Overrides** (`target:` / `swap:`) – Every kwarg that wires a request now honors per-call overrides: the declaration supplies the default, the call site gets the final word.
|
|
52
|
+
|
|
53
|
+
- **Confirmation Dialogs** (`confirm:`) – Shows the browser-native dialog and fires the request only on OK. Works alongside any interaction kwarg, or standalone on a container.
|
|
54
|
+
|
|
55
|
+
- **Loud Wiring Failures** – A kwarg that is unmistakably Weft's but can't resolve raises `Weft::InvalidUsage` instead of rendering as a junk HTML attribute you would notice only when clicking did nothing. A `nil` value still means "not this time."
|
|
56
|
+
|
|
57
|
+
- **Declarative Page Titles** (`title`) – `title "Orders"`, or `title { |params| "Order ##{params.order.number}" }` when the tab should name the record. Inherited down a page hierarchy.
|
|
58
|
+
|
|
59
|
+
- **Two More Semantic Triggers** – `:change` fires when a select or checkbox changes value; `:click_once` caps an interaction at a single firing, the right default for anything that inserts rather than replaces. `inline_expand:` now bakes it in.
|
|
60
|
+
|
|
61
|
+
- **Inline Head Scripts** (`register_inline_js`) – The JavaScript sibling of `register_inline_css`, landing after the registered external scripts.
|
|
62
|
+
|
|
63
|
+
### Breaking Changes:
|
|
64
|
+
|
|
65
|
+
- **Renames** – All mechanical; rename call sites.
|
|
66
|
+
- `attribute` → `param` and `attrs` → `params`, including every verb's block argument. Arbre's own HTML attributes are untouched
|
|
67
|
+
- `shorthand` → `preset`: `Weft::Shorthands` → `Weft::Presets`, `register_shorthand` → `register_preset`, `Weft.shorthand` → `Weft.preset`. The element kwargs are unchanged
|
|
68
|
+
- `Weft::Page.register_css` → `register_inline_css`
|
|
69
|
+
- `weft_id` → `weft_dom_id`, `weft_id_for` → `weft_dom_id_for`; derived values unchanged
|
|
70
|
+
- `Weft::Resolver#resolve` is now a class method, and `Weft::Params.extract_from` is removed
|
|
71
|
+
|
|
72
|
+
- **In-page param passing removed** – Components resolve their declared `param`s from the request at any nesting depth, so `orders_panel(status: params.status, page: params.page)` collapses to `orders_panel`. A builder kwarg naming a declared param now renders as a plain HTML attribute with a one-time warning; rich objects get `receives` instead.
|
|
73
|
+
- `params` resolves at construction, so a `build` body can read it before `super`
|
|
74
|
+
- `Weft::Context.new` accepts `wire_params:` for rendering outside the Router
|
|
75
|
+
- `Component.render` / `Page.render` kwargs are now exactly what a query string would carry
|
|
76
|
+
|
|
77
|
+
- **The params bag flows down the render tree** – Each component starts from a copy of its nearest ancestor's resolved params: everything above it, nothing beside it. Its own sources still win, in a fixed order ([precedence table](docs/dsl.md#how-the-doors-combine)), and only its own declared `param`s serialize.
|
|
78
|
+
|
|
79
|
+
- **Defaults belong to whoever declares them** – A `default:` no longer travels: a child, or a `transfers` target, falls back to its own rather than an ancestor's. Everything actually supplied still flows down. Declare the value where it's meant to come from.
|
|
80
|
+
|
|
81
|
+
- **Wire coercion follows `type:`, not the default** – An untyped param passes its wire value through as a string. Add `type:` wherever a default used to do the coercing — flag params especially, since `"false"` is truthy without `type: :boolean`.
|
|
82
|
+
|
|
83
|
+
- **Removed: `auto_reload` / `reload_paths`** – Use `Weft.configure_autoloading(reload: true)`, which reloads more and keeps the routing registry in sync. `sinatra-contrib` is dropped, `zeitwerk` added; hand-rolled reloaders now call `Weft.registry.evict` explicitly.
|
|
84
|
+
|
|
85
|
+
- **The element-kwarg surface fails loudly** – Miswired kwargs raise `Weft::InvalidUsage` where they used to fall through as HTML attributes, and the `loads:`/preset "requires `swap:`/`target:`" errors move there from `ArgumentError`. Follow the messages — each names the kwarg and the repair.
|
|
86
|
+
|
|
87
|
+
- **Non-routable load targets raise** – A `loads:`, preset, or `navigate:` aimed at a non-routable class now raises at render time instead of 404ing at click time with nothing in the logs. Mark purely presentational targets `routable!`.
|
|
88
|
+
|
|
89
|
+
- **DOM ids skip unusable suffixes** – The id suffix rides only for non-blank scalars, so `""`, `nil`, and non-scalar values all derive the bare class id (`member-roster-[]` used to break `querySelector`). `false` now suffixes like `true`. Update CSS or tests matching the old forms.
|
|
90
|
+
|
|
91
|
+
- **Destructive-swap responses are empty** – A successful delete-swap responds `200` with no body where it used to carry a render htmx discarded. Out-of-band fragments still arrive.
|
|
92
|
+
|
|
93
|
+
- **Page titles are declared, not extracted** – `Weft::Page` no longer reads `:title` from the build attributes; a page still setting it leaves the tab reading "Weft" and a stray `title="..."` on `<html>`. Use the class-body `title`.
|
|
94
|
+
|
|
95
|
+
- **`inline_expand:` fires once** – The default trigger is now `:click_once`, so a repeat click can't insert a second copy. Drop any hand-written `trigger: "click once"`; declare `trigger: :click` to keep re-triggering.
|
|
96
|
+
|
|
97
|
+
- **Transfers responses carry the target's companions** – The *rendered* component's `includes` fire, not the transferring one's, and `on:` matches only a component's own action names. Scope arrivals with `when: :transferred`.
|
|
98
|
+
|
|
99
|
+
- **A failed render walks the chain of whatever was rendering** – A `transfers` target that raises during its own `build` is handled by the target's `recovers` chain. Declare the edge on the target, or on a shared base class.
|
|
100
|
+
|
|
101
|
+
- **The declarer's schema stays its own** – A transfer or recovery target projects its own declared schema and its own defaults; what crosses over is the state the request composed, so nothing already loaded is fetched twice.
|
|
102
|
+
|
|
103
|
+
- **One DOM slot, one companion** – Two companions resolving to the same DOM id can't both land, so Weft keeps the first, warns naming both declaration sites, and never builds the loser. A component's first param now decides which companions can coexist, not only where each lands.
|
|
104
|
+
|
|
105
|
+
- **Inclusion blocks return deltas, not replacements** – The hash adjusts a companion's picture instead of defining its entire wire. Clear a key explicitly (`{ key: nil }`) where a block used to withhold it.
|
|
106
|
+
|
|
107
|
+
- **Recovery fragments always wear the failing component's id** – Weft stamps identity onto every recovery fragment, so a target lands correctly whether or not it knows about any of this. `:component_id` is gone from the auto-injected params (six remain); drop the declaration and any `weft_dom_id` override that read it.
|
|
108
|
+
|
|
3
109
|
## v0.1.0 (2026-07-12)
|
|
4
110
|
|
|
5
111
|
First usable release. Weft is component-oriented hypermedia for Ruby: components declare their structure, their data, and their interactive behaviors, and the framework derives the routing, request handling, and client-side wiring automatically.
|
|
6
112
|
|
|
7
113
|
### New Features:
|
|
8
114
|
|
|
9
|
-
- **Components and
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Pages carry the document shell
|
|
13
|
-
- **
|
|
14
|
-
- `performs :name` – user-initiated
|
|
15
|
-
- `transfers :name, to: Other` –
|
|
16
|
-
- `dismisses :name` –
|
|
17
|
-
- `refreshes every:` / `refreshes on:` – client re-fetches on a timer (whole or fractional seconds, down to a 1ms floor) or
|
|
18
|
-
- `pushes every:` – the server streams re-renders over SSE, with an immediate first frame for new
|
|
19
|
-
- `triggers "event"` – announce action
|
|
20
|
-
- `includes Other` – companion
|
|
21
|
-
- `recovers from:, with:` –
|
|
22
|
-
- **
|
|
23
|
-
-
|
|
24
|
-
- Routability inferred from
|
|
25
|
-
- SSE stream
|
|
26
|
-
- **
|
|
27
|
-
- **Element
|
|
28
|
-
- **Interaction
|
|
29
|
-
- **Error
|
|
30
|
-
- The `recovers` chain renders declared fallbacks with semantic status codes
|
|
31
|
-
- Recovery targets receive schema-gated context — `:exception`, `:request_path`, `:status_code`, `:component_id`, `:retry_url` — only where
|
|
32
|
-
- Brand the defaults app-wide via
|
|
33
|
-
- The
|
|
34
|
-
- **Configuration** – `Weft.configure` covers development reloading (`auto_reload`, `reload_paths`), logging (`Weft.logger`, stdout by default; `log_level`, `router_logging`), static asset bundles (`static_assets` with named bundles, path-containment checks, and `assets:` resolution on `register_stylesheet` / `register_script`), htmx delivery (`include_htmx`, `include_sse_ext`), and routing (`component_path`, `stream_suffix`)
|
|
35
|
-
- **
|
|
115
|
+
- **Components and Pages** (`Weft::Component`, `Weft::Page`) – Build HTML in Ruby with a component DSL over Arbre:
|
|
116
|
+
- Describe structure in a `build` block and declare a component's inputs with `attribute` — they arrive from request parameters, coerced to type and filled with your defaults, and reach your code as `attrs.whatever`
|
|
117
|
+
- Render a component as a standalone fragment or drop it into a page through a generated builder method (`builder_method :name`)
|
|
118
|
+
- Pages carry the whole document shell — title, stylesheets, scripts, inline CSS — inheritable down a page hierarchy, so a shared layout costs nothing at each page
|
|
119
|
+
- **Interactive Behaviors** – One-line declarations that wire up dynamic behavior, no routes or JavaScript written by hand:
|
|
120
|
+
- `performs :name` – a user-initiated action: run your callable, then re-render the component in place
|
|
121
|
+
- `transfers :name, to: Other` – an action whose response renders a *different* component where the caller was
|
|
122
|
+
- `dismisses :name` – an action that removes the component from the DOM
|
|
123
|
+
- `refreshes every:` / `refreshes on:` – the client re-fetches the component on a timer (whole or fractional seconds, down to a 1ms floor) or whenever a named page event fires
|
|
124
|
+
- `pushes every:` – the server streams re-renders over SSE, with an immediate first frame for every new subscriber
|
|
125
|
+
- `triggers "event"` – announce an action's result to the rest of the page for other components to react to
|
|
126
|
+
- `includes Other` – a companion component rides along out-of-band, updating a second region in the same response
|
|
127
|
+
- `recovers from:, with:` – declare per-class error behavior
|
|
128
|
+
- **Automatic Routing** – Every component and page gets a URL with no route table to maintain: components at `/_components/<name>`, pages at name-derived paths, the conventional class-name suffix stripped (`OrdersPanelComponent` and `OrdersPanel`, `DashboardPage` and `Dashboard`, all route without ceremony):
|
|
129
|
+
- Override explicitly with `self.page_path` and `self.component_path =`; tune the component prefix and stream suffix gem-wide
|
|
130
|
+
- Routability is inferred from what a class declares, with `abstract!` / `routable!` to force it either way
|
|
131
|
+
- Every pushing component gets its SSE stream endpoint generated automatically
|
|
132
|
+
- **Collision-Safe Routing** – If two routable classes would answer at the same URL, Weft raises `Weft::InvalidDefinition` naming both — on the first request, so you find out immediately. Code reloaders that redefine a class prune the stale registration automatically, and `Weft.registry.clear` gives reload integrations and tests a clean slate
|
|
133
|
+
- **Element-Level Wiring** – Attach behavior to any element at any nesting depth with `action:`, `loads:`, `trigger:`, `navigate:`, and `push_url:`, plus `target:` and `swap:` to refine where `loads:` and the shorthands land their response. Raw htmx attributes pass straight through, side by side with what the kwargs expand to
|
|
134
|
+
- **Interaction Shorthands** – Named one-word wirings over the `loads:` machinery, with the trigger and swap details baked in: `tooltip:`, `modal:`, `lazy:`, `load_more:`, `infinite_scroll:`, `live_search:`, `tabs:`, `inline_expand:`, and `retry:`. Register your own vocabulary with `Weft.register_shorthand`
|
|
135
|
+
- **Semantic Error Handling** – A full error family under `Weft::Error` (`InvalidConfiguration`, `InvalidDefinition`, `InvalidUsage`, and `HTTPError` classes like `Weft::NotFound` and `Weft::Unprocessable`), and a recovery system that renders the right fallback with the right status code:
|
|
136
|
+
- The `recovers` chain renders declared fallbacks with semantic status codes — a validation failure becomes a `422` whose body is the component wearing its error state
|
|
137
|
+
- Recovery targets receive schema-gated context — `:exception`, `:request_path`, `:status_code`, `:component_id`, `:retry_url` — only where they declare it
|
|
138
|
+
- Brand the defaults app-wide via `error_component` / `error_page` / `not_found_page` / `not_found_component`, or override per class with explicit `recovers` declarations
|
|
139
|
+
- The built-in error components offer one-click retry through the `retry:` shorthand
|
|
140
|
+
- **Configuration** – `Weft.configure` covers the operational surface: development reloading (`auto_reload`, `reload_paths`), logging (`Weft.logger`, stdout by default; `log_level`, `router_logging`), static asset bundles (`static_assets` with named bundles, path-containment checks, and `assets:` resolution on `register_stylesheet` / `register_script`), htmx delivery (`include_htmx`, `include_sse_ext`), and routing (`component_path`, `stream_suffix`)
|
|
141
|
+
- **Secure Script Delivery** – The htmx core and SSE-extension scripts Weft serves are subresource-integrity pinned out of the box, and `register_script` forwards `integrity:` / `crossorigin:` (and any other attributes) to the tag for your own CDN scripts
|
|
36
142
|
- **Documentation** – A complete set under `docs/`: a build-your-first-app tutorial; references for the DSL, routing, error handling, configuration, and the Arbre HTML layer; an application-patterns guide (service objects, databases, background jobs, authentication, CSRF, testing); and a twenty-one-page examples catalog with captured wire traffic that deliberately covers the ground of htmx's own examples
|
|
37
|
-
- **Demo
|
|
143
|
+
- **Demo Application** – A complete Sinatra + Weft application under `demo/`, exercising the feature surface end to end
|
data/README.md
CHANGED
|
@@ -6,28 +6,48 @@ Weft lets you write your application in terms of its interface: components decla
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
class DeliveryStatus < Weft::Component
|
|
9
|
-
|
|
9
|
+
param :delivery_id, type: :integer
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
derives(:delivery) { |params| Delivery.find(params.delivery_id) }
|
|
12
12
|
|
|
13
|
-
performs
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
13
|
+
performs(:cancel) { |params| CancelDelivery.call(params.delivery) }
|
|
14
|
+
|
|
15
|
+
refreshes every: 5.seconds
|
|
17
16
|
|
|
18
17
|
def build(attributes = {})
|
|
19
18
|
super
|
|
20
|
-
delivery
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
add_class "delivery-status"
|
|
20
|
+
|
|
21
|
+
span "Delivery ##{params.delivery_id}"
|
|
22
|
+
div(class: "delivery-detail") do
|
|
23
|
+
progress_bar value: params.delivery.progress_percent, max: 100
|
|
24
|
+
span "Arriving #{params.delivery.eta}"
|
|
25
|
+
button "Cancel", action: :cancel if params.delivery.cancellable?
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
That's a complete, interactive UI component.
|
|
31
|
+
That's a complete, interactive UI component. The cancel button invokes a service and re-renders the result; the card polls for fresh state every 5 seconds. There's no routes file, no controller, no custom JavaScript — just Ruby describing what the UI is and what it does. The UI is the source of truth; the plumbing is implied.
|
|
32
|
+
|
|
33
|
+
Here is everything that renders — htmx wiring and all:
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<div id="delivery-status-4471" hx-get="/_components/delivery_status?delivery_id=4471"
|
|
37
|
+
hx-trigger="every 5s" hx-swap="outerHTML" class="delivery-status">
|
|
38
|
+
<span>Delivery #4471</span>
|
|
39
|
+
<div class="delivery-detail">
|
|
40
|
+
<div id="progress-bar" class="progress">
|
|
41
|
+
<div class="progress-fill" style="width: 62%"></div>
|
|
42
|
+
</div>
|
|
43
|
+
<span>Arriving today, 4:15 PM</span>
|
|
44
|
+
<button hx-post="/_components/delivery_status/cancel" hx-target="#delivery-status-4471"
|
|
45
|
+
hx-swap="outerHTML" hx-vals="{"delivery_id":4471}">Cancel</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every attribute above was derived from those four declarations: the routes (`GET /_components/delivery_status` for the component, `POST /_components/delivery_status/cancel` for the action), the DOM id that keeps this delivery individually addressable, the polling on the wrapper, and the button's whole request — where the response lands, how it swaps, and which params ride along with it. `progress_bar` is a child component with declarations of its own, rendered inline, wrapper and all.
|
|
31
51
|
|
|
32
52
|
Weft is built on [Arbre](https://github.com/activeadmin/arbre) for HTML generation and [htmx](https://htmx.org) for hypermedia interactions. It runs standalone as a lightweight Sinatra-backed server, or mounts as middleware inside any existing Rack app. No build step, no npm, no hydration — just Ruby, HTML, and HTTP.
|
|
33
53
|
|
|
@@ -47,13 +67,14 @@ Components declare their dynamic behaviors with verbs:
|
|
|
47
67
|
| `includes Other` | Companion components ride along in action responses, out-of-band |
|
|
48
68
|
| `recovers from: Err, with: Fallback` | Declares what renders when something raises |
|
|
49
69
|
|
|
50
|
-
Elements get their own vocabulary — `action:`, `loads:`, `trigger:` kwargs and interaction
|
|
70
|
+
Elements get their own vocabulary — `action:`, `loads:`, `trigger:` kwargs and interaction presets like `tooltip:`, `modal:`, `lazy:`, `infinite_scroll:` — all covered in [the DSL reference](docs/dsl.md).
|
|
51
71
|
|
|
52
72
|
## Documentation
|
|
53
73
|
|
|
54
74
|
- **[Build your first Weft app](docs/tutorial.md)** — the tutorial: empty directory to a working app with pages, components, a validated form action, and live updates.
|
|
55
75
|
- **[Examples](docs/examples/README.md)** — twenty-one worked patterns with captured wire traffic. Coming from htmx? This catalog deliberately covers the ground of htmx's own examples.
|
|
56
|
-
- **[The Weft DSL](docs/dsl.md)** — every verb, element kwarg, and interaction
|
|
76
|
+
- **[The Weft DSL](docs/dsl.md)** — every verb, element kwarg, and interaction preset.
|
|
77
|
+
- **[How params flow](docs/params.md)** — the data lifecycle: a request comes in, each component pulls what it needs through `param`/`receives`/`derives`/`defines`, and renders with enough of its own wire state to refresh or act on its own.
|
|
57
78
|
- **[Application patterns](docs/app-patterns.md)** — the app around the components: service objects, databases, background jobs, authentication, CSRF, assets, and testing.
|
|
58
79
|
- **[Arbre: the HTML layer](docs/arbre.md)** — the HTML builder inside every `build` method, in depth.
|
|
59
80
|
- **[Routing](docs/routing.md)** — how classes become URLs, what's routable, and collision detection.
|
|
@@ -64,8 +85,9 @@ Elements get their own vocabulary — `action:`, `loads:`, `trigger:` kwargs and
|
|
|
64
85
|
|
|
65
86
|
| Version | Features | Status |
|
|
66
87
|
|---------|---------|--------|
|
|
67
|
-
| v0.1.0 | First usable release: the verb DSL, auto-routing with collision detection, interaction
|
|
68
|
-
| v0.2 |
|
|
88
|
+
| v0.1.0 | First usable release: the verb DSL, auto-routing with collision detection, interaction presets, SSE, error recovery, full documentation set | Shipped |
|
|
89
|
+
| v0.2.0 | The inputs model: four declared doors into `params`, typed wire params, values flowing down the render tree, one-call app loading, self-healing streams, brandable 404s | **Current** |
|
|
90
|
+
| v0.3 | The request–response lifecycle: how a request is addressed, carried, observed, and composed into a response | Next |
|
|
69
91
|
|
|
70
92
|
## Installation
|
|
71
93
|
|
|
@@ -113,16 +135,17 @@ run MyApp
|
|
|
113
135
|
|
|
114
136
|
### Configuration
|
|
115
137
|
|
|
116
|
-
`Weft.
|
|
138
|
+
The call you'll want on day one is `Weft.configure_autoloading` — it puts Zeitwerk in charge of loading your app's directories, and with `reload: true` your edits (new files and deletions included) apply without restarting the server:
|
|
117
139
|
|
|
118
140
|
```ruby
|
|
119
|
-
Weft.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
Weft.configure_autoloading(
|
|
142
|
+
paths: [File.expand_path("app/components", __dir__),
|
|
143
|
+
File.expand_path("app/pages", __dir__)],
|
|
144
|
+
reload: ENV.fetch("RACK_ENV", "production") == "development"
|
|
145
|
+
)
|
|
123
146
|
```
|
|
124
147
|
|
|
125
|
-
|
|
148
|
+
Gem-level settings live on its sibling, `Weft.configure` — static asset bundles, error presentation, routing overrides, logging — all in [the configuration reference](docs/configuration.md).
|
|
126
149
|
|
|
127
150
|
### Customizing error and not-found pages
|
|
128
151
|
|
|
@@ -139,7 +162,7 @@ Per-class `recovers` declarations override the app-wide fallbacks where you need
|
|
|
139
162
|
|
|
140
163
|
## Contributing
|
|
141
164
|
|
|
142
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/rusterholz/weft. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
165
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rusterholz/weft. The [development guide](docs/development.md) covers setup, the test suites, and the release process. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
143
166
|
|
|
144
167
|
## License
|
|
145
168
|
|
data/docs/app-patterns.md
CHANGED
|
@@ -35,7 +35,7 @@ The ordering carries the same rule the tutorial explains: files that *define* th
|
|
|
35
35
|
|
|
36
36
|
Components own what the user **sees and does** — the markup, the affordances, and the immediate response to an action. They should not own your business rules.
|
|
37
37
|
|
|
38
|
-
The dividing line runs through the action callable. A callable is a translation layer:
|
|
38
|
+
The dividing line runs through the action callable. A callable is a translation layer: params in, one operation invoked, a hash out for the re-render. The moment the middle step grows past a few lines — multiple records, a transaction, an email, a job — it belongs in a plain Ruby service object:
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
41
|
# app/services/comment_poster.rb
|
|
@@ -53,16 +53,17 @@ end
|
|
|
53
53
|
|
|
54
54
|
```ruby
|
|
55
55
|
# in the component
|
|
56
|
-
performs :post do |
|
|
57
|
-
CommentPoster.call(author:
|
|
56
|
+
performs :post do |params|
|
|
57
|
+
CommentPoster.call(author: params.author, body: params.body)
|
|
58
58
|
{ author: nil, body: nil }
|
|
59
59
|
end
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
The service knows nothing about components or HTML; the component knows nothing about how a post actually happens. Two details worth noticing:
|
|
63
63
|
|
|
64
|
-
- **End the callable with the hash you mean.** A callable's return value merges into the
|
|
65
|
-
- **The service returns plain Ruby values** (`:posted`, `:blank`, a record, a result object — whatever fits). When the component needs to branch on the outcome, branch in the callable and translate to
|
|
64
|
+
- **End the callable with the hash you mean.** A callable's return value merges into the params for the re-render *if it's a hash* — anything else is discarded (see [the callable contract](dsl.md#the-callable-contract)). Delegating to a service and then returning your own hash, as above, keeps the wire state deliberate even when the service's return value changes.
|
|
65
|
+
- **The service returns plain Ruby values** (`:posted`, `:blank`, a record, a result object — whatever fits). When the component needs to branch on the outcome, branch in the callable and translate to params; the service still shouldn't know what a DOM id is.
|
|
66
|
+
- **Feed the service from your declarations, not a second lookup.** A callable reads the same `derives` its `build` does, so `CommentPoster.call(post: params.post)` uses the post the component already knows how to find — one declaration, and one query for the whole response. Writing `Post.find(params.post_id)` inside the callable duplicates a rule that will drift.
|
|
66
67
|
|
|
67
68
|
## Databases
|
|
68
69
|
|
|
@@ -76,7 +77,7 @@ Weft doesn't run jobs, but it has a natural shape for showing their progress: **
|
|
|
76
77
|
|
|
77
78
|
The pattern in three steps:
|
|
78
79
|
|
|
79
|
-
1. **The action dispatches and returns.** A user action that starts long work shouldn't wait for it — the callable enqueues the job (Sidekiq, SolidQueue, GoodJob, a `Thread` in development — Weft doesn't care) and returns immediately with whatever
|
|
80
|
+
1. **The action dispatches and returns.** A user action that starts long work shouldn't wait for it — the callable enqueues the job (Sidekiq, SolidQueue, GoodJob, a `Thread` in development — Weft doesn't care) and returns immediately with whatever params render the "started" state.
|
|
80
81
|
2. **The job writes progress to the shared store** as it works: a status column, a percentage, a result row. The job knows nothing about components.
|
|
81
82
|
3. **The component re-renders on a cadence** — [`refreshes every:`](dsl.md#refreshes--the-client-re-fetches) polls, [`pushes every:`](dsl.md#pushes--the-server-sends-updates) streams over SSE — and each render just reads the store. Completion isn't an event to handle; it's data the next render picks up.
|
|
82
83
|
|
|
@@ -86,7 +87,7 @@ The [Progress Bar](examples/progress-bar.md) example is this exact lifecycle, ve
|
|
|
86
87
|
|
|
87
88
|
Weft is deliberately session-agnostic: no cookie handling, no `current_user`, no login machinery. Identity is your app's concern, handled with standard Rack pieces in front of the Router. What Weft *does* define is the seam, and it's narrower than you might expect:
|
|
88
89
|
|
|
89
|
-
> Components and callables receive exactly their **resolved
|
|
90
|
+
> Components and callables receive exactly their **resolved params** — values from request parameters, filtered through each component's declared schema. Session state and request headers are not part of that channel.
|
|
90
91
|
|
|
91
92
|
So per-request identity needs its own channel. The pattern that fits — the same one Rails blesses as `Current` — is a [`CurrentAttributes`](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html) object set by middleware. Weft already depends on ActiveSupport, so it's available without adding anything:
|
|
92
93
|
|
data/docs/arbre.md
CHANGED
|
@@ -12,6 +12,7 @@ Arbre's own upstream documentation is famously thin, so this document doesn't as
|
|
|
12
12
|
- [Inside `build`: the component contract](#inside-build-the-component-contract)
|
|
13
13
|
- [Receiving caller content](#receiving-caller-content)
|
|
14
14
|
- [Working with the tree](#working-with-the-tree)
|
|
15
|
+
- [Reaching enclosing components](#reaching-enclosing-components)
|
|
15
16
|
- [Forms](#forms)
|
|
16
17
|
- [Testing components](#testing-components)
|
|
17
18
|
- [Arbre 1.x vs 2.x](#arbre-1x-vs-2x)
|
|
@@ -139,32 +140,33 @@ A component describes its structure in `build`. The Weft-idiomatic shape takes a
|
|
|
139
140
|
class EventSummary < Weft::Component
|
|
140
141
|
builder_method :event_summary
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
param :event_id
|
|
143
144
|
|
|
144
145
|
def build(attributes = {})
|
|
145
146
|
super
|
|
146
|
-
event = EventStore.find(
|
|
147
|
+
event = EventStore.find(params.event_id)
|
|
147
148
|
h3 event.name
|
|
148
149
|
para "#{event.date} — #{event.location}"
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
```
|
|
152
153
|
|
|
153
|
-
**Arguments arrive positionally — always.** When a call site writes `event_summary(
|
|
154
|
+
**Arguments arrive positionally — always.** When a call site writes `event_summary(class: "compact")`, Arbre collects the arguments and passes them positionally to `build`; the keywords become one trailing hash. Declaring Ruby keyword parameters — `def build(compact:)` — raises `ArgumentError: wrong number of arguments`. Take the hash. Note what's *not* in that call: the component's `event_id` param. Params travel their own channel — from the request, or down the render tree from an enclosing page — never through the builder call. What you pass here is HTML chrome for the wrapper.
|
|
154
155
|
|
|
155
|
-
**`super`
|
|
156
|
+
**`super` applies the hash and wires the wrapper.** Your params are already resolved before `build` runs — Weft resolves them when the component is constructed, so you can read `params` even above the `super` call (deriving a heading from a record looked up by param, say). What `super` does is apply the trailing hash as HTML attributes on the wrapper element (that's where `class: "compact"` went), set the wrapper's DOM id ([derived from your first param](dsl.md#params)), and attach any refresh or push wiring. Skip `super` and none of that happens — the classic symptom is a component that ignores the `class:` you pass it.
|
|
156
157
|
|
|
157
|
-
**Rich objects
|
|
158
|
+
**Rich objects come through `receives`.** Wire params (declared with `param`) are for values small enough to travel in a URL. When a call site hands the component a rich object it already holds — a record, a computed value, anything that can't ride a query string — declare it with [`receives`](dsl.md#receives--caller-hand-offs) and read it from `params` like any other input. The value is handed straight across: it never serializes into a URL, and it never lands on the wrapper as an HTML attribute.
|
|
158
159
|
|
|
159
160
|
```ruby
|
|
160
161
|
class AttendeeRow < Weft::Component
|
|
161
162
|
builder_method :attendee_row
|
|
162
163
|
|
|
164
|
+
receives :attendee
|
|
165
|
+
|
|
163
166
|
def build(attributes = {})
|
|
164
|
-
@attendee = attributes.delete(:attendee) # rich object out first
|
|
165
167
|
super
|
|
166
|
-
td
|
|
167
|
-
td
|
|
168
|
+
td params.attendee.name
|
|
169
|
+
td params.attendee.answer
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def tag_name
|
|
@@ -182,7 +184,7 @@ end
|
|
|
182
184
|
Composable components take a block of caller content:
|
|
183
185
|
|
|
184
186
|
```ruby
|
|
185
|
-
event_card
|
|
187
|
+
event_card do
|
|
186
188
|
para "Bring a dish to share!"
|
|
187
189
|
end
|
|
188
190
|
```
|
|
@@ -196,11 +198,11 @@ class EventCard < Weft::Component
|
|
|
196
198
|
builder_method :event_card
|
|
197
199
|
adds_children_to :@body
|
|
198
200
|
|
|
199
|
-
|
|
201
|
+
param :event_id
|
|
200
202
|
|
|
201
203
|
def build(attributes = {})
|
|
202
204
|
super
|
|
203
|
-
h3 EventStore.find(
|
|
205
|
+
h3 EventStore.find(params.event_id).name # structure — lands on the wrapper
|
|
204
206
|
@body = div(class: "event-card-body") # caller content lands in here
|
|
205
207
|
end
|
|
206
208
|
end
|
|
@@ -244,13 +246,41 @@ end # <div>replacement</div>
|
|
|
244
246
|
|
|
245
247
|
It's the right tool for wholesale swaps and a footgun everywhere else. To add without destroying, create elements normally or use `text_node`.
|
|
246
248
|
|
|
249
|
+
## Reaching enclosing components
|
|
250
|
+
|
|
251
|
+
`parent` and `ancestors` walk the *element* tree — the divs and spans. Weft adds a **component**-level parallel: a nested component can reach an enclosing component or page and read its identity. This is how a child affects an ancestor. A pager, for instance, needs its enclosing panel's route and DOM id to aim its "next page" swap at the panel — rather than being hand-fed all of that at the call site, it reaches for it:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
class Pager < Weft::Component
|
|
255
|
+
def build(attributes = {})
|
|
256
|
+
super
|
|
257
|
+
panel = enclosing(Weft::Component) # the nearest ancestor component
|
|
258
|
+
button "Next →", loads: panel.class, target: "##{panel.weft_dom_id}"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**`closest` and `enclosing`.** `closest(matcher)` returns the nearest matching node, *self included*, walking upward — impedance-matched to the DOM's `element.closest()`. `enclosing(matcher)` is the same walk but strictly above self — the natural read for "my enclosing X." Both return `nil` when nothing matches; the `!` variants (`closest!` / `enclosing!`) raise `Weft::AncestorNotFound` instead, for a component that genuinely requires the ancestor (pair it with [`dependent!`](routing.md#abstract-and-routable)).
|
|
264
|
+
|
|
265
|
+
What you can match on:
|
|
266
|
+
|
|
267
|
+
- a **Class or Module** — matched `is_a?`, so subclasses and included modules count: `enclosing(Weft::Page)` finds the nearest page of any kind, `enclosing(OrdersPanel)` a specific type, `enclosing(Paginatable)` anything that mixes in that role.
|
|
268
|
+
- a **Symbol** — matched against the tag name: `closest(:section)` hands back the nearest enclosing `<section>`, no need to know Arbre's internal element classes.
|
|
269
|
+
- an optional **block** that *refines*: a candidate must match the positional matcher **and** the block — `closest(Weft::Component) { |c| c.params.key?(:order_id) }`.
|
|
270
|
+
|
|
271
|
+
The return value is the matching node — a component for a class match, a plain element for a tag.
|
|
272
|
+
|
|
273
|
+
**What to rely on.** The ancestor you reach mid-`build` is itself mid-build, above you on the stack — so lean on its *identity* and *params* (its class, `weft_dom_id`, route, resolved params, all fixed at construction), not on instance variables its own `build` may not have set yet. And mind one edge of the refining block: it runs only on nodes that already matched the positional, but `closest(Weft::Component, &:paginatable?)` still raises if a matched component doesn't define `paginatable?` — prefer matching the **role module** (`closest(Paginatable)`) over a predicate where you can.
|
|
274
|
+
|
|
275
|
+
One include-self subtlety: because `closest` includes self and *every* component is a `Weft::Component`, `closest(Weft::Component)` returns **self**. "My nearest ancestor component" is `enclosing(Weft::Component)` (or `closest(Weft::Component, include_self: false)`).
|
|
276
|
+
|
|
247
277
|
## Forms
|
|
248
278
|
|
|
249
279
|
Forms are ordinary Arbre: `form`, `label`, `input`, `select`, `option`, `textarea` are tag builders like any other, and you compose them like any other markup. What Weft adds is the wiring — `form(action: :submit)` connects the form to a declared action, with htmx submission and a no-JavaScript fallback emitted for free ([the DSL reference](dsl.md#action) has the mechanics; the [tutorial](tutorial.md#7-taking-rsvps) builds a full working form).
|
|
250
280
|
|
|
251
281
|
Two field-level idioms worth knowing:
|
|
252
282
|
|
|
253
|
-
- **Field names pair with declared
|
|
283
|
+
- **Field names pair with declared params.** In a Weft form, an `input name: "answer"` reaches the action callable as `params.answer` when the component declares `param :answer` — and component params that *aren't* form fields need a hidden input to travel. The [tutorial](tutorial.md#7-taking-rsvps) walks through both halves.
|
|
254
284
|
- **Array parameters use the `name[]` convention:** `input type: "checkbox", name: "toppings[]", value: "olives"` — submitted values arrive as an array.
|
|
255
285
|
|
|
256
286
|
## Testing components
|
|
@@ -267,22 +297,23 @@ RSpec.describe AttendeeList do
|
|
|
267
297
|
end
|
|
268
298
|
```
|
|
269
299
|
|
|
270
|
-
`Component.render
|
|
300
|
+
`Component.render` is the gem-provided entry point and covers most component testing — its keyword arguments are exactly the wire params a request would carry. When you want the element tree rather than the string — asserting on classes, structure, or specific descendants — build a `Weft::Context` and search it. Its `wire_params:` argument stands in for the request, so the component's declared params resolve just as they would over the wire:
|
|
271
301
|
|
|
272
302
|
```ruby
|
|
273
|
-
ctx =
|
|
274
|
-
attendee_list
|
|
303
|
+
ctx = Weft::Context.new({}, nil, wire_params: { "event_id" => "trivia-night" }) do
|
|
304
|
+
attendee_list
|
|
275
305
|
end
|
|
276
306
|
list = ctx.children.first
|
|
277
307
|
expect(list.class_list).to include("roster")
|
|
278
308
|
expect(ctx.find_by_tag("li").length).to eq(2)
|
|
279
309
|
```
|
|
280
310
|
|
|
281
|
-
|
|
311
|
+
A value the component `receives` is handed the way it is in production — as a builder kwarg, `attendee_list(roster: some_roster)` — since a declared `receives` key consumes the kwarg rather than letting it fall through to an HTML attribute. Capture that value into a local first: the block runs *inside* the context, so a bare `let` name isn't in scope there.
|
|
312
|
+
|
|
313
|
+
Two Arbre-specific notes for test code:
|
|
282
314
|
|
|
283
315
|
- **Give test component classes real names.** `builder_method` resolves its class by name at call time, so an anonymous class (`Class.new(Weft::Component)`) with a stubbed `name` raises `NameError` the first time its builder is invoked — and under Arbre 1.x, even `insert_tag` with a truly anonymous class crashes. Define named classes (a `TestCard = Class.new(...)` constant works) rather than fighting it.
|
|
284
|
-
-
|
|
285
|
-
- **The helpers slot** (`Arbre::Context.new(assigns, helpers)`) makes any object's methods callable inside the block. Weft renders components without helpers, so tests should too, unless you're testing raw Arbre code that expects them.
|
|
316
|
+
- **`assigns` and `helpers` are Arbre's channels, not Weft's.** `Weft::Context.new(assigns, helpers, wire_params:)` still carries Arbre's two data slots — `assigns` (resolved through Arbre's lookup chain) and `helpers` (an object whose methods become callable bare in the block). Weft components read `params` and use neither; reach for these only when a block holds raw Arbre code that expects them.
|
|
286
317
|
|
|
287
318
|
## Arbre 1.x vs 2.x
|
|
288
319
|
|