Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add inertia_config to enable controller-specific config #121

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e8fc70
chore: simplify condition
ElMassimo Jun 14, 2024
b5187be
refactor: configuration is now a reusable class instead of a singleton
ElMassimo Jun 14, 2024
c5624be
refactor: allow controllers to override the app version
ElMassimo Jun 14, 2024
20e9671
refactor: use configuration from controller for ssr
ElMassimo Jun 25, 2024
a153301
refactor: remove global for inertia ssr head
ElMassimo Jun 25, 2024
967fa4a
refactor: pull shared data out of the controller
ElMassimo Jun 25, 2024
4d2468d
feat: add `inertia_config` to override global config in controllers
ElMassimo Jun 25, 2024
f80603e
refactor: avoid using global variables for shared data
ElMassimo Jun 25, 2024
e4700d0
refactor: drop `InertiaRails.reset!`
ElMassimo Jun 25, 2024
a780760
refactor: allow to introduce different merge strategies in the future
ElMassimo Jun 25, 2024
c0d938a
Merge remote-tracking branch 'origin' into refactor/shared
ElMassimo Jun 26, 2024
005de89
chore: update tests after merge
ElMassimo Jun 26, 2024
187e45f
refactor: re-introduce `inertia_headers` helper for backwards compat
ElMassimo Jun 26, 2024
48a0994
fix: ensure setting `config.layout` to `nil` manually still behaves t…
ElMassimo Jun 26, 2024
f8a6663
chore: remove duplicate key
ElMassimo Jun 26, 2024
5fb2df2
refactor: cache resolved shared data and config in controller classes
ElMassimo Jun 26, 2024
46626e4
refactor: avoid exposing `to_h` in configuration
ElMassimo Jun 26, 2024
7f84110
docs: update readme to cover the new configuration options
ElMassimo Jun 26, 2024
39710b7
docs: link to configuration options
ElMassimo Jun 26, 2024
05b211d
docs: reference community-maintained docs.
ElMassimo Jun 26, 2024
ce859b2
perf: reduce array allocations when controllers don't override shared…
ElMassimo Jun 26, 2024
2aacfed
perf: reduce `Configuration` instances to the bare minimum
ElMassimo Jun 26, 2024
c747bad
Merge branch 'master' into refactor/shared
bknoles Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@

# Appraisal
gemfiles/*.gemfile.lock

# Local files, such as .env.development.local
*.local
15 changes: 7 additions & 8 deletions bin/console
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
#!/usr/bin/env ruby

require "bundler/setup"
require "inertia_rails/rails"

# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)

# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "rubygems"
require "bundler/setup"
require "rails/all"
require "inertia_rails"

require "irb"
IRB.start(__FILE__)
64 changes: 64 additions & 0 deletions lib/inertia_rails/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

module InertiaRails
class Configuration
DEFAULTS = {
default_render: false,

# Let Rails decide which layout should be used based on the controller configuration.
layout: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that's a breaking change, since current default is nil.
We can write about that in the changelog, or add a default to Renderer#layout:

     def layout
       # we can also raise a warning here?
       return true if configuration.layout.nil?

       configuration.layout
     end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the original change in #86.

The controller method was returning true if there global was nil, so the behavior stays the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested, if someone sets nil manually, this PR will brake their code (Rails renders page without layout):

InertiaRails.configure do |config|
  config.layout = nil
end

Copy link
Contributor Author

@ElMassimo ElMassimo Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unlikely that users are doing that, given it's the default.

That behavior would also be consistent with using layout: false.

If this is released in a major version, I'd say it's more intuitive if whatever the user configures is not overriden internally.

As an alternative, we can raise if the user sets it to nil, since it's more likely a mistake.

Finally, I'd suggest removing layout in the future, it makes more sense to customize using plain Rails in controllers instead, although I'm not proposing we do this now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, that's a great addition, and since README uses true as a default, a note in the changelog should be enough, I guess 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about it, we could be strict in backwards compatibility first, and remove it later, just mentioning that I don't think it's as useful a setting after #86.

Thanks for the review 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-introduced the layout.nil? ? true : layout check, in order to minimize friction when upgrading. Thanks!


deep_merge_shared_data: false,
ssr_enabled: false,
ssr_url: 'http://localhost:13714',
version: nil,
}.freeze

OPTION_NAMES = DEFAULTS.keys.freeze

protected attr_reader :controller
protected attr_reader :options

def initialize(controller: nil, **attrs)
@controller = controller
@options = attrs.extract!(*OPTION_NAMES)

unless attrs.empty?
raise ArgumentError, "Unknown options for #{self.class}: #{attrs.keys}"
end
end

def bind_controller(controller)
Configuration.new(**@options, controller: controller)
end

def to_h
@options.to_h
end

def merge(config)
Configuration.new(**@options.merge(config.options))
end

OPTION_NAMES.each do |option|
define_method(option) {
evaluate_option @options[option]
}
define_method("#{option}=") { |value|
@options[option] = value
}
end

def self.default
new(**DEFAULTS)
end

private

def evaluate_option(value)
return value unless value.respond_to?(:call)
return value.call unless controller
controller.instance_exec(&value)
end
end
end
79 changes: 36 additions & 43 deletions lib/inertia_rails/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,25 @@ module Controller
extend ActiveSupport::Concern

included do
helper_method :inertia_headers

before_action do
error_sharing = proc do
# :inertia_errors are deleted from the session by the middleware
if @_request && session[:inertia_errors].present?
{ errors: session[:inertia_errors] }
else
{}
end
end

@_inertia_shared_plain_data ||= {}
@_inertia_shared_blocks ||= [error_sharing]
@_inertia_html_headers ||= []
end

after_action do
cookies['XSRF-TOKEN'] = form_authenticity_token unless !protect_against_forgery?
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end

module ClassMethods
def inertia_share(hash = nil, &block)
before_action do
@_inertia_shared_plain_data = @_inertia_shared_plain_data.merge(hash) if hash
@_inertia_shared_blocks = @_inertia_shared_blocks + [block] if block_given?
def inertia_share(**attrs, &block)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, should we continue supporting hashes or at least throw a deprecation at first, wdyt?

inertia_share({foo: 'bar'}) # => throws error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #137

@inertia_shared_data ||= []
@inertia_shared_data << attrs unless attrs.empty?
@inertia_shared_data << block if block
end

def inertia_config(**attrs)
config = InertiaRails::Configuration.new(**attrs)

if @inertia_configuration
@inertia_configuration.merge!(config)
else
@inertia_configuration = config
end
end

Expand All @@ -41,28 +33,25 @@ def use_inertia_instance_props
@_inertia_skip_props = view_assigns.keys + ['_inertia_skip_props']
end
end
end

def inertia_headers
@_inertia_html_headers.join.html_safe
end
def _inertia_configuration
config = superclass.try(:_inertia_configuration) || ::InertiaRails.configuration
@inertia_configuration ? config.merge(@inertia_configuration) : config
end

def inertia_headers=(value)
@_inertia_html_headers = value
def _inertia_shared_data
[*superclass.try(:_inertia_shared_data), *@inertia_shared_data]
end
end

def default_render
if InertiaRails.default_render?
if inertia_configuration.default_render
render(inertia: true)
else
super
end
end

def shared_data
(@_inertia_shared_plain_data || {}).merge(evaluated_blocks)
end

def redirect_to(options = {}, response_options = {})
capture_inertia_errors(response_options)
super(options, response_options)
Expand All @@ -77,19 +66,27 @@ def redirect_back(fallback_location:, allow_other_host: true, **options)
)
end

private

def inertia_view_assigns
return {} unless @_inertia_instance_props
view_assigns.except(*@_inertia_skip_props)
end

private
def inertia_configuration
self.class._inertia_configuration.bind_controller(self)
end

def inertia_layout
layout = ::InertiaRails.layout
def inertia_shared_data
initial_data = session[:inertia_errors].present? ? {errors: session[:inertia_errors]} : {}
Copy link
Contributor

@skryukov skryukov Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to set inertia_share(errors: {}) to mimic Laravel plugin's default, with this change — I always overwrite inertia's errors with my empty hash.

Should we move this to the end of the chain?

    def inertia_shared_data
      self.class._inertia_shared_data.filter_map { |shared_data|
        if shared_data.respond_to?(:call)
          instance_exec(&shared_data)
        else
          shared_data
        end
      }.reduce({}, &:merge).tap do |data|
        data[:errors] = session[:inertia_errors] if session[:inertia_errors].present?
      end
    end


# When the global configuration is not set, let Rails decide which layout
# should be used based on the controller configuration.
layout.nil? ? true : layout
self.class._inertia_shared_data.filter_map { |shared_data|
if shared_data.respond_to?(:call)
instance_exec(&shared_data)
else
shared_data
end
}.reduce(initial_data, &:merge)
end

def inertia_location(url)
Expand All @@ -102,9 +99,5 @@ def capture_inertia_errors(options)
session[:inertia_errors] = inertia_errors
end
end

def evaluated_blocks
(@_inertia_shared_blocks || []).map { |block| instance_exec(&block) }.reduce(&:merge) || {}
end
end
end
44 changes: 6 additions & 38 deletions lib/inertia_rails/inertia_rails.rb
Original file line number Diff line number Diff line change
@@ -1,52 +1,20 @@
# Needed for `thread_mattr_accessor`
require 'active_support/core_ext/module/attribute_accessors_per_thread'
require 'inertia_rails/lazy'
require 'inertia_rails/configuration'

module InertiaRails
def self.configure
yield(Configuration)
end

def self.version
Configuration.evaluated_version
end

def self.layout
Configuration.layout
end

def self.ssr_enabled?
Configuration.ssr_enabled
end
CONFIGURATION = Configuration.default

def self.ssr_url
Configuration.ssr_url
end

def self.default_render?
Configuration.default_render
def self.configure
yield(CONFIGURATION)
end

def self.deep_merge_shared_data?
Configuration.deep_merge_shared_data
def self.configuration
CONFIGURATION
end

def self.lazy(value = nil, &block)
InertiaRails::Lazy.new(value, &block)
end

private

module Configuration
mattr_accessor(:layout) { nil }
mattr_accessor(:version) { nil }
mattr_accessor(:ssr_enabled) { false }
mattr_accessor(:ssr_url) { 'http://localhost:13714' }
mattr_accessor(:default_render) { false }
mattr_accessor(:deep_merge_shared_data) { false }

def self.evaluated_version
self.version.respond_to?(:call) ? self.version.call : self.version
end
end
end
21 changes: 11 additions & 10 deletions lib/inertia_rails/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ def initialize(app)
end

def call(env)
InertiaRailsRequest.new(@app, env)
.response
InertiaRailsRequest.new(@app, env).response
end

class InertiaRailsRequest
Expand Down Expand Up @@ -58,11 +57,15 @@ def get?
request_method == 'GET'
end

def controller
@env["action_controller.instance"]
end

def request_method
@env['REQUEST_METHOD']
end

def inertia_version
def client_version
@env['HTTP_X_INERTIA_VERSION']
end

Expand All @@ -71,17 +74,15 @@ def inertia_request?
end

def version_stale?
sent_version != saved_version
coerce_version(client_version) != coerce_version(server_version)
end

def sent_version
return nil if inertia_version.nil?

InertiaRails.version.is_a?(Numeric) ? inertia_version.to_f : inertia_version
def server_version
controller.send(:inertia_configuration).version
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller might be nil (i.e. in case of 404 error), I think this is a valid fix:

Suggested change
controller.send(:inertia_configuration).version
config = controller ? controller.send(:inertia_configuration) : ::InertiaRails.configuration
config.version

end

def saved_version
InertiaRails.version.is_a?(Numeric) ? InertiaRails.version.to_f : InertiaRails.version
def coerce_version(version)
server_version.is_a?(Numeric) ? version.to_f : version
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that a typo?

Suggested change
server_version.is_a?(Numeric) ? version.to_f : version
version.is_a?(Numeric) ? version.to_f : version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's the original logic.

end

def force_refresh(request)
Expand Down
Loading