inertia_jb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/lib/inertia_jb/controller.rb +42 -0
- data/lib/inertia_jb/handler.rb +24 -0
- data/lib/inertia_jb/helper.rb +41 -0
- data/lib/inertia_jb/railtie.rb +18 -0
- data/lib/inertia_jb/renderer.rb +41 -0
- data/lib/inertia_jb/version.rb +5 -0
- data/lib/inertia_jb.rb +22 -0
- data/test/controller_test.rb +168 -0
- data/test/test_helper.rb +24 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5318dc498cde30ce2dfada1a46e68138c4a85dfef6a7cf275b411f861e059d67
|
|
4
|
+
data.tar.gz: abb2907bdaba0907f38376557c750e99d918988dc28e2b36e02bd209bbcedd37
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8039dfc4f8d98b88c328c622a007f9baf110bed9024b828443bac26ef12cc05853c5c59087b472f454de9dace0693c34cd864914822541f4be1879db76e7f500
|
|
7
|
+
data.tar.gz: 535c7d2247b8da621a0f55ab3973006b27a2dab97a0af71cfd2294bebd65b9a2406a5ab8ba6956c3097a5bbff85472e3c9b7db8a1f32b678ad1b8495d8a90168
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kikyous
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# InertiaJb
|
|
2
|
+
|
|
3
|
+
InertiaJb lets you declare [Inertia.js](https://inertiajs.com/) props
|
|
4
|
+
for your Rails frontend components inside **view templates**, using plain Ruby
|
|
5
|
+
Hashes powered by [jb](https://github.com/amatsuda/jb).
|
|
6
|
+
|
|
7
|
+
It is built on top of the [inertia-rails](https://github.com/inertiajs/inertia-rails) gem.
|
|
8
|
+
|
|
9
|
+
A `*.html.inertia` template is just Ruby code whose last expression is a Hash.
|
|
10
|
+
That Hash is handed straight to `InertiaRails::Renderer`, so **every Inertia
|
|
11
|
+
protocol feature works out of the box** — partial reloads (at any nesting
|
|
12
|
+
depth), optional / always / deferred / scroll props, prop merging, shared data,
|
|
13
|
+
and global key transforms.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# app/controllers/messages_controller.rb
|
|
17
|
+
class MessagesController < ApplicationController
|
|
18
|
+
# Shared props are merged in automatically.
|
|
19
|
+
inertia_share user: -> { Current.user }
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
@message = Message.find(params[:id])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# app/views/messages/show.html.inertia
|
|
29
|
+
{
|
|
30
|
+
message: {
|
|
31
|
+
content: @message.content,
|
|
32
|
+
author: {
|
|
33
|
+
name: @message.author.name,
|
|
34
|
+
email: @message.author.email,
|
|
35
|
+
url: url_for(@message.author, format: :json)
|
|
36
|
+
},
|
|
37
|
+
comments: render(partial: "comments/comment", collection: @message.comments)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```jsx
|
|
43
|
+
// app/javascript/pages/messages/show.jsx
|
|
44
|
+
export default function Message({ user, message }) {
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<p>Hi, {user.name}</p>
|
|
48
|
+
<p>{message.content}</p>
|
|
49
|
+
<a href={message.author.url}>
|
|
50
|
+
{message.author.name} <{message.author.email}>
|
|
51
|
+
</a>
|
|
52
|
+
{message.comments.map((c) => <Comment key={c.id} comment={c} />)}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why jb?
|
|
59
|
+
|
|
60
|
+
Inertia's server side is fundamentally about producing **a Hash of props** that
|
|
61
|
+
`inertia-rails` then resolves and serializes. jb templates *are* plain Ruby
|
|
62
|
+
Hashes, so there is zero impedance mismatch: no DSL to learn, no intermediate
|
|
63
|
+
representation, and the full power of Ruby for building collections and
|
|
64
|
+
conditionals.
|
|
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
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Add the gem to your Gemfile:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
gem "inertia_jb"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then, in your inertia-rails initializer, **disable `default_render`** so that a
|
|
79
|
+
normal implicit render falls through to your `.html.inertia` template:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# config/initializers/inertia_rails.rb
|
|
83
|
+
InertiaRails.configure do |config|
|
|
84
|
+
config.default_render = false
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You do **not** need to call `use_inertia_instance_props`.
|
|
89
|
+
|
|
90
|
+
## Templates and partials
|
|
91
|
+
|
|
92
|
+
- **Page templates** live at `app/views/<controller>/<action>.html.inertia` and
|
|
93
|
+
must return a Hash (your Inertia props).
|
|
94
|
+
- **Partials** are ordinary [jb](https://github.com/amatsuda/jb) partials named
|
|
95
|
+
`_name.html.jb`. They return a Hash, and compose naturally:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# app/views/messages/show.html.inertia
|
|
99
|
+
{
|
|
100
|
+
author: render(partial: "authors/author", object: @message.author),
|
|
101
|
+
comments: render(partial: "comments/comment", collection: @message.comments)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# app/views/authors/_author.html.jb
|
|
107
|
+
{ id: author.id, name: author.name }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`render(partial:, collection:)` returns an **array of Hashes** (thanks to jb),
|
|
111
|
+
which you embed directly. Don't name partials `.html.inertia` — that extension
|
|
112
|
+
triggers the Inertia response wrapper and is only for top-level page templates.
|
|
113
|
+
|
|
114
|
+
## Inertia prop types
|
|
115
|
+
|
|
116
|
+
Because props are just a Hash, Inertia's special prop types are plain values you
|
|
117
|
+
drop in. Inside a `.html.inertia` template you can use the short helpers
|
|
118
|
+
(`optional`, `always`, `defer`, `scroll`, `merge`, `deep_merge`) or the full
|
|
119
|
+
`InertiaRails.*` methods.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
{
|
|
123
|
+
id: @post.id,
|
|
124
|
+
title: @post.title,
|
|
125
|
+
|
|
126
|
+
# Only fetched when explicitly requested in a partial reload.
|
|
127
|
+
stats: optional { @post.expensive_stats },
|
|
128
|
+
|
|
129
|
+
# Always fetched, even if not requested in a partial reload.
|
|
130
|
+
settings: always { current_settings },
|
|
131
|
+
|
|
132
|
+
# Excluded from the initial load; fetched in a follow-up request.
|
|
133
|
+
comments: defer(group: :comments) {
|
|
134
|
+
render(partial: "comments/comment", collection: @post.comments)
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
# Infinite scrolling (accepts a paginator or explicit metadata).
|
|
138
|
+
feed: scroll(@pagy) {
|
|
139
|
+
render(partial: "feed/item", collection: @items)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
See the inertia-rails docs for [partial reloads](https://inertiajs.com/partial-reloads),
|
|
145
|
+
[deferred props](https://inertiajs.com/deferred-props), and
|
|
146
|
+
[infinite scroll](https://inertia-rails.dev/guide/infinite-scroll).
|
|
147
|
+
|
|
148
|
+
## Caching
|
|
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
|
+
```
|
|
157
|
+
|
|
158
|
+
## camelCase keys
|
|
159
|
+
|
|
160
|
+
If you configure inertia-rails' `prop_transformer` to camelize keys, it applies
|
|
161
|
+
at **every nesting depth** — because your props reach inertia-rails as a real
|
|
162
|
+
Hash:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# config/initializers/inertia_rails.rb
|
|
166
|
+
InertiaRails.configure do |config|
|
|
167
|
+
config.default_render = false
|
|
168
|
+
config.prop_transformer = ->(props:) { props.deep_transform_keys { |k| k.to_s.camelize(:lower) } }
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Alternatively, just write camelCase keys directly in your templates.
|
|
173
|
+
|
|
174
|
+
## Notes
|
|
175
|
+
|
|
176
|
+
- Inertia props must be an **object**, so page templates should return a Hash
|
|
177
|
+
(not a top-level Array).
|
|
178
|
+
- Don't install this alongside `inertia-builder`; both register an `:inertia`
|
|
179
|
+
template handler.
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bundle install
|
|
185
|
+
bundle exec rake test
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaJb
|
|
4
|
+
# Controller concern that makes `.html.inertia` templates behave like proper
|
|
5
|
+
# Inertia responses.
|
|
6
|
+
#
|
|
7
|
+
# Our {Renderer} only computes the page hash (it does not go through
|
|
8
|
+
# +InertiaRails::Renderer#render+), so we set the Inertia response headers and
|
|
9
|
+
# skip the layout for Inertia (XHR) requests here — mirroring inertia-builder.
|
|
10
|
+
#
|
|
11
|
+
# +InertiaRails::Controller+ (which provides +inertia_share+, shared data and
|
|
12
|
+
# +default_render+) is already mixed into +ActionController::Base+ by
|
|
13
|
+
# inertia-rails' engine, so we don't include it again.
|
|
14
|
+
module Controller
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
before_action :format_inertia_jb_response, if: -> { inertia_json_request? }
|
|
19
|
+
end
|
|
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"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaJb
|
|
4
|
+
# ActionView template handler for `*.html.inertia` templates.
|
|
5
|
+
#
|
|
6
|
+
# An `.html.inertia` template is jb-style Ruby: its last expression is a Hash
|
|
7
|
+
# of Inertia props. We evaluate it, then hand the resulting Hash to
|
|
8
|
+
# {Renderer}, whose return value becomes the rendered body.
|
|
9
|
+
#
|
|
10
|
+
# Unlike props_template, there is no shared builder state to guard: jb
|
|
11
|
+
# partials each return their own Hash independently, so no `@__json`-style
|
|
12
|
+
# finalize dance is needed here.
|
|
13
|
+
class Handler
|
|
14
|
+
def self.call(template, source = nil)
|
|
15
|
+
source ||= template.source
|
|
16
|
+
|
|
17
|
+
# `begin;#{source}` keeps the template's own line numbers aligned in
|
|
18
|
+
# backtraces. The begin/end block evaluates to the template's last
|
|
19
|
+
# expression (the props Hash).
|
|
20
|
+
"__inertia_props = begin;#{source}\nend\n" \
|
|
21
|
+
"::InertiaJb::Renderer.new(self, __inertia_props, true).render"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaJb
|
|
4
|
+
# Optional syntax sugar available inside `.html.inertia` templates, so you can
|
|
5
|
+
# write `optional { ... }` instead of `InertiaRails.optional { ... }`.
|
|
6
|
+
#
|
|
7
|
+
# These simply delegate to the +InertiaRails+ factory methods; the returned
|
|
8
|
+
# prop objects are resolved by +InertiaRails::PropsResolver+ when the page is
|
|
9
|
+
# built (respecting partial reloads, grouping, etc.).
|
|
10
|
+
module Helper
|
|
11
|
+
# Only fetched when explicitly requested in a partial reload.
|
|
12
|
+
def optional(&block)
|
|
13
|
+
::InertiaRails.optional(&block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Always fetched, even when not requested in a partial reload.
|
|
17
|
+
def always(&block)
|
|
18
|
+
::InertiaRails.always(&block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Excluded from the initial load; fetched in a follow-up request.
|
|
22
|
+
def defer(...)
|
|
23
|
+
::InertiaRails.defer(...)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Infinite-scroll prop (accepts a paginator or explicit metadata).
|
|
27
|
+
def scroll(...)
|
|
28
|
+
::InertiaRails.scroll(...)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Merged with existing client-side data instead of replacing it.
|
|
32
|
+
def merge(...)
|
|
33
|
+
::InertiaRails.merge(...)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Deep-merged with existing client-side data.
|
|
37
|
+
def deep_merge(...)
|
|
38
|
+
::InertiaRails.deep_merge(...)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module InertiaJb
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer :inertia_jb do
|
|
8
|
+
ActiveSupport.on_load(:action_controller) do
|
|
9
|
+
include InertiaJb::Controller
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
13
|
+
ActionView::Template.register_template_handler :inertia, InertiaJb::Handler
|
|
14
|
+
ActionView::Base.include InertiaJb::Helper
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaJb
|
|
4
|
+
# Bridges a props Hash (built by a `.html.inertia` template) to the Inertia
|
|
5
|
+
# protocol via +InertiaRails::Renderer+.
|
|
6
|
+
#
|
|
7
|
+
# Because the props are already a plain Ruby Hash, +InertiaRails::Renderer+
|
|
8
|
+
# can walk them natively: shared data is merged in, Procs and prop objects
|
|
9
|
+
# (optional/always/defer/scroll) are resolved, and partial reloads are
|
|
10
|
+
# filtered at any nesting depth.
|
|
11
|
+
class Renderer
|
|
12
|
+
def initialize(view_context, props, component)
|
|
13
|
+
@view_context = view_context
|
|
14
|
+
@inertia_renderer = ::InertiaRails::Renderer.new(
|
|
15
|
+
component,
|
|
16
|
+
view_context.controller,
|
|
17
|
+
view_context.request,
|
|
18
|
+
view_context.response,
|
|
19
|
+
view_context.controller.method(:render),
|
|
20
|
+
props: props
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render
|
|
25
|
+
# `page` computes the full Inertia page hash (running PropsResolver)
|
|
26
|
+
# without triggering a controller render, so we can return the body
|
|
27
|
+
# ourselves from inside the template rendering pipeline.
|
|
28
|
+
page = @inertia_renderer.send(:page)
|
|
29
|
+
|
|
30
|
+
if @view_context.request.headers["X-Inertia"]
|
|
31
|
+
page.to_json
|
|
32
|
+
else
|
|
33
|
+
@view_context.controller.render_to_string(
|
|
34
|
+
template: "inertia",
|
|
35
|
+
layout: false,
|
|
36
|
+
locals: { page: page }
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/inertia_jb.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jb"
|
|
4
|
+
require "inertia_rails"
|
|
5
|
+
|
|
6
|
+
require "inertia_jb/version"
|
|
7
|
+
require "inertia_jb/handler"
|
|
8
|
+
require "inertia_jb/renderer"
|
|
9
|
+
require "inertia_jb/controller"
|
|
10
|
+
require "inertia_jb/helper"
|
|
11
|
+
require "inertia_jb/railtie" if defined?(Rails::Railtie)
|
|
12
|
+
|
|
13
|
+
# InertiaJb lets you declare Inertia.js props for your Rails
|
|
14
|
+
# frontend components inside view templates, using plain Ruby Hashes powered by
|
|
15
|
+
# the {https://github.com/amatsuda/jb jb} renderer.
|
|
16
|
+
#
|
|
17
|
+
# A `*.html.inertia` template is Ruby code whose last expression is a Hash of
|
|
18
|
+
# props. That Hash is handed straight to +InertiaRails::Renderer+, so every
|
|
19
|
+
# Inertia protocol feature (partial reloads, optional/always/deferred props,
|
|
20
|
+
# merging, shared data, key transforms) works out of the box.
|
|
21
|
+
module InertiaJb
|
|
22
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "action_view/testing/resolvers"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
class TestController < ActionController::Base
|
|
8
|
+
include InertiaJb::Controller
|
|
9
|
+
|
|
10
|
+
layout "application"
|
|
11
|
+
|
|
12
|
+
# Shared props are merged in automatically by inertia-rails.
|
|
13
|
+
inertia_share user: -> { { name: "Alice" } }
|
|
14
|
+
|
|
15
|
+
def index; end
|
|
16
|
+
|
|
17
|
+
def nested; end
|
|
18
|
+
|
|
19
|
+
def collection
|
|
20
|
+
@products = [{ id: 10, name: "Keyboard" }, { id: 11, name: "Monitor" }]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def with_partial
|
|
24
|
+
@author = { id: 42, name: "John" }
|
|
25
|
+
@comments = [{ body: "first" }, { body: "second" }]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# optional (only on partial reload) + deferred (excluded on first load).
|
|
29
|
+
def special
|
|
30
|
+
@id = 1
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ControllerTest < ActionController::TestCase
|
|
35
|
+
tests TestController
|
|
36
|
+
|
|
37
|
+
TEMPLATES = {
|
|
38
|
+
"layouts/application.html.erb" => "<html><body><%= yield %></body></html>",
|
|
39
|
+
"test/index.html.inertia" => "{ content: 'content' }",
|
|
40
|
+
"test/nested.html.inertia" => "{ product: { id: 10, name: 'Keyboard', price: 29.99 } }",
|
|
41
|
+
"test/collection.html.inertia" => "{ products: @products.map { |p| { id: p[:id], name: p[:name] } } }",
|
|
42
|
+
"test/with_partial.html.inertia" =>
|
|
43
|
+
"{ author: render(partial: 'authors/author', object: @author), " \
|
|
44
|
+
"comments: render(partial: 'comments/comment', collection: @comments) }",
|
|
45
|
+
"authors/_author.html.jb" => "{ id: author[:id], name: author[:name] }",
|
|
46
|
+
"comments/_comment.html.jb" => "{ body: comment[:body] }",
|
|
47
|
+
"test/special.html.inertia" =>
|
|
48
|
+
"{ id: @id, stats: optional { { visits: 42 } }, feed: defer(group: :feed) { [1, 2, 3] } }"
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
def setup
|
|
52
|
+
super
|
|
53
|
+
|
|
54
|
+
@routes = ActionDispatch::Routing::RouteSet.new
|
|
55
|
+
@routes.draw do
|
|
56
|
+
%i[index nested collection with_partial special].each do |action|
|
|
57
|
+
get action.to_s => "test##{action}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
resolver = ActionView::FixtureResolver.new(TEMPLATES)
|
|
62
|
+
@controller.prepend_view_path(resolver)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def teardown
|
|
66
|
+
super
|
|
67
|
+
@routes.clear!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ---- basic rendering -----------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def test_non_inertia_request_renders_html_with_data_page
|
|
73
|
+
get :index
|
|
74
|
+
|
|
75
|
+
assert_response :success
|
|
76
|
+
assert_equal "text/html; charset=utf-8", response.content_type
|
|
77
|
+
assert_nil response.headers["X-Inertia"]
|
|
78
|
+
|
|
79
|
+
assert_includes response.body, "<html><body>"
|
|
80
|
+
assert_includes response.body, 'id="app"'
|
|
81
|
+
assert_includes response.body, "data-page"
|
|
82
|
+
# component + a prop value survive into the escaped data-page payload.
|
|
83
|
+
assert_includes response.body, "test/index"
|
|
84
|
+
assert_includes response.body, "content"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_inertia_request_returns_json
|
|
88
|
+
inertia_get :index
|
|
89
|
+
|
|
90
|
+
assert_response :success
|
|
91
|
+
assert_equal "true", response.headers["X-Inertia"]
|
|
92
|
+
assert_includes response.headers["Vary"].to_s, "X-Inertia"
|
|
93
|
+
|
|
94
|
+
page = JSON.parse(response.body)
|
|
95
|
+
assert_equal "test/index", page["component"]
|
|
96
|
+
assert_equal "content", page.dig("props", "content")
|
|
97
|
+
assert_equal({}, page.dig("props", "errors"))
|
|
98
|
+
assert_equal false, page["encryptHistory"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_shared_props_are_merged
|
|
102
|
+
inertia_get :index
|
|
103
|
+
page = JSON.parse(response.body)
|
|
104
|
+
assert_equal({ "name" => "Alice" }, page.dig("props", "user"))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_nested_props
|
|
108
|
+
inertia_get :nested
|
|
109
|
+
page = JSON.parse(response.body)
|
|
110
|
+
assert_equal({ "id" => 10, "name" => "Keyboard", "price" => 29.99 }, page.dig("props", "product"))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_collection_props
|
|
114
|
+
inertia_get :collection
|
|
115
|
+
page = JSON.parse(response.body)
|
|
116
|
+
assert_equal(
|
|
117
|
+
[{ "id" => 10, "name" => "Keyboard" }, { "id" => 11, "name" => "Monitor" }],
|
|
118
|
+
page.dig("props", "products")
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# ---- jb partials ---------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def test_object_and_collection_partials
|
|
125
|
+
inertia_get :with_partial
|
|
126
|
+
page = JSON.parse(response.body)
|
|
127
|
+
|
|
128
|
+
assert_equal({ "id" => 42, "name" => "John" }, page.dig("props", "author"))
|
|
129
|
+
assert_equal([{ "body" => "first" }, { "body" => "second" }], page.dig("props", "comments"))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ---- optional / deferred props ------------------------------------------
|
|
133
|
+
|
|
134
|
+
def test_optional_prop_excluded_on_full_visit
|
|
135
|
+
inertia_get :special
|
|
136
|
+
page = JSON.parse(response.body)
|
|
137
|
+
|
|
138
|
+
assert_equal 1, page.dig("props", "id")
|
|
139
|
+
refute page["props"].key?("stats"), "optional prop should be absent on a full visit"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_optional_prop_included_on_partial_reload
|
|
143
|
+
inertia_get :special,
|
|
144
|
+
headers: {
|
|
145
|
+
"X-Inertia-Partial-Component" => "test/special",
|
|
146
|
+
"X-Inertia-Partial-Data" => "stats"
|
|
147
|
+
}
|
|
148
|
+
page = JSON.parse(response.body)
|
|
149
|
+
|
|
150
|
+
assert_equal({ "visits" => 42 }, page.dig("props", "stats"))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_deferred_prop_is_listed_and_excluded_on_full_visit
|
|
154
|
+
inertia_get :special
|
|
155
|
+
page = JSON.parse(response.body)
|
|
156
|
+
|
|
157
|
+
refute page["props"].key?("feed"), "deferred prop should be absent on the initial load"
|
|
158
|
+
assert_equal({ "feed" => ["feed"] }, page["deferredProps"])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def inertia_get(action, headers: {})
|
|
164
|
+
headers.each { |k, v| @request.headers[k] = v }
|
|
165
|
+
@request.headers["X-Inertia"] = "true"
|
|
166
|
+
get action
|
|
167
|
+
end
|
|
168
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "action_controller/railtie"
|
|
5
|
+
|
|
6
|
+
require "inertia_rails"
|
|
7
|
+
require "inertia_jb"
|
|
8
|
+
require "active_support/testing/autorun"
|
|
9
|
+
|
|
10
|
+
ActiveSupport.test_order = :random
|
|
11
|
+
|
|
12
|
+
# Instantiate an Application in order to trigger the initializers (inertia-rails
|
|
13
|
+
# engine, jb railtie, and our own railtie that registers the :inertia handler).
|
|
14
|
+
Class.new(Rails::Application) do
|
|
15
|
+
config.secret_key_base = "secret"
|
|
16
|
+
config.eager_load = false
|
|
17
|
+
end.initialize!
|
|
18
|
+
|
|
19
|
+
InertiaRails.configure do |c|
|
|
20
|
+
c.always_include_errors_hash = true # comply with the Inertia protocol + silence deprecation
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Touch AV::Base so the :action_view on_load hooks (handler + helper) run before tests.
|
|
24
|
+
ActionView::Base.inspect
|
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inertia_jb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kikyous
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: inertia_rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.12'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.12'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: jb
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.8'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: appraisal
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.5'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.5'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
description: InertiaJb lets you declare Inertia.js props for your Rails frontend components
|
|
70
|
+
inside `.html.inertia` view templates using plain Ruby Hashes, powered by the jb
|
|
71
|
+
renderer. Built on top of inertia_rails.
|
|
72
|
+
email:
|
|
73
|
+
- kikyous@163.com
|
|
74
|
+
executables: []
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- LICENSE
|
|
79
|
+
- README.md
|
|
80
|
+
- lib/inertia_jb.rb
|
|
81
|
+
- lib/inertia_jb/controller.rb
|
|
82
|
+
- lib/inertia_jb/handler.rb
|
|
83
|
+
- lib/inertia_jb/helper.rb
|
|
84
|
+
- lib/inertia_jb/railtie.rb
|
|
85
|
+
- lib/inertia_jb/renderer.rb
|
|
86
|
+
- lib/inertia_jb/version.rb
|
|
87
|
+
- test/controller_test.rb
|
|
88
|
+
- test/test_helper.rb
|
|
89
|
+
homepage: https://github.com/kikyous/inertia_jb
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
bug_tracker_uri: https://github.com/kikyous/inertia_jb/issues
|
|
94
|
+
source_code_uri: https://github.com/kikyous/inertia_jb
|
|
95
|
+
rubygems_mfa_required: 'true'
|
|
96
|
+
post_install_message:
|
|
97
|
+
rdoc_options: []
|
|
98
|
+
require_paths:
|
|
99
|
+
- lib
|
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: 3.0.0
|
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
requirements: []
|
|
111
|
+
rubygems_version: 3.2.22
|
|
112
|
+
signing_key:
|
|
113
|
+
specification_version: 4
|
|
114
|
+
summary: Declare Inertia.js props in Rails view templates with plain Ruby Hashes (powered
|
|
115
|
+
by jb)
|
|
116
|
+
test_files:
|
|
117
|
+
- test/controller_test.rb
|
|
118
|
+
- test/test_helper.rb
|