inertia_jb 0.2.0 → 0.3.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/README.md +78 -41
- data/lib/inertia_jb/helper.rb +11 -0
- data/lib/inertia_jb/version.rb +1 -1
- data/test/controller_test.rb +57 -2
- data/test/test_helper.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c821eb8ccc74579d17f768744858bb9a14f7838201dd2ff89be61f8f70298780
|
|
4
|
+
data.tar.gz: 4e385aefbe3adcdcd6069b422382cd91cc7cbe208f2275230433110a761a4d3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb985df39ed4ec250604244d93c968884b8d4a47588e031126d48f5b9489e5015192fc021de81c4b4e4d06018714065ed757046ec9728ed7804f930f073dd95e
|
|
7
|
+
data.tar.gz: d3e94480c54b50667a66c9416a59d6124e410b6d5da165e3344137128c9b2b88b111e07897ffccc0b56a915c955e060a0c103129813d476a99e085af337f2ccf
|
data/README.md
CHANGED
|
@@ -63,9 +63,6 @@ Hashes, so there is zero impedance mismatch: no DSL to learn, no intermediate
|
|
|
63
63
|
representation, and the full power of Ruby for building collections and
|
|
64
64
|
conditionals.
|
|
65
65
|
|
|
66
|
-
> Coming from `props_template`? That gem streams JSON strings, which don't map
|
|
67
|
-
> cleanly onto Inertia's Hash-based resolver. jb's plain-Hash output is the
|
|
68
|
-
> natural fit, which is why this gem is built on it.
|
|
69
66
|
|
|
70
67
|
## Installation
|
|
71
68
|
|
|
@@ -95,31 +92,6 @@ a layout; Inertia (XHR) visits always return a bare JSON body with no layout.
|
|
|
95
92
|
The layout is chosen from inertia-rails' `config.layout`, matching
|
|
96
93
|
`InertiaRails::Renderer`'s own semantics:
|
|
97
94
|
|
|
98
|
-
```ruby
|
|
99
|
-
# config/initializers/inertia_rails.rb
|
|
100
|
-
InertiaRails.configure do |config|
|
|
101
|
-
config.default_render = false
|
|
102
|
-
|
|
103
|
-
# config.layout = true # (default) use the controller's normal layout
|
|
104
|
-
# config.layout = "inertia" # use app/views/layouts/inertia.html.erb
|
|
105
|
-
# config.layout = false # no layout — render just the <div id="app"> root
|
|
106
|
-
end
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
- `true` / `nil` — the controller's default layout, resolved the normal Rails
|
|
110
|
-
way (`app/views/layouts/application.html.erb`, or any `layout "..."`
|
|
111
|
-
declaration in the controller).
|
|
112
|
-
- A **String** — that named layout (`app/views/layouts/<name>.html.erb`).
|
|
113
|
-
- `false` — no layout at all; the response is only the `<div id="app"
|
|
114
|
-
data-page="…">` root, so you provide `<html>`/`<head>`/asset tags elsewhere.
|
|
115
|
-
|
|
116
|
-
Because `config.layout` is scoped per controller in inertia-rails, you can also
|
|
117
|
-
set it on a single controller with `inertia_config layout: "..."`. Plain
|
|
118
|
-
(non-Inertia) `.html.erb` actions in the same app keep their layout regardless
|
|
119
|
-
of this setting.
|
|
120
|
-
|
|
121
|
-
> **Note:** this gem does not perform server-side rendering (SSR), so
|
|
122
|
-
> `inertia_ssr_head` in your layout will always be empty.
|
|
123
95
|
|
|
124
96
|
## Templates and partials
|
|
125
97
|
|
|
@@ -145,11 +117,75 @@ of this setting.
|
|
|
145
117
|
which you embed directly. Don't name partials `.html.inertia` — that extension
|
|
146
118
|
triggers the Inertia response wrapper and is only for top-level page templates.
|
|
147
119
|
|
|
120
|
+
## Sharing a partial with a plain JSON API
|
|
121
|
+
|
|
122
|
+
An Inertia page and a plain JSON endpoint are both, in the end, just **a Hash**,
|
|
123
|
+
so a single jb partial can back both. Name the partial **without a format**
|
|
124
|
+
(`_message.jb`, not `_message.html.jb`) so it resolves for the `html` format
|
|
125
|
+
Inertia uses *and* the `json` format a normal API request uses:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# app/views/messages/_message.jb
|
|
129
|
+
{
|
|
130
|
+
id: message.id,
|
|
131
|
+
content: message.content,
|
|
132
|
+
author: render(partial: "authors/author", object: message.author)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# app/views/messages/show.html.inertia — the Inertia page
|
|
138
|
+
{ **render(partial: "messages/message", object: @message) }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# app/views/messages/index.html.inertia — nested under a key
|
|
143
|
+
{ messages: render(partial: "messages/message", collection: @messages, as: :message) }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# app/views/messages/show.json.jb — a plain JSON endpoint
|
|
148
|
+
render(partial: "messages/message", object: @message)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
Note the asymmetry: the JSON endpoint can return that Array at the top level,
|
|
153
|
+
but the Inertia page **must** nest it under a key (`{ posts: … }`) — Inertia
|
|
154
|
+
props must be an object, never a top-level Array.
|
|
155
|
+
|
|
156
|
+
> **Gotcha — wrap the page template in a Hash literal.** A `.html.inertia` page
|
|
157
|
+
> must **not** be a bare top-level `render(partial: …)`:
|
|
158
|
+
>
|
|
159
|
+
> ```ruby
|
|
160
|
+
> # ❌ props get misread as the component name
|
|
161
|
+
> render(partial: "messages/message", object: @message)
|
|
162
|
+
>
|
|
163
|
+
> # ✅ spread into a real Hash literal
|
|
164
|
+
> { **render(partial: "messages/message", object: @message) }
|
|
165
|
+
> ```
|
|
166
|
+
>
|
|
167
|
+
> jb's `render(partial:)` returns a `Jb::TemplateResult` (a delegator), not a
|
|
168
|
+
> true `Hash`. inertia-rails decides *"is this props or a component name?"* with
|
|
169
|
+
> `component.is_a?(Hash)`, so a bare partial result is taken for a component name
|
|
170
|
+
> and your props end up in the `component` field. Wrapping it in a literal
|
|
171
|
+
> `{ **… }` — or nesting it under a key, e.g. `{ message: render(…) }` — makes
|
|
172
|
+
> the top-level value a genuine `Hash`, which inertia-rails reads as props. A
|
|
173
|
+
> `.json.jb` endpoint never hits this, because jb serializes its top-level result
|
|
174
|
+
> with `to_json` directly.
|
|
175
|
+
|
|
176
|
+
If you'd rather keep a format-specific partial (`_message.json.jb`), borrow the
|
|
177
|
+
`:json` variant from the Inertia side with `formats:`:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# app/views/messages/show.html.inertia
|
|
181
|
+
{ **render(partial: "messages/message", object: @message, formats: [:json]) }
|
|
182
|
+
```
|
|
183
|
+
|
|
148
184
|
## Inertia prop types
|
|
149
185
|
|
|
150
186
|
Because props are just a Hash, Inertia's special prop types are plain values you
|
|
151
187
|
drop in. Inside a `.html.inertia` template you can use the short helpers
|
|
152
|
-
(`optional`, `always`, `defer`, `scroll`, `merge`, `deep_merge`) or the full
|
|
188
|
+
(`optional`, `always`, `defer`, `scroll`, `merge`, `deep_merge`, `once`, `cache`) or the full
|
|
153
189
|
`InertiaRails.*` methods.
|
|
154
190
|
|
|
155
191
|
```ruby
|
|
@@ -171,23 +207,24 @@ drop in. Inside a `.html.inertia` template you can use the short helpers
|
|
|
171
207
|
# Infinite scrolling (accepts a paginator or explicit metadata).
|
|
172
208
|
feed: scroll(@pagy) {
|
|
173
209
|
render(partial: "feed/item", collection: @items)
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
# Sent once and cached client-side; skipped on later visits until reset.
|
|
213
|
+
flash: once { session.delete(:flash) },
|
|
214
|
+
|
|
215
|
+
# Server-side cached via Rails.cache; the block's JSON output is reused
|
|
216
|
+
# across requests until the cache entry expires.
|
|
217
|
+
report: cache("posts/#{@post.id}/report", expires_in: 5.minutes) {
|
|
218
|
+
render(partial: "reports/report", object: @post.report)
|
|
174
219
|
}
|
|
175
220
|
}
|
|
176
221
|
```
|
|
177
222
|
|
|
178
223
|
See the inertia-rails docs for [partial reloads](https://inertiajs.com/partial-reloads),
|
|
179
|
-
[deferred props](https://inertiajs.com/deferred-props),
|
|
180
|
-
[infinite scroll](https://inertia-rails.dev/guide/infinite-scroll)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Use plain Rails caching — you're caching Ruby Hashes:
|
|
185
|
-
|
|
186
|
-
```ruby
|
|
187
|
-
@posts.map do |post|
|
|
188
|
-
Rails.cache.fetch(post) { render(partial: "posts/post", object: post) }
|
|
189
|
-
end
|
|
190
|
-
```
|
|
224
|
+
[deferred props](https://inertiajs.com/deferred-props),
|
|
225
|
+
[infinite scroll](https://inertia-rails.dev/guide/infinite-scroll),
|
|
226
|
+
[once props](https://inertia-rails.dev/guide/once-props), and
|
|
227
|
+
[prop caching](https://inertia-rails.dev/guide/prop-caching).
|
|
191
228
|
|
|
192
229
|
## camelCase keys
|
|
193
230
|
|
data/lib/inertia_jb/helper.rb
CHANGED
|
@@ -37,5 +37,16 @@ module InertiaJb
|
|
|
37
37
|
def deep_merge(...)
|
|
38
38
|
::InertiaRails.deep_merge(...)
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
# Sent once and cached client-side; skipped on later visits unless reset or expired.
|
|
42
|
+
def once(...)
|
|
43
|
+
::InertiaRails.once(...)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Server-side cached via Rails.cache; the block's result is stored as JSON
|
|
47
|
+
# and reused on subsequent requests until the cache expires.
|
|
48
|
+
def cache(...)
|
|
49
|
+
::InertiaRails.cache(...)
|
|
50
|
+
end
|
|
40
51
|
end
|
|
41
52
|
end
|
data/lib/inertia_jb/version.rb
CHANGED
data/test/controller_test.rb
CHANGED
|
@@ -29,11 +29,18 @@ class TestController < ActionController::Base
|
|
|
29
29
|
def special
|
|
30
30
|
@id = 1
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
# once (client-cached) + cache (server-cached).
|
|
34
|
+
def cached
|
|
35
|
+
@call_count = 0
|
|
36
|
+
end
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
class ControllerTest < ActionController::TestCase
|
|
35
40
|
tests TestController
|
|
36
41
|
|
|
42
|
+
include ActiveSupport::Testing::TimeHelpers
|
|
43
|
+
|
|
37
44
|
TEMPLATES = {
|
|
38
45
|
"layouts/application.html.erb" => "<html><body><%= yield %></body></html>",
|
|
39
46
|
"test/index.html.inertia" => "{ content: 'content' }",
|
|
@@ -45,7 +52,10 @@ class ControllerTest < ActionController::TestCase
|
|
|
45
52
|
"authors/_author.html.jb" => "{ id: author[:id], name: author[:name] }",
|
|
46
53
|
"comments/_comment.html.jb" => "{ body: comment[:body] }",
|
|
47
54
|
"test/special.html.inertia" =>
|
|
48
|
-
"{ id: @id, stats: optional { { visits: 42 } }, feed: defer(group: :feed) { [1, 2, 3] } }"
|
|
55
|
+
"{ id: @id, stats: optional { { visits: 42 } }, feed: defer(group: :feed) { [1, 2, 3] } }",
|
|
56
|
+
"test/cached.html.inertia" =>
|
|
57
|
+
"{ flash: once { 'hello' }, " \
|
|
58
|
+
"report: cache('report', expires_in: 1.minute) { Time.current.to_f } }"
|
|
49
59
|
}.freeze
|
|
50
60
|
|
|
51
61
|
def setup
|
|
@@ -53,7 +63,7 @@ class ControllerTest < ActionController::TestCase
|
|
|
53
63
|
|
|
54
64
|
@routes = ActionDispatch::Routing::RouteSet.new
|
|
55
65
|
@routes.draw do
|
|
56
|
-
%i[index nested collection with_partial special].each do |action|
|
|
66
|
+
%i[index nested collection with_partial special cached].each do |action|
|
|
57
67
|
get action.to_s => "test##{action}"
|
|
58
68
|
end
|
|
59
69
|
end
|
|
@@ -65,6 +75,7 @@ class ControllerTest < ActionController::TestCase
|
|
|
65
75
|
def teardown
|
|
66
76
|
super
|
|
67
77
|
@routes.clear!
|
|
78
|
+
Rails.cache.clear
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
# ---- basic rendering -----------------------------------------------------
|
|
@@ -158,6 +169,50 @@ class ControllerTest < ActionController::TestCase
|
|
|
158
169
|
assert_equal({ "feed" => ["feed"] }, page["deferredProps"])
|
|
159
170
|
end
|
|
160
171
|
|
|
172
|
+
# ---- once props (client-cached) -----------------------------------------
|
|
173
|
+
|
|
174
|
+
def test_once_prop_included_on_first_visit
|
|
175
|
+
inertia_get :cached
|
|
176
|
+
page = JSON.parse(response.body)
|
|
177
|
+
|
|
178
|
+
assert_equal "hello", page.dig("props", "flash")
|
|
179
|
+
assert_equal({ "flash" => { "prop" => "flash" } }, page["onceProps"])
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_once_prop_excluded_when_client_reports_it_cached
|
|
183
|
+
inertia_get :cached,
|
|
184
|
+
headers: {
|
|
185
|
+
"X-Inertia-Partial-Component" => "test/cached",
|
|
186
|
+
"X-Inertia-Except-Once-Props" => "flash"
|
|
187
|
+
}
|
|
188
|
+
page = JSON.parse(response.body)
|
|
189
|
+
|
|
190
|
+
refute page["props"].key?("flash"), "once prop should be absent when client reports it cached"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ---- cached props (server-cached) ---------------------------------------
|
|
194
|
+
|
|
195
|
+
def test_cached_prop_serves_same_value_across_requests
|
|
196
|
+
inertia_get :cached
|
|
197
|
+
first = JSON.parse(response.body).dig("props", "report")
|
|
198
|
+
|
|
199
|
+
inertia_get :cached
|
|
200
|
+
second = JSON.parse(response.body).dig("props", "report")
|
|
201
|
+
|
|
202
|
+
assert_equal first, second, "cached prop should return the same value across requests"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_cached_prop_recomputes_after_expiry
|
|
206
|
+
inertia_get :cached
|
|
207
|
+
first = JSON.parse(response.body).dig("props", "report")
|
|
208
|
+
|
|
209
|
+
travel_to 2.minutes.from_now do
|
|
210
|
+
inertia_get :cached
|
|
211
|
+
second = JSON.parse(response.body).dig("props", "report")
|
|
212
|
+
refute_equal first, second, "cached prop should recompute after expiry"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
161
216
|
private
|
|
162
217
|
|
|
163
218
|
def inertia_get(action, headers: {})
|
data/test/test_helper.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "action_controller/railtie"
|
|
|
6
6
|
require "inertia_rails"
|
|
7
7
|
require "inertia_jb"
|
|
8
8
|
require "active_support/testing/autorun"
|
|
9
|
+
require "active_support/testing/time_helpers"
|
|
9
10
|
|
|
10
11
|
ActiveSupport.test_order = :random
|
|
11
12
|
|
|
@@ -14,6 +15,7 @@ ActiveSupport.test_order = :random
|
|
|
14
15
|
Class.new(Rails::Application) do
|
|
15
16
|
config.secret_key_base = "secret"
|
|
16
17
|
config.eager_load = false
|
|
18
|
+
config.cache_store = :memory_store
|
|
17
19
|
end.initialize!
|
|
18
20
|
|
|
19
21
|
InertiaRails.configure do |c|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inertia_jb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kikyous
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: inertia_rails
|