inertia_rails 3.11.0 → 3.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 416af24e523dd32daacb768be83fc88bf5465a87adbf49e8105cb630409e45f9
4
- data.tar.gz: 4c1d7c883d6ffc3e3239777bd1b1f400f5a0f69c22093a307225d696551b2da5
3
+ metadata.gz: 95b5901a42397a1ae01678a1101d5d21a6a2f2f33c6afb413a2fbca48f04e064
4
+ data.tar.gz: a4d04ff25ce45b4392e5ad5c5153530446f83256e2b00452999da0c920751416
5
5
  SHA512:
6
- metadata.gz: 23f7eeab79b5e54cc12408f6196f946f5cb5ea78086d55ac56d06827ea0243ba96c90e432e7c69583f7531ae7cd2611a026fdce15039219c36f7ef9034888fa6
7
- data.tar.gz: c29f0e2d3c88ebc7a4cc6a405219152a3ae5f4428b23b8ec9a82de8e38e98a52c035d15389c51858b1a6534aae47adef1a26ebd179c4892c7a7b0b76427b88b0
6
+ metadata.gz: 222ebe8277d96856c05c9d712ad0d0e33cea995e7195dada790cf6423e3ad9f16b970c555c9889a1d7eb9082ad9045ca73a00bc95116fc93912f429fc1ded50f
7
+ data.tar.gz: f859bfb3d498ae8bc2c146e5bd8d9be8cadb65a6f3a51135c89bd1f3e86c83007f2602b9e6b115179e7f294dc663e3c96ce9073890511b478689ddf99156e1b9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.12.0] - 2025-11-08
8
+
9
+ * Docs updates (@leenyburger, @skryukov, @bn-l)
10
+ * Reimplement devcontainers (@kieraneglin)
11
+ * Support for Inertia.js infinite scroll components (@skyrukov)
12
+ * New merge options (@skryukov)
13
+
7
14
  ## [3.11.0] - 2025-08-29
8
15
 
9
16
  * Fix Svelte generator (@skryukov)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  class StaticController < InertiaRails.configuration.parent_controller.constantize
3
5
  def static
@@ -128,8 +128,12 @@ module Inertia
128
128
  add_dependencies(*FRAMEWORKS[framework]['packages_ts'])
129
129
 
130
130
  say 'Copying adding scripts to package.json'
131
- run 'npm pkg set scripts.check="svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"' if svelte?
132
- run 'npm pkg set scripts.check="vue-tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'vue'
131
+ if svelte?
132
+ run 'npm pkg set scripts.check="svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"'
133
+ end
134
+ if framework == 'vue'
135
+ run 'npm pkg set scripts.check="vue-tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"'
136
+ end
133
137
  run 'npm pkg set scripts.check="tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'react'
134
138
  end
135
139
 
@@ -262,8 +266,9 @@ module Inertia
262
266
  end
263
267
 
264
268
  def inertia_resolved_version
269
+ package = "@inertiajs/core@#{options[:inertia_version]}"
265
270
  @inertia_resolved_version ||= Gem::Version.new(
266
- `npm show @inertiajs/core@#{options[:inertia_version]} version --json | tail -n2 | head -n1 | tr -d '", '`.strip
271
+ `npm show #{package} version --json | tail -n2 | head -n1 | tr -d '", '`.strip
267
272
  )
268
273
  end
269
274
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # Based on AbstractController::Callbacks::ActionFilter
4
5
  # https://github.com/rails/rails/blob/v7.2.0/actionpack/lib/abstract_controller/callbacks.rb#L39
@@ -6,7 +7,7 @@ module InertiaRails
6
7
  class ActionFilter
7
8
  def initialize(conditional_key, actions)
8
9
  @conditional_key = conditional_key
9
- @actions = Array(actions).map(&:to_s).to_set
10
+ @actions = Array(actions).to_set(&:to_s)
10
11
  end
11
12
 
12
13
  def match?(controller)
@@ -1,7 +1,9 @@
1
- require_relative "inertia_rails"
2
- require_relative "helper"
3
- require_relative "action_filter"
4
- require_relative "meta_tag_builder"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'inertia_rails'
4
+ require_relative 'helper'
5
+ require_relative 'action_filter'
6
+ require_relative 'meta_tag_builder'
5
7
 
6
8
  module InertiaRails
7
9
  module Controller
@@ -21,8 +23,8 @@ module InertiaRails
21
23
  return push_to_inertia_share(**(hash || props), &block) if options.empty?
22
24
 
23
25
  push_to_inertia_share do
24
- next unless options[:if].all? { |filter| instance_exec(&filter) } if options[:if]
25
- next unless options[:unless].none? { |filter| instance_exec(&filter) } if options[:unless]
26
+ next if options[:if] && !options[:if].all? { |filter| instance_exec(&filter) }
27
+ next if options[:unless]&.any? { |filter| instance_exec(&filter) }
26
28
 
27
29
  next hash unless block
28
30
 
@@ -81,7 +83,9 @@ module InertiaRails
81
83
  return options if options.empty?
82
84
 
83
85
  if props.except(:if, :unless, :only, :except).any?
84
- raise ArgumentError, "You must not mix shared data and [:if, :unless, :only, :except] options, pass data as a hash or a block."
86
+ raise ArgumentError,
87
+ 'You must not mix shared data and [:if, :unless, :only, :except] options, ' \
88
+ 'pass data as a hash or a block.'
85
89
  end
86
90
 
87
91
  transform_inertia_share_option(options, :only, :if)
@@ -110,7 +114,7 @@ module InertiaRails
110
114
  when InertiaRails::ActionFilter
111
115
  -> { filter.match?(self) }
112
116
  else
113
- raise ArgumentError, "You must pass a symbol or a proc as a filter."
117
+ raise ArgumentError, 'You must pass a symbol or a proc as a filter.'
114
118
  end
115
119
  end
116
120
  end
@@ -136,6 +140,7 @@ module InertiaRails
136
140
 
137
141
  def inertia_view_assigns
138
142
  return {} unless @_inertia_instance_props
143
+
139
144
  view_assigns.except(*@_inertia_skip_props)
140
145
  end
141
146
 
@@ -152,22 +157,22 @@ module InertiaRails
152
157
  else
153
158
  if inertia_configuration.always_include_errors_hash.nil?
154
159
  InertiaRails.deprecator.warn(
155
- "To comply with the Inertia protocol, an empty errors hash `{errors: {}}` " \
156
- "will be included to all responses by default starting with InertiaRails 4.0. " \
157
- "To opt-in now, set `config.always_include_errors_hash = true`. " \
158
- "To disable this warning, set it to `false`."
160
+ 'To comply with the Inertia protocol, an empty errors hash `{errors: {}}` ' \
161
+ 'will be included to all responses by default starting with InertiaRails 4.0. ' \
162
+ 'To opt-in now, set `config.always_include_errors_hash = true`. ' \
163
+ 'To disable this warning, set it to `false`.'
159
164
  )
160
165
  end
161
166
  {}
162
167
  end
163
168
 
164
- self.class._inertia_shared_data.filter_map { |shared_data|
169
+ self.class._inertia_shared_data.filter_map do |shared_data|
165
170
  if shared_data.respond_to?(:call)
166
171
  instance_exec(&shared_data)
167
172
  else
168
173
  shared_data
169
174
  end
170
- }.reduce(initial_data, &:merge)
175
+ end.reduce(initial_data, &:merge)
171
176
  end
172
177
 
173
178
  def inertia_location(url)
@@ -183,7 +188,7 @@ module InertiaRails
183
188
  session[:inertia_errors] = inertia_errors.to_hash
184
189
  else
185
190
  InertiaRails.deprecator.warn(
186
- "Object passed to `inertia: { errors: ... }` must respond to `to_hash`. Pass a hash-like object instead."
191
+ 'Object passed to `inertia: { errors: ... }` must respond to `to_hash`. Pass a hash-like object instead.'
187
192
  )
188
193
  session[:inertia_errors] = inertia_errors
189
194
  end
@@ -2,27 +2,16 @@
2
2
 
3
3
  module InertiaRails
4
4
  class DeferProp < IgnoreOnFirstLoadProp
5
- DEFAULT_GROUP = 'default'
5
+ prepend PropMergeable
6
6
 
7
- attr_reader :group, :match_on
7
+ DEFAULT_GROUP = 'default'
8
8
 
9
- def initialize(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
10
- raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge
9
+ attr_reader :group
11
10
 
11
+ def initialize(**props, &block)
12
12
  super(&block)
13
13
 
14
- @group = group || DEFAULT_GROUP
15
- @merge = merge || deep_merge
16
- @deep_merge = deep_merge
17
- @match_on = match_on.nil? ? nil : Array(match_on)
18
- end
19
-
20
- def merge?
21
- @merge
22
- end
23
-
24
- def deep_merge?
25
- @deep_merge
14
+ @group = props[:group] || DEFAULT_GROUP
26
15
  end
27
16
  end
28
17
  end
@@ -1,13 +1,15 @@
1
- require_relative "middleware"
2
- require_relative "controller"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middleware'
4
+ require_relative 'controller'
3
5
 
4
6
  module InertiaRails
5
7
  class Engine < ::Rails::Engine
6
- initializer "inertia_rails.configure_rails_initialization" do |app|
8
+ initializer 'inertia_rails.configure_rails_initialization' do |app|
7
9
  app.middleware.use ::InertiaRails::Middleware
8
10
  end
9
11
 
10
- initializer "inertia_rails.action_controller" do
12
+ initializer 'inertia_rails.action_controller' do
11
13
  ActiveSupport.on_load(:action_controller_base) do
12
14
  include ::InertiaRails::Controller
13
15
  end
@@ -1,32 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'inertia_rails'
2
4
 
3
- module InertiaRails::Helper
4
- def inertia_ssr_head
5
- controller.instance_variable_get("@_inertia_ssr_head")
6
- end
5
+ module InertiaRails
6
+ module Helper
7
+ def inertia_ssr_head
8
+ controller.instance_variable_get('@_inertia_ssr_head')
9
+ end
7
10
 
8
- def inertia_headers
9
- InertiaRails.deprecator.warn(
10
- "`inertia_headers` is deprecated and will be removed in InertiaRails 4.0, use `inertia_ssr_head` instead."
11
- )
12
- inertia_ssr_head
13
- end
11
+ def inertia_headers
12
+ InertiaRails.deprecator.warn(
13
+ '`inertia_headers` is deprecated and will be removed in InertiaRails 4.0, use `inertia_ssr_head` instead.'
14
+ )
15
+ inertia_ssr_head
16
+ end
14
17
 
15
- def inertia_rendering?
16
- controller.instance_variable_get("@_inertia_rendering")
17
- end
18
+ def inertia_rendering?
19
+ controller.instance_variable_get('@_inertia_rendering')
20
+ end
18
21
 
19
- def inertia_page
20
- controller.instance_variable_get("@_inertia_page")
21
- end
22
+ def inertia_page
23
+ controller.instance_variable_get('@_inertia_page')
24
+ end
22
25
 
23
- def inertia_meta_tags
24
- meta_tag_data = (inertia_page || {}).dig(:props, :_inertia_meta) || []
26
+ def inertia_meta_tags
27
+ meta_tag_data = (inertia_page || {}).dig(:props, :_inertia_meta) || []
25
28
 
26
- meta_tags = meta_tag_data.map do |inertia_meta_tag|
27
- inertia_meta_tag.to_tag(tag)
28
- end
29
+ meta_tags = meta_tag_data.map do |inertia_meta_tag|
30
+ inertia_meta_tag.to_tag(tag)
31
+ end
29
32
 
30
- safe_join(meta_tags, "\n")
33
+ safe_join(meta_tags, "\n")
34
+ end
31
35
  end
32
36
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'inertia_rails/prop_mergeable'
3
4
  require 'inertia_rails/base_prop'
4
5
  require 'inertia_rails/ignore_on_first_load_prop'
5
6
  require 'inertia_rails/always_prop'
@@ -7,6 +8,7 @@ require 'inertia_rails/lazy_prop'
7
8
  require 'inertia_rails/optional_prop'
8
9
  require 'inertia_rails/defer_prop'
9
10
  require 'inertia_rails/merge_prop'
11
+ require 'inertia_rails/scroll_prop'
10
12
  require 'inertia_rails/configuration'
11
13
  require 'inertia_rails/meta_tag'
12
14
 
@@ -34,8 +36,8 @@ module InertiaRails
34
36
  AlwaysProp.new(&block)
35
37
  end
36
38
 
37
- def merge(match_on: nil, &block)
38
- MergeProp.new(match_on: match_on, &block)
39
+ def merge(...)
40
+ MergeProp.new(...)
39
41
  end
40
42
 
41
43
  def deep_merge(match_on: nil, &block)
@@ -45,5 +47,9 @@ module InertiaRails
45
47
  def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
46
48
  DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
47
49
  end
50
+
51
+ def scroll(metadata = nil, **options, &block)
52
+ ScrollProp.new(metadata: metadata, **options, &block)
53
+ end
48
54
  end
49
55
  end
@@ -10,7 +10,7 @@ module InertiaRails
10
10
  )
11
11
 
12
12
  @value = value
13
- @block = block
13
+ super(&block)
14
14
  end
15
15
 
16
16
  def call(controller)
@@ -2,20 +2,11 @@
2
2
 
3
3
  module InertiaRails
4
4
  class MergeProp < BaseProp
5
- attr_reader :match_on
5
+ prepend PropMergeable
6
6
 
7
- def initialize(deep_merge: false, match_on: nil, &block)
7
+ def initialize(**_props, &block)
8
8
  super(&block)
9
- @deep_merge = deep_merge
10
- @match_on = match_on.nil? ? nil : Array(match_on)
11
- end
12
-
13
- def merge?
14
- true
15
- end
16
-
17
- def deep_merge?
18
- @deep_merge
9
+ @merge = true
19
10
  end
20
11
  end
21
12
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ module PropMergeable
5
+ attr_reader :match_on, :appends_at_paths, :prepends_at_paths
6
+
7
+ def initialize(**props, &block)
8
+ raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if props[:deep_merge] && props[:merge]
9
+
10
+ @deep_merge = props.fetch(:deep_merge, false)
11
+ @merge = props[:merge] || @deep_merge
12
+ @match_on = props[:match_on].nil? ? nil : Array(props[:match_on])
13
+ @appends_at_paths = []
14
+ @prepends_at_paths = []
15
+ @append = true
16
+
17
+ append(props[:append]) if props.key?(:append)
18
+ prepend(props[:prepend]) if props.key?(:prepend)
19
+
20
+ super
21
+ end
22
+
23
+ def appends_at_root?
24
+ @append && merges_at_root?
25
+ end
26
+
27
+ def prepends_at_root?
28
+ !@append && merges_at_root?
29
+ end
30
+
31
+ def merges_at_root?
32
+ merge? && appends_at_paths.none? && prepends_at_paths.none?
33
+ end
34
+
35
+ def merge?
36
+ @merge
37
+ end
38
+
39
+ def deep_merge?
40
+ @deep_merge
41
+ end
42
+
43
+ private
44
+
45
+ def append(path, match_on: nil)
46
+ case path
47
+ when TrueClass, FalseClass
48
+ @append = path
49
+ when String
50
+ @appends_at_paths << path
51
+ when Array
52
+ @appends_at_paths += path
53
+ when Hash
54
+ @match_on ||= []
55
+ path.each do |key, value|
56
+ @appends_at_paths << key.to_s
57
+ @match_on << "#{key}.#{value}" if value
58
+ end
59
+ end
60
+
61
+ (@match_on ||= []) << "#{path}.#{match_on}" if match_on && path.is_a?(String)
62
+ end
63
+
64
+ def prepend(path, match_on: nil)
65
+ case path
66
+ when TrueClass, FalseClass
67
+ @append = !path
68
+ when String
69
+ @prepends_at_paths << path
70
+ when Array
71
+ @prepends_at_paths += path
72
+ when Hash
73
+ @match_on ||= []
74
+ path.each do |key, value|
75
+ @prepends_at_paths << key.to_s
76
+ @match_on << "#{key}.#{value}" if value
77
+ end
78
+ end
79
+
80
+ (@match_on ||= []) << "#{path}.#{match_on}" if match_on && path.is_a?(String)
81
+ end
82
+ end
83
+ end
@@ -101,13 +101,16 @@ module InertiaRails
101
101
  end
102
102
  .then { |props| deep_transform_props(props) } # Internal hydration/filtering
103
103
  .then { |props| configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
104
- .tap { |props| props[:_inertia_meta] = meta_tags if meta_tags.present? } # Add meta tags last (never transformed)
105
-
104
+ .tap do |props| # Add meta tags last (never transformed)
105
+ props[:_inertia_meta] = meta_tags if meta_tags.present?
106
+ end
106
107
  # rubocop:enable Style/MultilineBlockChain
107
108
  end
108
109
 
109
110
  def page
110
- default_page = {
111
+ return @page if defined?(@page)
112
+
113
+ @page = {
111
114
  component: component,
112
115
  props: computed_props,
113
116
  url: @request.original_fullpath,
@@ -117,21 +120,11 @@ module InertiaRails
117
120
  }
118
121
 
119
122
  deferred_props = deferred_props_keys
120
- default_page[:deferredProps] = deferred_props if deferred_props.present?
123
+ @page[:deferredProps] = deferred_props if deferred_props.present?
124
+ @page[:scrollProps] = scroll_props if scroll_props.present?
125
+ @page.merge!(resolve_merge_props)
121
126
 
122
- deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
123
- prop.deep_merge?
124
- end
125
-
126
- match_props_on = all_merge_props.filter_map do |key, prop|
127
- prop.match_on.map { |ms| "#{key}.#{ms}" } if prop.match_on.present?
128
- end.flatten
129
-
130
- default_page[:mergeProps] = merge_props.map(&:first) if merge_props.present?
131
- default_page[:deepMergeProps] = deep_merge_props.map(&:first) if deep_merge_props.present?
132
- default_page[:matchPropsOn] = match_props_on if match_props_on.present?
133
-
134
- default_page
127
+ @page
135
128
  end
136
129
 
137
130
  def deep_transform_props(props, parent_path = [])
@@ -163,10 +156,28 @@ module InertiaRails
163
156
  end
164
157
  end
165
158
 
166
- def all_merge_props
167
- @all_merge_props ||= @props.select do |key, prop|
159
+ def resolve_merge_props
160
+ deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
161
+ prop.deep_merge?
162
+ end
163
+
164
+ {
165
+ mergeProps: append_merge_props(merge_props),
166
+ prependProps: prepend_merge_props(merge_props),
167
+ deepMergeProps: deep_merge_props.map!(&:first),
168
+ matchPropsOn: resolve_match_on_props,
169
+ }.delete_if { |_, v| v.blank? }
170
+ end
171
+
172
+ def resolve_match_on_props
173
+ all_merge_props.filter_map do |key, prop|
174
+ prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
175
+ end.flatten
176
+ end
177
+
178
+ def requested_merge_props
179
+ @requested_merge_props ||= @props.select do |key, prop|
168
180
  next unless prop.try(:merge?)
169
- next if reset_keys.include?(key)
170
181
  next if rendering_partial_component? && (
171
182
  (partial_keys.present? && partial_keys.exclude?(key.name)) ||
172
183
  (partial_except_keys.present? && partial_except_keys.include?(key.name))
@@ -176,16 +187,64 @@ module InertiaRails
176
187
  end
177
188
  end
178
189
 
190
+ def append_merge_props(props)
191
+ return props if props.empty?
192
+
193
+ root_append_props, nested_append_props = props.partition { |_key, prop| prop.appends_at_root? }
194
+
195
+ result = Set.new(root_append_props.map!(&:first))
196
+
197
+ nested_append_props.each do |key, prop|
198
+ prop.appends_at_paths.each do |path|
199
+ result.add("#{key}.#{path}")
200
+ end
201
+ end
202
+
203
+ result.to_a
204
+ end
205
+
206
+ def prepend_merge_props(props)
207
+ return props if props.empty?
208
+
209
+ root_prepend_props, nested_prepend_props = props.partition { |_key, prop| prop.prepends_at_root? }
210
+
211
+ result = Set.new(root_prepend_props.map!(&:first))
212
+
213
+ nested_prepend_props.each do |key, prop|
214
+ prop.prepends_at_paths.each do |path|
215
+ result.add("#{key}.#{path}")
216
+ end
217
+ end
218
+
219
+ result.to_a
220
+ end
221
+
222
+ def scroll_props
223
+ return @scroll_props if defined?(@scroll_props)
224
+
225
+ @scroll_props = {}
226
+ requested_merge_props.each do |key, prop|
227
+ next unless prop.is_a?(ScrollProp)
228
+
229
+ @scroll_props[key] = prop.metadata.merge!(reset: reset_keys.include?(key))
230
+ end
231
+ @scroll_props
232
+ end
233
+
234
+ def all_merge_props
235
+ @all_merge_props ||= requested_merge_props.reject { |key,| reset_keys.include?(key) }
236
+ end
237
+
179
238
  def partial_keys
180
- @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact
239
+ @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact_blank!
181
240
  end
182
241
 
183
242
  def reset_keys
184
- (@request.headers['X-Inertia-Reset'] || '').split(',').compact.map(&:to_sym)
243
+ @reset_keys ||= (@request.headers['X-Inertia-Reset'] || '').split(',').compact_blank!.map!(&:to_sym)
185
244
  end
186
245
 
187
246
  def partial_except_keys
188
- (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact
247
+ @partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
189
248
  end
190
249
 
191
250
  def rendering_partial_component?
@@ -1,30 +1,32 @@
1
- require "rspec/core"
2
- require "rspec/matchers"
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/matchers'
3
5
 
4
6
  module InertiaRails
5
7
  module RSpec
6
8
  class InertiaRenderWrapper
7
9
  attr_reader :view_data, :props, :component
8
-
10
+
9
11
  def initialize
10
12
  @view_data = nil
11
13
  @props = nil
12
14
  @component = nil
13
15
  end
14
-
16
+
15
17
  def call(params)
16
- set_values(params)
18
+ assign_locals(params)
17
19
  @render_method&.call(params)
18
20
  end
19
-
21
+
20
22
  def wrap_render(render_method)
21
23
  @render_method = render_method
22
24
  self
23
25
  end
24
-
26
+
25
27
  protected
26
-
27
- def set_values(params)
28
+
29
+ def assign_locals(params)
28
30
  if params[:locals].present?
29
31
  @view_data = params[:locals].except(:page)
30
32
  @props = params[:locals][:page][:props]
@@ -33,18 +35,24 @@ module InertiaRails
33
35
  # Sequential Inertia request
34
36
  @view_data = {}
35
37
  json = JSON.parse(params[:json])
36
- @props = json["props"]
37
- @component = json["component"]
38
+ @props = json['props']
39
+ @component = json['component']
38
40
  end
39
41
  end
40
42
  end
41
43
 
42
44
  module Helpers
43
45
  def inertia
44
- raise 'Inertia test helpers aren\'t set up! Make sure you add inertia: true to describe blocks using inertia tests.' unless inertia_tests_setup?
46
+ unless inertia_tests_setup?
47
+ raise "Inertia test helpers aren't set up! " \
48
+ 'Make sure you add `inertia: true` to describe blocks using inertia tests.'
49
+ end
45
50
 
46
51
  if @_inertia_render_wrapper.nil? && !::RSpec.configuration.inertia[:skip_missing_renderer_warnings]
47
- warn 'WARNING: the test never created an Inertia renderer. Maybe the code wasn\'t able to reach a `render inertia:` call? If this was intended, or you don\'t want to see this message, set ::RSpec.configuration.inertia[:skip_missing_renderer_warnings] = true'
52
+ warn 'WARNING: the test never created an Inertia renderer. ' \
53
+ "Maybe the code wasn't able to reach a `render inertia:` call? If this was intended, " \
54
+ "or you don't want to see this message, " \
55
+ 'set ::RSpec.configuration.inertia[:skip_missing_renderer_warnings] = true'
48
56
  end
49
57
  @_inertia_render_wrapper
50
58
  end
@@ -57,8 +65,8 @@ module InertiaRails
57
65
  @_inertia_render_wrapper = InertiaRenderWrapper.new.wrap_render(render)
58
66
  end
59
67
 
60
- protected
61
-
68
+ protected
69
+
62
70
  def inertia_tests_setup?
63
71
  ::RSpec.current_example.metadata.fetch(:inertia, false)
64
72
  end
@@ -67,9 +75,9 @@ module InertiaRails
67
75
  end
68
76
 
69
77
  RSpec.configure do |config|
70
- config.include ::InertiaRails::RSpec::Helpers
78
+ config.include InertiaRails::RSpec::Helpers
71
79
  config.add_setting :inertia, default: {
72
- skip_missing_renderer_warnings: false
80
+ skip_missing_renderer_warnings: false,
73
81
  }
74
82
 
75
83
  config.before(:each, inertia: true) do
@@ -106,13 +114,14 @@ RSpec::Matchers.define :render_component do |expected_component|
106
114
  end
107
115
 
108
116
  failure_message do |inertia|
109
- "expected rendered inertia component to be #{expected_component}, instead received #{inertia.component || 'nothing'}"
117
+ "expected rendered inertia component to be #{expected_component}, " \
118
+ "instead received #{inertia.component || 'nothing'}"
110
119
  end
111
120
  end
112
121
 
113
122
  RSpec::Matchers.define :have_exact_view_data do |expected_view_data|
114
123
  match do |inertia|
115
- expect(inertia.view_data).to eq expected_view_data
124
+ expect(inertia.view_data).to eq expected_view_data
116
125
  end
117
126
 
118
127
  failure_message do |inertia|
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ module ScrollMetadata
5
+ class MissingMetadataAdapterError < StandardError; end
6
+
7
+ class Props
8
+ def initialize(page_name:, previous_page:, next_page:, current_page:)
9
+ @page_name = page_name
10
+ @previous_page = previous_page
11
+ @next_page = next_page
12
+ @current_page = current_page
13
+ end
14
+
15
+ def as_json(_options = nil)
16
+ {
17
+ pageName: @page_name,
18
+ previousPage: @previous_page,
19
+ nextPage: @next_page,
20
+ currentPage: @current_page,
21
+ }
22
+ end
23
+ end
24
+
25
+ class KaminariAdapter
26
+ def match?(metadata)
27
+ defined?(Kaminari) && metadata.is_a?(Kaminari::PageScopeMethods)
28
+ end
29
+
30
+ def call(metadata, **_options)
31
+ {
32
+ page_name: (Kaminari.config.param_name || 'page').to_s,
33
+ previous_page: metadata.prev_page,
34
+ next_page: metadata.next_page,
35
+ current_page: metadata.current_page,
36
+ }
37
+ end
38
+ end
39
+
40
+ class PagyAdapter
41
+ def match?(metadata)
42
+ defined?(Pagy) && metadata.is_a?(Pagy)
43
+ end
44
+
45
+ def call(metadata, **_options)
46
+ page_name = metadata.respond_to?(:vars) ? metadata.vars.fetch(:page_param) : metadata.options[:page_key]
47
+ {
48
+ page_name: page_name.to_s,
49
+ previous_page: metadata.try(:prev) || metadata.try(:previous),
50
+ next_page: metadata.next,
51
+ current_page: metadata.page,
52
+ }
53
+ end
54
+ end
55
+
56
+ class HashAdapter
57
+ def match?(metadata)
58
+ metadata.is_a?(Hash)
59
+ end
60
+
61
+ def call(metadata, **_options)
62
+ {
63
+ page_name: metadata.fetch(:page_name),
64
+ previous_page: metadata.fetch(:previous_page),
65
+ next_page: metadata.fetch(:next_page),
66
+ current_page: metadata.fetch(:current_page),
67
+ }
68
+ end
69
+ end
70
+
71
+ class << self
72
+ attr_accessor :adapters
73
+
74
+ def extract(metadata, **options)
75
+ overrides = options.slice(:page_name, :previous_page, :next_page, :current_page)
76
+
77
+ adapters.each do |adapter|
78
+ next unless adapter.match?(metadata)
79
+
80
+ return Props.new(**adapter.call(metadata, **options).merge!(overrides)).as_json
81
+ end
82
+
83
+ begin
84
+ Props.new(**overrides).as_json
85
+ rescue ArgumentError
86
+ raise MissingMetadataAdapterError, "No ScrollMetadata adapter found for #{metadata}"
87
+ end
88
+ end
89
+
90
+ def register_adapter(adapter)
91
+ adapters.unshift(adapter.new)
92
+ end
93
+ end
94
+
95
+ self.adapters = [KaminariAdapter, PagyAdapter, HashAdapter].map(&:new)
96
+ end
97
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scroll_metadata'
4
+
5
+ module InertiaRails
6
+ class ScrollProp < BaseProp
7
+ prepend PropMergeable
8
+
9
+ def initialize(**options, &block)
10
+ super(&block)
11
+
12
+ @merge = true
13
+ @metadata = options.delete(:metadata)
14
+ @wrapper = options.delete(:wrapper)
15
+
16
+ @options = options
17
+ end
18
+
19
+ def call(controller)
20
+ @value = super
21
+ configure_merge_intent(controller.request.headers['X-Inertia-Infinite-Scroll-Merge-Intent'])
22
+ @value
23
+ end
24
+
25
+ def metadata
26
+ ScrollMetadata.extract(@metadata, **@options)
27
+ end
28
+
29
+ private
30
+
31
+ def configure_merge_intent(scroll_intent)
32
+ scroll_intent == 'prepend' ? prepend(@wrapper || true) : append(@wrapper || true)
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
- VERSION = "3.11.0"
4
+ VERSION = '3.12.0'
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Patch BetterErrors::Middleware to render HTML for Inertia requests
2
4
  #
3
5
  # Original source:
@@ -7,13 +9,11 @@
7
9
  module InertiaRails
8
10
  module InertiaBetterErrors
9
11
  def text?(env)
10
- return false if env["HTTP_X_INERTIA"]
12
+ return false if env['HTTP_X_INERTIA']
11
13
 
12
14
  super
13
15
  end
14
16
  end
15
17
  end
16
18
 
17
- if defined?(BetterErrors)
18
- BetterErrors::Middleware.include InertiaRails::InertiaBetterErrors
19
- end
19
+ BetterErrors::Middleware.include InertiaRails::InertiaBetterErrors if defined?(BetterErrors)
@@ -1,17 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
4
  #
3
- # Rails has introduced text rendering for XHR requests with Rails 4.1 and
4
- # changed the implementation in 4.2, 5.0 and 5.1 (unchanged since then).
5
- #
6
5
  # The original source needs to be patched, so that Inertia requests are
7
6
  # NOT responded with plain text, but with HTML.
7
+ #
8
+ # Original source (unchanged since Rails 5.1):
9
+ # https://github.com/rails/rails/blob/5-1-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
10
+ # https://github.com/rails/rails/blob/8-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
11
+ #
8
12
 
9
- if defined?(ActionDispatch::DebugExceptions)
10
- if ActionPack.version.to_s >= '5.1'
11
- require 'patches/debug_exceptions/patch-5-1'
12
- elsif ActionPack.version.to_s >= '5.0'
13
- require 'patches/debug_exceptions/patch-5-0'
14
- else
15
- # This gem supports Rails 5 or later
13
+ module InertiaRails
14
+ module InertiaDebugExceptions
15
+ def render_for_browser_request(request, wrapper)
16
+ template = create_template(request, wrapper)
17
+ file = "rescues/#{wrapper.rescue_template}"
18
+
19
+ if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
20
+ body = template.render(template: file, layout: false, formats: [:text])
21
+ format = 'text/plain'
22
+ else
23
+ body = template.render(template: file, layout: 'rescues/layout')
24
+ format = 'text/html'
25
+ end
26
+
27
+ render(wrapper.status_code, body, format)
28
+ end
16
29
  end
17
30
  end
31
+
32
+ if defined?(ActionDispatch::DebugExceptions)
33
+ ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
34
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  module InertiaMapper
3
5
  def inertia(*args, **options)
@@ -12,7 +14,10 @@ module InertiaRails
12
14
  if path.is_a?(Hash)
13
15
  path.first
14
16
  elsif resource_scope?
15
- [path, InertiaRails.configuration.component_path_resolver(path: [@scope[:module], @scope[:controller]].compact.join('/'), action: path)]
17
+ [path,
18
+ InertiaRails.configuration.component_path_resolver(
19
+ path: [@scope[:module], @scope[:controller]].compact.join('/'), action: path
20
+ )]
16
21
  elsif @scope[:module].blank?
17
22
  [path, path]
18
23
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InertiaRails
2
4
  module InertiaRequest
3
5
  def inertia?
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :inertia_rails do
2
4
  namespace :install do
3
- desc "Installs inertia_rails packages and configurations for a React based app"
4
- task :react => :environment do
5
+ desc 'Installs inertia_rails packages and configurations for a React based app'
6
+ task react: :environment do
5
7
  system 'rails g inertia_rails:install --front_end react'
6
8
  end
7
- desc "Installs inertia_rails packages and configurations for a Vue based app"
9
+ desc 'Installs inertia_rails packages and configurations for a Vue based app'
8
10
  task vue: :environment do
9
11
  system 'rails g inertia_rails:install --front_end vue'
10
12
  end
11
- desc "Installs inertia_rails packages and configurations for a Svelte based app"
13
+ desc 'Installs inertia_rails packages and configurations for a Svelte based app'
12
14
  task svelte: :environment do
13
15
  system 'rails g inertia_rails:install --front_end svelte'
14
16
  end
15
17
  end
16
- end
18
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inertia_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.0
4
+ version: 3.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Knoles
@@ -9,7 +9,7 @@ authors:
9
9
  - Eugene Granovsky
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-08-29 00:00:00.000000000 Z
12
+ date: 2025-11-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -229,13 +229,14 @@ files:
229
229
  - lib/inertia_rails/meta_tag_builder.rb
230
230
  - lib/inertia_rails/middleware.rb
231
231
  - lib/inertia_rails/optional_prop.rb
232
+ - lib/inertia_rails/prop_mergeable.rb
232
233
  - lib/inertia_rails/renderer.rb
233
234
  - lib/inertia_rails/rspec.rb
235
+ - lib/inertia_rails/scroll_metadata.rb
236
+ - lib/inertia_rails/scroll_prop.rb
234
237
  - lib/inertia_rails/version.rb
235
238
  - lib/patches/better_errors.rb
236
239
  - lib/patches/debug_exceptions.rb
237
- - lib/patches/debug_exceptions/patch-5-0.rb
238
- - lib/patches/debug_exceptions/patch-5-1.rb
239
240
  - lib/patches/mapper.rb
240
241
  - lib/patches/request.rb
241
242
  - lib/tasks/inertia_rails.rake
@@ -1,27 +0,0 @@
1
- # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
- #
3
- # Original source:
4
- # https://github.com/rails/rails/blob/5-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
5
- #
6
-
7
- module InertiaRails
8
- module InertiaDebugExceptions
9
- def render_for_default_application(request, wrapper)
10
- template = create_template(request, wrapper)
11
- file = "rescues/#{wrapper.rescue_template}"
12
-
13
- if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
14
- body = template.render(template: file, layout: false, formats: [:text])
15
- format = "text/plain"
16
- else
17
- body = template.render(template: file, layout: 'rescues/layout')
18
- format = "text/html"
19
- end
20
- render(wrapper.status_code, body, format)
21
- end
22
- end
23
- end
24
-
25
- if defined?(ActionDispatch::DebugExceptions)
26
- ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
27
- end
@@ -1,30 +0,0 @@
1
- # Patch ActionDispatch::DebugExceptions to render HTML for Inertia requests
2
- #
3
- # Original source (unchanged since Rails 5.1):
4
- # https://github.com/rails/rails/blob/5-1-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
5
- # https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
6
- # https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
7
- #
8
-
9
- module InertiaRails
10
- module InertiaDebugExceptions
11
- def render_for_browser_request(request, wrapper)
12
- template = create_template(request, wrapper)
13
- file = "rescues/#{wrapper.rescue_template}"
14
-
15
- if request.xhr? && !request.headers['X-Inertia'] # <<<< this line is changed only
16
- body = template.render(template: file, layout: false, formats: [:text])
17
- format = "text/plain"
18
- else
19
- body = template.render(template: file, layout: "rescues/layout")
20
- format = "text/html"
21
- end
22
-
23
- render(wrapper.status_code, body, format)
24
- end
25
- end
26
- end
27
-
28
- if defined?(ActionDispatch::DebugExceptions)
29
- ActionDispatch::DebugExceptions.prepend InertiaRails::InertiaDebugExceptions
30
- end