inertia_rails 3.17.0 → 3.18.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: daa51c630e98a180cf58db897801bccf070932b66f5959d3e1a14663b6cda4a1
4
- data.tar.gz: c4610fb28fd87ab63182715358000e417b24d436c8228d77e7cab5e023009c8f
3
+ metadata.gz: 9e041109440a74fee87c6aa2ec7a338db68e01e1c5160dee3e9e00364a55f504
4
+ data.tar.gz: e4e8fc4f115e2f7d84f8f39bfe90099ca5b17a2490cd97aa6c21282b112ef15c
5
5
  SHA512:
6
- metadata.gz: b174cdfe7024669af7a7b560effdfa5ad8931e1487d4cf77f72f33c616008c915931bd1e7f687018ddcb64aeea00bb3c534662c3567d98c42ceef2561a21868c
7
- data.tar.gz: e143611be5a920f9caa5bc6895fd6977ab638ea02cf81a30a522b53d1d2e638bc2c582eb4968315a64666e9db851a8b0ba398d6f0a85db25a98167963cf0e13a
6
+ metadata.gz: f399be828b7f2e01c5cb9dedb86d67d3a0fe278370846f2c70a37576246e96d8ea00395239152bd908aad238b9389e06c5349f6bbb51b68b1b131367fe51b317
7
+ data.tar.gz: 833c82fff42dce56d47e20dbfb32e9103a76dd889b921902f73f9f020369409bfcd81f8ddfecabe122b95983034d0c3eb2e24cf5a0e46e018a78b2aeb94f7b1e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ 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.18.0] - 2026-03-11
8
+
9
+ * New (super awesome) landing page design (seriously, go look at the animated graphic) (@skryukov)
10
+ * Convert `_inertia_meta` to Hash in props for testing (@greendrop)
11
+ * Make InertaRails.scroll props deferable (@skyrukov)
12
+ * Add `withAllErrors` as default (@skyrukov)
13
+ * Improved testing with `evaluate_optional_props` (@skyrukov)
14
+ * Add Railsified Precognition support (@skyrukov)
15
+
7
16
  ## [3.17.0] - 2026-02-04
8
17
 
9
18
  * Add support for use_data_inertia_head_attribute configuration option (@greendrop)
@@ -42,6 +42,7 @@ createInertiaApp({
42
42
  defaults: {
43
43
  form: {
44
44
  forceIndicesArrayFormatInFormData: false,
45
+ withAllErrors: true,
45
46
  },
46
47
  future: {
47
48
  useScriptElementForInitialPage: true,
@@ -42,6 +42,7 @@ void createInertiaApp({
42
42
  defaults: {
43
43
  form: {
44
44
  forceIndicesArrayFormatInFormData: false,
45
+ withAllErrors: true,
45
46
  },
46
47
  future: {
47
48
  useScriptElementForInitialPage: true,
@@ -40,6 +40,7 @@ createInertiaApp({
40
40
  defaults: {
41
41
  form: {
42
42
  forceIndicesArrayFormatInFormData: false,
43
+ withAllErrors: true,
43
44
  },
44
45
  future: {
45
46
  useScriptElementForInitialPage: true,
@@ -39,6 +39,7 @@ createInertiaApp({
39
39
  defaults: {
40
40
  form: {
41
41
  forceIndicesArrayFormatInFormData: false,
42
+ withAllErrors: true,
42
43
  },
43
44
  future: {
44
45
  useScriptElementForInitialPage: true,
@@ -47,6 +47,11 @@ module InertiaRails
47
47
  # Flash keys from Rails flash to expose to frontend.
48
48
  # Set to nil to disable Rails flash integration (use only flash.inertia).
49
49
  flash_keys: %i[notice alert].freeze,
50
+
51
+ # Whether to prevent database writes during precognition requests.
52
+ # When enabled, any ActiveRecord write during a precognition request
53
+ # will raise ActiveRecord::ReadOnlyError.
54
+ precognition_prevent_writes: false,
50
55
  }.freeze
51
56
 
52
57
  OPTION_NAMES = DEFAULTS.keys.freeze
@@ -5,6 +5,7 @@ require_relative 'flash_extension'
5
5
  require_relative 'helper'
6
6
  require_relative 'action_filter'
7
7
  require_relative 'meta_tag_builder'
8
+ require_relative 'precognition'
8
9
 
9
10
  module InertiaRails
10
11
  module Controller
@@ -13,9 +14,17 @@ module InertiaRails
13
14
  included do
14
15
  helper ::InertiaRails::Helper
15
16
 
17
+ before_action do
18
+ InertiaRails::Current.request = request
19
+ end
20
+
16
21
  after_action do
17
22
  cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
18
23
  end
24
+
25
+ rescue_from InertiaRails::PrecognitionResponse do |e|
26
+ render_precognition(e.errors)
27
+ end
19
28
  end
20
29
 
21
30
  module ClassMethods
@@ -139,6 +148,29 @@ module InertiaRails
139
148
 
140
149
  private
141
150
 
151
+ def precognition!(model_or_errors)
152
+ InertiaRails.precognition!(model_or_errors)
153
+ end
154
+
155
+ def precognition(model_or_errors)
156
+ errors = InertiaRails::Precognition.validate(model_or_errors)
157
+ return if errors.nil?
158
+
159
+ render_precognition(errors)
160
+ true
161
+ end
162
+
163
+ def render_precognition(errors)
164
+ response.headers['Precognition'] = 'true'
165
+
166
+ if errors.empty?
167
+ response.headers['Precognition-Success'] = 'true'
168
+ head :no_content
169
+ else
170
+ render json: { errors: errors }, status: :unprocessable_entity
171
+ end
172
+ end
173
+
142
174
  def inertia_view_assigns
143
175
  return {} unless @_inertia_instance_props
144
176
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :request, :precognition_called
6
+ end
7
+ end
@@ -14,5 +14,9 @@ module InertiaRails
14
14
 
15
15
  @group = props[:group] || DEFAULT_GROUP
16
16
  end
17
+
18
+ def deferred?
19
+ true
20
+ end
17
21
  end
18
22
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InertiaRails
4
+ class Error < StandardError; end
5
+
6
+ class DoublePrecognitionError < StandardError
7
+ def initialize
8
+ super('You can only call precognition once per action, use a form object to validate multiple models.')
9
+ end
10
+ end
11
+ end
@@ -18,7 +18,11 @@ module InertiaRails
18
18
 
19
19
  def response
20
20
  copy_xsrf_to_csrf!
21
- status, headers, body = @app.call(@env)
21
+ status, headers, body = if prevent_precognition_writes?
22
+ ActiveRecord::Base.while_preventing_writes { @app.call(@env) }
23
+ else
24
+ @app.call(@env)
25
+ end
22
26
  request = ActionDispatch::Request.new(@env)
23
27
 
24
28
  # Inertia session data is added via redirect_to
@@ -96,7 +100,19 @@ module InertiaRails
96
100
  end
97
101
 
98
102
  def copy_xsrf_to_csrf!
99
- @env['HTTP_X_CSRF_TOKEN'] = @env['HTTP_X_XSRF_TOKEN'] if @env['HTTP_X_XSRF_TOKEN'] && inertia_request?
103
+ return unless @env['HTTP_X_XSRF_TOKEN'] && (inertia_request? || precognition_request?)
104
+
105
+ @env['HTTP_X_CSRF_TOKEN'] = @env['HTTP_X_XSRF_TOKEN']
106
+ end
107
+
108
+ def precognition_request?
109
+ @env['HTTP_PRECOGNITION'] == 'true'
110
+ end
111
+
112
+ def prevent_precognition_writes?
113
+ precognition_request? &&
114
+ InertiaRails.configuration.precognition_prevent_writes &&
115
+ defined?(ActiveRecord::Base)
100
116
  end
101
117
  end
102
118
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'current'
4
+ require_relative 'errors'
5
+
6
+ module InertiaRails
7
+ class PrecognitionResponse < StandardError
8
+ attr_reader :errors
9
+
10
+ def initialize(errors)
11
+ @errors = errors
12
+ super('Precognition response')
13
+ end
14
+ end
15
+
16
+ module Precognition
17
+ class << self
18
+ # Returns filtered errors hash if precognition request, nil otherwise
19
+ def validate(model_or_errors)
20
+ # Check before the precognitive? guard to catch errors early
21
+ # without waiting for precognition requests.
22
+ ensure_single_precognition_call!
23
+ request = Current.request
24
+ return unless request&.inertia_precognitive?
25
+
26
+ errors = normalize_errors(model_or_errors)
27
+ filter_errors(errors, request)
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_errors(errors)
33
+ return errors if errors.is_a?(Hash)
34
+
35
+ if errors.respond_to?(:valid?) && errors.respond_to?(:errors)
36
+ errors.valid?
37
+ return errors.errors.to_hash
38
+ end
39
+
40
+ return errors.to_hash if errors.respond_to?(:to_hash)
41
+ return errors.to_h if errors.respond_to?(:to_h)
42
+
43
+ raise ArgumentError,
44
+ "Expected a Hash or an object responding to :valid? and :errors, :to_hash, or :to_h, got #{errors.class}"
45
+ end
46
+
47
+ def filter_errors(errors, request)
48
+ only_keys = request.inertia_precognitive_validate_only
49
+ return errors unless only_keys&.any?
50
+
51
+ errors.slice(*only_keys, *only_keys.map(&:to_sym))
52
+ end
53
+
54
+ def ensure_single_precognition_call!
55
+ raise DoublePrecognitionError if Current.precognition_called
56
+
57
+ Current.precognition_called = true
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.precognition!(model_or_errors)
63
+ errors = Precognition.validate(model_or_errors)
64
+ return false if errors.nil?
65
+
66
+ raise PrecognitionResponse, errors, []
67
+ end
68
+ end
@@ -98,17 +98,17 @@ module InertiaRails
98
98
  # rubocop:disable Style/MultilineBlockChain
99
99
  @props
100
100
  .tap do |merged_props|
101
- # Always keep errors in the props
102
- if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
103
- errors = merged_props[:errors]
104
- merged_props[:errors] = InertiaRails.always { errors }
105
- end
101
+ # Always keep errors in the props
102
+ if merged_props.key?(:errors) && !merged_props[:errors].is_a?(BaseProp)
103
+ errors = merged_props[:errors]
104
+ merged_props[:errors] = InertiaRails.always { errors }
105
+ end
106
106
  end
107
107
  .then { |props| deep_transform_props(props) } # Internal hydration/filtering
108
108
  .then { |props| @configuration.prop_transformer(props: props) } # Apply user-defined prop transformer
109
109
  .tap do |props|
110
- # Add meta tags last (never transformed)
111
- props[:_inertia_meta] = meta_tags if meta_tags.present?
110
+ # Add meta tags last (never transformed)
111
+ props[:_inertia_meta] = meta_tags if meta_tags.present?
112
112
  end
113
113
  # rubocop:enable Style/MultilineBlockChain
114
114
  end
@@ -164,7 +164,7 @@ module InertiaRails
164
164
  return if rendering_partial_component?
165
165
 
166
166
  @props.each_with_object({}) do |(key, prop), result|
167
- (result[prop.group] ||= []) << key if prop.is_a?(DeferProp)
167
+ (result[prop.group] ||= []) << key if prop.try(:deferred?)
168
168
  end
169
169
  end
170
170
 
@@ -248,6 +248,7 @@ module InertiaRails
248
248
  @scroll_props = {}
249
249
  requested_merge_props.each do |key, prop|
250
250
  next unless prop.is_a?(ScrollProp)
251
+ next if prop.deferred? && !rendering_partial_component?
251
252
 
252
253
  @scroll_props[key] = prop.metadata.merge!(reset: reset_keys.include?(key))
253
254
  end
@@ -292,7 +293,7 @@ module InertiaRails
292
293
  return false if excluded_by_partial_request?(path)
293
294
 
294
295
  # Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
295
- return false if prop.is_a?(IgnoreOnFirstLoadProp) && !rendering_partial_component?
296
+ return false if (prop.is_a?(IgnoreOnFirstLoadProp) || prop.try(:deferred?)) && !rendering_partial_component?
296
297
 
297
298
  true
298
299
  end
@@ -6,16 +6,24 @@ module InertiaRails
6
6
  class ScrollProp < BaseProp
7
7
  prepend PropMergeable
8
8
 
9
+ attr_reader :group
10
+
9
11
  def initialize(**options, &block)
10
12
  super(&block)
11
13
 
12
14
  @merge = true
15
+ @deferred = options.delete(:defer) || false
16
+ @group = options.delete(:group) || DeferProp::DEFAULT_GROUP
13
17
  @metadata = options.delete(:metadata)
14
18
  @wrapper = options.delete(:wrapper)
15
19
 
16
20
  @options = options
17
21
  end
18
22
 
23
+ def deferred?
24
+ @deferred
25
+ end
26
+
19
27
  def call(controller)
20
28
  @value = super
21
29
  configure_merge_intent(controller.request.headers['X-Inertia-Infinite-Scroll-Merge-Intent'])
@@ -3,6 +3,7 @@
3
3
  module InertiaRails
4
4
  module Testing
5
5
  thread_mattr_accessor :current_response
6
+ mattr_accessor :evaluate_optional_props, default: false
6
7
 
7
8
  module RendererTestingPatch
8
9
  def new(component, controller, request, response, render, **options)
@@ -12,10 +13,23 @@ module InertiaRails
12
13
  end
13
14
  end
14
15
 
16
+ module RendererOptionalInTests
17
+ private
18
+
19
+ def keep_prop?(prop, path)
20
+ return true if InertiaRails::Testing.evaluate_optional_props &&
21
+ (prop.is_a?(IgnoreOnFirstLoadProp) || prop.try(:deferred?)) &&
22
+ !rendering_partial_component?
23
+
24
+ super
25
+ end
26
+ end
27
+
15
28
  def self.install!
16
29
  return if @installed
17
30
 
18
31
  InertiaRails::Renderer.singleton_class.prepend(RendererTestingPatch)
32
+ InertiaRails::Renderer.prepend(RendererOptionalInTests)
19
33
  @installed = true
20
34
  end
21
35
 
@@ -37,7 +51,7 @@ module InertiaRails
37
51
  def assign_locals(params)
38
52
  if params[:locals].present?
39
53
  @view_data = params[:locals].except(:page).with_indifferent_access
40
- page = params[:locals][:page] || {}
54
+ page = JSON.parse((params[:locals][:page] || {}).to_json)
41
55
  else
42
56
  # Sequential Inertia request
43
57
  @view_data = {}
@@ -189,7 +203,7 @@ module InertiaRails
189
203
  def inertia_reload_only(*props)
190
204
  partial_headers = {
191
205
  'X-Inertia' => 'true',
192
- 'X-Inertia-Partial-Data' => props.map(&:to_s).join(','),
206
+ 'X-Inertia-Partial-Data' => props.join(','),
193
207
  'X-Inertia-Partial-Component' => inertia.component,
194
208
  }
195
209
  get request.fullpath, headers: partial_headers
@@ -198,7 +212,7 @@ module InertiaRails
198
212
  def inertia_reload_except(*props)
199
213
  partial_headers = {
200
214
  'X-Inertia' => 'true',
201
- 'X-Inertia-Partial-Except' => props.map(&:to_s).join(','),
215
+ 'X-Inertia-Partial-Except' => props.join(','),
202
216
  'X-Inertia-Partial-Component' => inertia.component,
203
217
  }
204
218
  get request.fullpath, headers: partial_headers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InertiaRails
4
- VERSION = '3.17.0'
4
+ VERSION = '3.18.0'
5
5
  end
@@ -9,6 +9,14 @@ module InertiaRails
9
9
  def inertia_partial?
10
10
  key?('HTTP_X_INERTIA_PARTIAL_COMPONENT')
11
11
  end
12
+
13
+ def inertia_precognitive?
14
+ headers['Precognition'] == 'true'
15
+ end
16
+
17
+ def inertia_precognitive_validate_only
18
+ headers['Precognition-Validate-Only']&.split(',')&.map(&:strip)
19
+ end
12
20
  end
13
21
  end
14
22
 
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.17.0
4
+ version: 3.18.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: 2026-02-05 00:00:00.000000000 Z
12
+ date: 2026-03-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -187,8 +187,10 @@ files:
187
187
  - lib/inertia_rails/base_prop.rb
188
188
  - lib/inertia_rails/configuration.rb
189
189
  - lib/inertia_rails/controller.rb
190
+ - lib/inertia_rails/current.rb
190
191
  - lib/inertia_rails/defer_prop.rb
191
192
  - lib/inertia_rails/engine.rb
193
+ - lib/inertia_rails/errors.rb
192
194
  - lib/inertia_rails/flash_extension.rb
193
195
  - lib/inertia_rails/generators/controller_template_base.rb
194
196
  - lib/inertia_rails/generators/helper.rb
@@ -204,6 +206,7 @@ files:
204
206
  - lib/inertia_rails/minitest.rb
205
207
  - lib/inertia_rails/once_prop.rb
206
208
  - lib/inertia_rails/optional_prop.rb
209
+ - lib/inertia_rails/precognition.rb
207
210
  - lib/inertia_rails/prop_mergeable.rb
208
211
  - lib/inertia_rails/prop_onceable.rb
209
212
  - lib/inertia_rails/renderer.rb