inertia_jb 0.1.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 +87 -16
- data/lib/inertia_jb/controller.rb +16 -30
- data/lib/inertia_jb/handler.rb +7 -10
- data/lib/inertia_jb/helper.rb +11 -0
- data/lib/inertia_jb/railtie.rb +1 -0
- data/lib/inertia_jb/renderer.rb +26 -33
- data/lib/inertia_jb/version.rb +1 -1
- data/test/controller_test.rb +57 -2
- data/test/layout_suppression_test.rb +90 -0
- data/test/layout_test.rb +136 -0
- data/test/test_helper.rb +2 -0
- metadata +6 -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
|
|
|
@@ -87,6 +84,15 @@ end
|
|
|
87
84
|
|
|
88
85
|
You do **not** need to call `use_inertia_instance_props`.
|
|
89
86
|
|
|
87
|
+
## Layout
|
|
88
|
+
|
|
89
|
+
On an **initial (non-XHR) page load** the `data-page` root element is wrapped in
|
|
90
|
+
a layout; Inertia (XHR) visits always return a bare JSON body with no layout.
|
|
91
|
+
|
|
92
|
+
The layout is chosen from inertia-rails' `config.layout`, matching
|
|
93
|
+
`InertiaRails::Renderer`'s own semantics:
|
|
94
|
+
|
|
95
|
+
|
|
90
96
|
## Templates and partials
|
|
91
97
|
|
|
92
98
|
- **Page templates** live at `app/views/<controller>/<action>.html.inertia` and
|
|
@@ -111,11 +117,75 @@ You do **not** need to call `use_inertia_instance_props`.
|
|
|
111
117
|
which you embed directly. Don't name partials `.html.inertia` — that extension
|
|
112
118
|
triggers the Inertia response wrapper and is only for top-level page templates.
|
|
113
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
|
+
|
|
114
184
|
## Inertia prop types
|
|
115
185
|
|
|
116
186
|
Because props are just a Hash, Inertia's special prop types are plain values you
|
|
117
187
|
drop in. Inside a `.html.inertia` template you can use the short helpers
|
|
118
|
-
(`optional`, `always`, `defer`, `scroll`, `merge`, `deep_merge`) or the full
|
|
188
|
+
(`optional`, `always`, `defer`, `scroll`, `merge`, `deep_merge`, `once`, `cache`) or the full
|
|
119
189
|
`InertiaRails.*` methods.
|
|
120
190
|
|
|
121
191
|
```ruby
|
|
@@ -137,23 +207,24 @@ drop in. Inside a `.html.inertia` template you can use the short helpers
|
|
|
137
207
|
# Infinite scrolling (accepts a paginator or explicit metadata).
|
|
138
208
|
feed: scroll(@pagy) {
|
|
139
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)
|
|
140
219
|
}
|
|
141
220
|
}
|
|
142
221
|
```
|
|
143
222
|
|
|
144
223
|
See the inertia-rails docs for [partial reloads](https://inertiajs.com/partial-reloads),
|
|
145
|
-
[deferred props](https://inertiajs.com/deferred-props),
|
|
146
|
-
[infinite scroll](https://inertia-rails.dev/guide/infinite-scroll)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Use plain Rails caching — you're caching Ruby Hashes:
|
|
151
|
-
|
|
152
|
-
```ruby
|
|
153
|
-
@posts.map do |post|
|
|
154
|
-
Rails.cache.fetch(post) { render(partial: "posts/post", object: post) }
|
|
155
|
-
end
|
|
156
|
-
```
|
|
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).
|
|
157
228
|
|
|
158
229
|
## camelCase keys
|
|
159
230
|
|
|
@@ -1,42 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module InertiaJb
|
|
4
|
-
# Controller concern that
|
|
5
|
-
#
|
|
4
|
+
# Controller concern that renders `.html.inertia` templates as Inertia
|
|
5
|
+
# responses.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# A `.html.inertia` page template is reached through Rails' normal implicit
|
|
8
|
+
# render. Its body (compiled by {Handler}) throws the props Hash back here,
|
|
9
|
+
# which aborts the implicit render — layout and all — before we re-render the
|
|
10
|
+
# page through inertia-rails' native renderer via `render inertia:`. Component
|
|
11
|
+
# resolution, shared data, `PropsResolver`/partial reloads, `config.layout`,
|
|
12
|
+
# SSR and the X-Inertia/Vary headers are therefore all handled by
|
|
13
|
+
# inertia-rails, identically to a plain inertia-rails app.
|
|
10
14
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
15
|
+
# Plain (non-Inertia) `.html.erb` actions never throw, so `catch` falls
|
|
16
|
+
# through to `super` and they render exactly as in vanilla Rails.
|
|
17
|
+
#
|
|
18
|
+
# +InertiaRails::Controller+ is already mixed into +ActionController::Base+ by
|
|
13
19
|
# inertia-rails' engine, so we don't include it again.
|
|
14
20
|
module Controller
|
|
15
21
|
extend ActiveSupport::Concern
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def format_inertia_jb_response
|
|
24
|
-
response.headers["Vary"] = if response.headers["Vary"].blank?
|
|
25
|
-
"X-Inertia"
|
|
26
|
-
else
|
|
27
|
-
"#{response.headers['Vary']}, X-Inertia"
|
|
28
|
-
end
|
|
29
|
-
response.set_header("X-Inertia", "true")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Inertia (XHR) requests return a bare JSON body — no application layout.
|
|
33
|
-
# Initial page loads keep the layout so the `data-page` root is wrapped.
|
|
34
|
-
def action_has_layout?
|
|
35
|
-
!inertia_json_request? && super
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def inertia_json_request?
|
|
39
|
-
request.headers["X-Inertia"] == "true"
|
|
23
|
+
def default_render(*)
|
|
24
|
+
props = catch(:__inertia_jb) { return super }
|
|
25
|
+
render(inertia: props)
|
|
40
26
|
end
|
|
41
27
|
end
|
|
42
28
|
end
|
data/lib/inertia_jb/handler.rb
CHANGED
|
@@ -4,21 +4,18 @@ module InertiaJb
|
|
|
4
4
|
# ActionView template handler for `*.html.inertia` templates.
|
|
5
5
|
#
|
|
6
6
|
# An `.html.inertia` template is jb-style Ruby: its last expression is a Hash
|
|
7
|
-
# of Inertia props.
|
|
8
|
-
# {
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# partials each return their own Hash independently, so no `@__json`-style
|
|
12
|
-
# finalize dance is needed here.
|
|
7
|
+
# of Inertia props. The handler evaluates that Hash and immediately `throw`s
|
|
8
|
+
# it back to {Controller#default_render}, which hands it to inertia-rails'
|
|
9
|
+
# native renderer. The `throw` also aborts this implicit render (and the
|
|
10
|
+
# layout it was about to apply), so the handler emits no body of its own.
|
|
13
11
|
class Handler
|
|
14
12
|
def self.call(template, source = nil)
|
|
15
13
|
source ||= template.source
|
|
16
14
|
|
|
17
15
|
# `begin;#{source}` keeps the template's own line numbers aligned in
|
|
18
|
-
# backtraces. The
|
|
19
|
-
#
|
|
20
|
-
"
|
|
21
|
-
"::InertiaJb::Renderer.new(self, __inertia_props, true).render"
|
|
16
|
+
# backtraces. The block evaluates to the template's last expression (the
|
|
17
|
+
# props Hash), which we throw up to the controller to render.
|
|
18
|
+
"throw(:__inertia_jb, begin;#{source}\nend)"
|
|
22
19
|
end
|
|
23
20
|
end
|
|
24
21
|
end
|
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/railtie.rb
CHANGED
|
@@ -12,6 +12,7 @@ module InertiaJb
|
|
|
12
12
|
ActiveSupport.on_load(:action_view) do
|
|
13
13
|
ActionView::Template.register_template_handler :inertia, InertiaJb::Handler
|
|
14
14
|
ActionView::Base.include InertiaJb::Helper
|
|
15
|
+
ActionView::TemplateRenderer.prepend InertiaJb::Renderer
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
data/lib/inertia_jb/renderer.rb
CHANGED
|
@@ -1,41 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module InertiaJb
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Prepended onto +ActionView::TemplateRenderer+ to guarantee that a
|
|
5
|
+
# `.html.inertia` template is never wrapped in a layout.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
page = @inertia_renderer.send(:page)
|
|
7
|
+
# An `.inertia` template's body (see {Handler}) throws its props Hash back to
|
|
8
|
+
# the controller before it produces any output, so a surrounding layout is
|
|
9
|
+
# always discarded. Left alone, Rails would still resolve and compile the
|
|
10
|
+
# controller's layout and emit phantom `render_layout.action_view` /
|
|
11
|
+
# `render_template.action_view` events for it — their `ensure` blocks fire as
|
|
12
|
+
# the `throw` unwinds the stack — whose timing absorbs the prop-building work,
|
|
13
|
+
# polluting request logs and APM traces with a layout render that never
|
|
14
|
+
# actually happened.
|
|
15
|
+
#
|
|
16
|
+
# +render_template+ is the one point where Rails has already resolved the
|
|
17
|
+
# template (so we can see its handler) but has not yet looked up the layout
|
|
18
|
+
# (`find_layout` runs one call deeper, in +render_with_layout+). Nulling the
|
|
19
|
+
# layout here for our handler skips the lookup entirely — cleanly, without the
|
|
20
|
+
# controller having to guess in advance which template an action will render.
|
|
21
|
+
#
|
|
22
|
+
# This only affects our own templates. The real Inertia shell is applied later
|
|
23
|
+
# by inertia-rails when it renders the `inertia` root template (an ERB
|
|
24
|
+
# template) with `config.layout`, and plain `.erb`/`.jb` templates keep their
|
|
25
|
+
# layouts exactly as in vanilla Rails.
|
|
26
|
+
module Renderer
|
|
27
|
+
private
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@view_context.controller.render_to_string(
|
|
34
|
-
template: "inertia",
|
|
35
|
-
layout: false,
|
|
36
|
-
locals: { page: page }
|
|
37
|
-
)
|
|
38
|
-
end
|
|
29
|
+
def render_template(view, template, layout_name, locals)
|
|
30
|
+
layout_name = nil if template.handler == InertiaJb::Handler
|
|
31
|
+
super
|
|
39
32
|
end
|
|
40
33
|
end
|
|
41
34
|
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: {})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "action_view/testing/resolvers"
|
|
5
|
+
|
|
6
|
+
# Regression test for the "phantom layout render" bug.
|
|
7
|
+
#
|
|
8
|
+
# An `.inertia` page throws its props before producing output, so the
|
|
9
|
+
# controller's layout is always discarded. Rails must therefore never resolve,
|
|
10
|
+
# compile, or *instrument* that layout while rendering the props template —
|
|
11
|
+
# otherwise request logs and APM traces show a `crm/application` layout render
|
|
12
|
+
# that never happened, with its timing absorbing the prop-building work.
|
|
13
|
+
#
|
|
14
|
+
# The controller here uses `layout "application"` but points Inertia at a
|
|
15
|
+
# *different* shell (`config.layout = "inertia"`), so the two are easy to tell
|
|
16
|
+
# apart in the instrumentation stream:
|
|
17
|
+
# * the `.inertia` props template must be rendered with NO layout, and
|
|
18
|
+
# * `layouts/application` must never be rendered at all.
|
|
19
|
+
class SuppressLayoutController < ActionController::Base
|
|
20
|
+
include InertiaJb::Controller
|
|
21
|
+
layout "application"
|
|
22
|
+
inertia_config layout: "inertia"
|
|
23
|
+
def index; end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class LayoutSuppressionTest < ActionController::TestCase
|
|
27
|
+
tests SuppressLayoutController
|
|
28
|
+
|
|
29
|
+
TEMPLATES = {
|
|
30
|
+
"layouts/application.html.erb" => "<html><body><%= yield %></body></html>",
|
|
31
|
+
"layouts/inertia.html.erb" => "<main data-inertia-shell=\"1\"><%= yield %></main>",
|
|
32
|
+
"suppress_layout/index.html.inertia" => "{ ok: true }"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def setup
|
|
36
|
+
super
|
|
37
|
+
@routes = ActionDispatch::Routing::RouteSet.new
|
|
38
|
+
@routes.draw { get "index" => "suppress_layout#index" }
|
|
39
|
+
@controller.prepend_view_path(ActionView::FixtureResolver.new(TEMPLATES))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def teardown
|
|
43
|
+
super
|
|
44
|
+
@routes.clear!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_inertia_template_is_rendered_without_a_layout
|
|
48
|
+
templates = capture("render_template.action_view") { get :index }
|
|
49
|
+
|
|
50
|
+
page = templates.find { |e| e.payload[:identifier].end_with?("index.html.inertia") }
|
|
51
|
+
assert page, "the .inertia props template should have been rendered"
|
|
52
|
+
assert_nil page.payload[:layout],
|
|
53
|
+
"the .inertia props template must be rendered without a layout, " \
|
|
54
|
+
"got #{page.payload[:layout].inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_controller_layout_is_never_rendered_for_an_inertia_page
|
|
58
|
+
layouts = capture("render_layout.action_view") { get :index }
|
|
59
|
+
identifiers = layouts.map { |e| e.payload[:identifier] }
|
|
60
|
+
|
|
61
|
+
assert identifiers.none? { |id| id.include?("layouts/application") },
|
|
62
|
+
"the controller layout must not be rendered for an Inertia page, saw #{identifiers.inspect}"
|
|
63
|
+
# Sanity check: the real Inertia shell (config.layout) still wraps the root.
|
|
64
|
+
assert identifiers.any? { |id| id.include?("layouts/inertia") },
|
|
65
|
+
"the Inertia shell layout should still wrap the data-page root, saw #{identifiers.inspect}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_rendered_output_is_wrapped_only_by_the_inertia_shell
|
|
69
|
+
get :index
|
|
70
|
+
|
|
71
|
+
assert_response :success
|
|
72
|
+
assert_includes response.body, "data-inertia-shell"
|
|
73
|
+
assert_includes response.body, "data-page"
|
|
74
|
+
refute_includes response.body, "<html><body>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Collects the instrumentation events for `name` emitted while the block runs.
|
|
80
|
+
def capture(name)
|
|
81
|
+
events = []
|
|
82
|
+
subscriber = ActiveSupport::Notifications.subscribe(name) do |*args|
|
|
83
|
+
events << ActiveSupport::Notifications::Event.new(*args)
|
|
84
|
+
end
|
|
85
|
+
yield
|
|
86
|
+
events
|
|
87
|
+
ensure
|
|
88
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
|
89
|
+
end
|
|
90
|
+
end
|
data/test/layout_test.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "action_view/testing/resolvers"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
# Controllers exercising inertia-rails' `config.layout` for initial page loads.
|
|
8
|
+
# `config.layout` is scoped per controller class via `inertia_config`, so each
|
|
9
|
+
# case gets its own controller and no global state is mutated.
|
|
10
|
+
|
|
11
|
+
class LayoutDefaultController < ActionController::Base
|
|
12
|
+
include InertiaJb::Controller
|
|
13
|
+
layout "application"
|
|
14
|
+
# No `inertia_config` — `config.layout` defaults to `true`.
|
|
15
|
+
def index; end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class LayoutFalseController < ActionController::Base
|
|
19
|
+
include InertiaJb::Controller
|
|
20
|
+
layout "application"
|
|
21
|
+
inertia_config layout: false
|
|
22
|
+
def index; end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class LayoutNamedController < ActionController::Base
|
|
26
|
+
include InertiaJb::Controller
|
|
27
|
+
layout "application"
|
|
28
|
+
inertia_config layout: "inertia"
|
|
29
|
+
def index; end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A plain (non-Inertia) HTML action living in an app that configured
|
|
33
|
+
# `config.layout = false`. Its layout must behave like vanilla Rails.
|
|
34
|
+
class PlainController < ActionController::Base
|
|
35
|
+
include InertiaJb::Controller
|
|
36
|
+
layout "application"
|
|
37
|
+
inertia_config layout: false
|
|
38
|
+
def index; end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module LayoutTestHelpers
|
|
42
|
+
TEMPLATES = {
|
|
43
|
+
"layouts/application.html.erb" => "<html><body><%= yield %></body></html>",
|
|
44
|
+
"layouts/inertia.html.erb" => "<main data-inertia-layout=\"1\"><%= yield %></main>",
|
|
45
|
+
"layout_default/index.html.inertia" => "{ ok: true }",
|
|
46
|
+
"layout_false/index.html.inertia" => "{ ok: true }",
|
|
47
|
+
"layout_named/index.html.inertia" => "{ ok: true }",
|
|
48
|
+
"plain/index.html.erb" => "plain-body-<%= 1 + 1 %>"
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
def setup
|
|
52
|
+
super
|
|
53
|
+
controller_path = @controller.class.controller_path
|
|
54
|
+
@routes = ActionDispatch::Routing::RouteSet.new
|
|
55
|
+
@routes.draw { get "index" => "#{controller_path}#index" }
|
|
56
|
+
@controller.prepend_view_path(ActionView::FixtureResolver.new(TEMPLATES))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def teardown
|
|
60
|
+
super
|
|
61
|
+
@routes.clear!
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# config.layout = true (default): the controller's default layout wraps the root.
|
|
66
|
+
class LayoutDefaultTest < ActionController::TestCase
|
|
67
|
+
include LayoutTestHelpers
|
|
68
|
+
tests LayoutDefaultController
|
|
69
|
+
|
|
70
|
+
def test_default_layout_wraps_the_data_page_root
|
|
71
|
+
get :index
|
|
72
|
+
|
|
73
|
+
assert_response :success
|
|
74
|
+
assert_includes response.body, "<html><body>"
|
|
75
|
+
assert_includes response.body, 'id="app"'
|
|
76
|
+
assert_includes response.body, "data-page"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# config.layout = false: no layout at all — just the bare `data-page` root.
|
|
81
|
+
class LayoutFalseTest < ActionController::TestCase
|
|
82
|
+
include LayoutTestHelpers
|
|
83
|
+
tests LayoutFalseController
|
|
84
|
+
|
|
85
|
+
def test_disabled_layout_renders_bare_root
|
|
86
|
+
get :index
|
|
87
|
+
|
|
88
|
+
assert_response :success
|
|
89
|
+
assert_includes response.body, 'id="app"'
|
|
90
|
+
assert_includes response.body, "data-page"
|
|
91
|
+
refute_includes response.body, "<html><body>"
|
|
92
|
+
refute_includes response.body, "data-inertia-layout"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_xhr_still_returns_json_when_layout_disabled
|
|
96
|
+
@request.headers["X-Inertia"] = "true"
|
|
97
|
+
get :index
|
|
98
|
+
|
|
99
|
+
assert_equal "true", response.headers["X-Inertia"]
|
|
100
|
+
page = JSON.parse(response.body)
|
|
101
|
+
assert_equal true, page.dig("props", "ok")
|
|
102
|
+
refute_includes response.body, "data-page"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# config.layout = "inertia": the named layout wraps the root, not the
|
|
107
|
+
# controller's own `layout "application"`.
|
|
108
|
+
class LayoutNamedTest < ActionController::TestCase
|
|
109
|
+
include LayoutTestHelpers
|
|
110
|
+
tests LayoutNamedController
|
|
111
|
+
|
|
112
|
+
def test_named_layout_wraps_the_root
|
|
113
|
+
get :index
|
|
114
|
+
|
|
115
|
+
assert_response :success
|
|
116
|
+
assert_includes response.body, "data-inertia-layout"
|
|
117
|
+
assert_includes response.body, 'id="app"'
|
|
118
|
+
refute_includes response.body, "<html><body>"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# A non-Inertia `.html.erb` action must keep its layout even when the app set
|
|
123
|
+
# `config.layout = false` for Inertia responses.
|
|
124
|
+
class PlainHtmlLayoutTest < ActionController::TestCase
|
|
125
|
+
include LayoutTestHelpers
|
|
126
|
+
tests PlainController
|
|
127
|
+
|
|
128
|
+
def test_plain_html_action_is_unaffected_by_inertia_config_layout
|
|
129
|
+
get :index
|
|
130
|
+
|
|
131
|
+
assert_response :success
|
|
132
|
+
assert_includes response.body, "<html><body>"
|
|
133
|
+
assert_includes response.body, "plain-body-2"
|
|
134
|
+
refute_includes response.body, "data-page"
|
|
135
|
+
end
|
|
136
|
+
end
|
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
|
|
@@ -85,6 +85,8 @@ files:
|
|
|
85
85
|
- lib/inertia_jb/renderer.rb
|
|
86
86
|
- lib/inertia_jb/version.rb
|
|
87
87
|
- test/controller_test.rb
|
|
88
|
+
- test/layout_suppression_test.rb
|
|
89
|
+
- test/layout_test.rb
|
|
88
90
|
- test/test_helper.rb
|
|
89
91
|
homepage: https://github.com/kikyous/inertia_jb
|
|
90
92
|
licenses:
|
|
@@ -115,4 +117,6 @@ summary: Declare Inertia.js props in Rails view templates with plain Ruby Hashes
|
|
|
115
117
|
by jb)
|
|
116
118
|
test_files:
|
|
117
119
|
- test/controller_test.rb
|
|
120
|
+
- test/layout_suppression_test.rb
|
|
121
|
+
- test/layout_test.rb
|
|
118
122
|
- test/test_helper.rb
|