Skip to content

Commit

Permalink
Merge pull request #163 from inertiajs/dot-notation-for-only-props
Browse files Browse the repository at this point in the history
Support dot notation for :only keys in partial reloads
  • Loading branch information
bknoles authored Nov 21, 2024
2 parents 602394f + 2027984 commit e11e8c5
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 24 deletions.
50 changes: 49 additions & 1 deletion docs/guide/partial-reloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ router.visit(url, {

## Except certain props

In addition to the only visit option you can also use the except option to specify which data the server should exclude. This option should also be an array of keys which correspond to the keys of the props.

:::tabs key:frameworks
== Vue

Expand Down Expand Up @@ -79,7 +81,53 @@ router.visit(url, {

:::

In addition to the only visit option you can also use the except option to specify which data the server should exclude. This option should also be an array of keys which correspond to the keys of the props.
## Dot notation

Both the `only` and `except` visit options support dot notation to specify nested data, and they can be used together. In the following example, only `settings.theme` will be rendered, but without its `colors` property.

:::tabs key:frameworks
== Vue

```js
import { router } from '@inertiajs/vue3'

router.visit(url, {
only: ['settings.theme'],
except: ['setting.theme.colors'],
})
```

== React

```jsx
import { router } from '@inertiajs/react'

router.visit(url, {
only: ['settings.theme'],
except: ['setting.theme.colors'],
})
```

== Svelte 4|Svelte 5

```js
import { router } from '@inertiajs/svelte'

router.visit(url, {
only: ['settings.theme'],
except: ['setting.theme.colors'],
})
```

:::

Please remember that, by design, partial reloading filters props _before_ they are evaluated, so it can only target explicitly defined prop keys. Let's say you have this prop:

`users: -> { User.all }`

Requesting `only: ['users.name']` will exclude the entire `users` prop, since `users.name` is not available before evaluating the prop.

Requesting `except: ['users.name']` will not exclude anything.

## Router shorthand

Expand Down
74 changes: 51 additions & 23 deletions lib/inertia_rails/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

module InertiaRails
class Renderer
KEEP_PROP = :keep
DONT_KEEP_PROP = :dont_keep

attr_reader(
:component,
:configuration,
Expand Down Expand Up @@ -74,25 +77,21 @@ def merge_props(shared_data, props)
end

def computed_props
_props = merge_props(shared_data, props).select do |key, prop|
if rendering_partial_component?
partial_keys.none? || key.in?(partial_keys) || prop.is_a?(AlwaysProp)
else
!prop.is_a?(LazyProp)
end
end
_props = merge_props(shared_data, props)

drop_partial_except_keys(_props) if rendering_partial_component?
deep_transform_props _props do |prop, path|
next [DONT_KEEP_PROP] unless keep_prop?(prop, path)

deep_transform_values _props do |prop|
case prop
transformed_prop = case prop
when BaseProp
prop.call(controller)
when Proc
controller.instance_exec(&prop)
else
prop
end

[KEEP_PROP, transformed_prop]
end
end

Expand All @@ -105,28 +104,28 @@ def page
}
end

def deep_transform_values(hash, &block)
return block.call(hash) unless hash.is_a? Hash
def deep_transform_props(props, parent_path = [], &block)
props.reduce({}) do |transformed_props, (key, prop)|
current_path = parent_path + [key]

hash.transform_values {|value| deep_transform_values(value, &block)}
end

def drop_partial_except_keys(hash)
partial_except_keys.each do |key|
parts = key.to_s.split('.').map(&:to_sym)
*initial_keys, last_key = parts
current = initial_keys.any? ? hash.dig(*initial_keys) : hash
if prop.is_a?(Hash) && prop.any?
nested = deep_transform_props(prop, current_path, &block)
transformed_props.merge!(key => nested) unless nested.empty?
else
action, transformed_prop = block.call(prop, current_path)
transformed_props.merge!(key => transformed_prop) if action == KEEP_PROP
end

current.delete(last_key) if current.is_a?(Hash) && !current[last_key].is_a?(AlwaysProp)
transformed_props
end
end

def partial_keys
(@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact.map(&:to_sym)
(@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact
end

def partial_except_keys
(@request.headers['X-Inertia-Partial-Except'] || '').split(',').filter_map(&:to_sym)
(@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact
end

def rendering_partial_component?
Expand All @@ -138,5 +137,34 @@ def resolve_component(component)

configuration.component_path_resolver(path: controller.controller_path, action: controller.action_name)
end

def keep_prop?(prop, path)
return true if prop.is_a?(AlwaysProp)

if rendering_partial_component?
path_with_prefixes = path_prefixes(path)
return false if excluded_by_only_partial_keys?(path_with_prefixes)
return false if excluded_by_except_partial_keys?(path_with_prefixes)
end

# Precedence: Evaluate LazyProp only after partial keys have been checked
return false if prop.is_a?(LazyProp) && !rendering_partial_component?

true
end

def path_prefixes(parts)
(0...parts.length).map do |i|
parts[0..i].join('.')
end
end

def excluded_by_only_partial_keys?(path_with_prefixes)
partial_keys.present? && (path_with_prefixes & partial_keys).empty?
end

def excluded_by_except_partial_keys?(path_with_prefixes)
partial_except_keys.present? && (path_with_prefixes & partial_except_keys).any?
end
end
end
31 changes: 31 additions & 0 deletions spec/dummy/app/controllers/inertia_render_test_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@ def except_props
}
end

def deeply_nested_props
render inertia: 'TestComponent', props: {
flat: 'flat param',
lazy: InertiaRails.lazy('lazy param'),
nested_lazy: InertiaRails.lazy do
{
first: 'first nested lazy param',
}
end,
nested: {
first: 'first nested param',
second: 'second nested param',
evaluated: -> do
{
first: 'first evaluated nested param',
second: 'second evaluated nested param'
}
end,
deeply_nested: {
first: 'first deeply nested param',
second: false,
what_about_nil: nil,
what_about_empty_hash: {},
deeply_nested_always: InertiaRails.always { 'deeply nested always prop' },
deeply_nested_lazy: InertiaRails.lazy { 'deeply nested lazy prop' }
}
},
always: InertiaRails.always { 'always prop' }
}
end

def view_data
render inertia: 'TestComponent', view_data: {
name: 'Brian',
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
get 'always_props' => 'inertia_render_test#always_props'
get 'except_props' => 'inertia_render_test#except_props'
get 'non_inertiafied' => 'inertia_test#non_inertiafied'
get 'deeply_nested_props' => 'inertia_render_test#deeply_nested_props'

get 'instance_props_test' => 'inertia_rails_mimic#instance_props_test'
get 'default_render_test' => 'inertia_rails_mimic#default_render_test'
Expand Down
140 changes: 140 additions & 0 deletions spec/inertia/rendering_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,146 @@
is_expected.to include('Brandon')
end
end

context 'with dot notation' do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Data' => 'nested.first,nested.deeply_nested.second,nested.deeply_nested.what_about_nil,nested.deeply_nested.what_about_empty_hash',
'X-Inertia-Partial-Component' => 'TestComponent',
}
end

before { get deeply_nested_props_path, headers: headers }

it 'only renders the dot notated props' do
expect(response.parsed_body['props']).to eq(
'always' => 'always prop',
'nested' => {
'first' => 'first nested param',
'deeply_nested' => {
'second' => false,
'what_about_nil' => nil,
'what_about_empty_hash' => {},
'deeply_nested_always' => 'deeply nested always prop',
},
},
)
end
end

context 'with both partial and except dot notation' do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Component' => 'TestComponent',
'X-Inertia-Partial-Data' => 'lazy,nested.deeply_nested',
'X-Inertia-Partial-Except' => 'nested.deeply_nested.first',
}
end

before { get deeply_nested_props_path, headers: headers }

it 'renders the partial data and excludes the excepted data' do
expect(response.parsed_body['props']).to eq(
'always' => 'always prop',
'lazy' => 'lazy param',
'nested' => {
'deeply_nested' => {
'second' => false,
'what_about_nil' => nil,
'what_about_empty_hash' => {},
'deeply_nested_always' => 'deeply nested always prop',
'deeply_nested_lazy' => 'deeply nested lazy prop',
},
},
)
end
end

context 'with nonsensical partial data that includes and excludes the same prop and tries to exclude an always prop' do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Component' => 'TestComponent',
'X-Inertia-Partial-Data' => 'lazy',
'X-Inertia-Partial-Except' => 'lazy,always',
}
end

before { get deeply_nested_props_path, headers: headers }

it 'excludes everything but Always props' do
expect(response.parsed_body['props']).to eq(
'always' => 'always prop',
'nested' => {
'deeply_nested' => {
'deeply_nested_always' => 'deeply nested always prop',
},
},
)
end
end

context 'with only props that target transformed data' do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Component' => 'TestComponent',
'X-Inertia-Partial-Data' => 'nested.evaluated.first',
}
end

before { get deeply_nested_props_path, headers: headers }

it 'filters out the entire evaluated prop' do
expect(response.parsed_body['props']).to eq(
'always' => 'always prop',
'nested' => {
'deeply_nested' => {
'deeply_nested_always' => 'deeply nested always prop',
},
},
)
end
end

context 'with except props that target transformed data' do
let(:headers) do
{
'X-Inertia' => true,
'X-Inertia-Partial-Component' => 'TestComponent',
'X-Inertia-Partial-Except' => 'nested.evaluated.first',
}
end

before { get deeply_nested_props_path, headers: headers }

it 'renders the entire evaluated prop' do
expect(response.parsed_body['props']).to eq(
'always' => 'always prop',
'flat' => 'flat param',
'lazy' => 'lazy param',
'nested_lazy' => { 'first' => 'first nested lazy param' },
'nested' => {
'first' => 'first nested param',
'second' => 'second nested param',
'evaluated' => {
'first' => 'first evaluated nested param',
'second' => 'second evaluated nested param',
},
'deeply_nested' => {
'first' => 'first deeply nested param',
'second' => false,
'what_about_nil' => nil,
'what_about_empty_hash' => {},
'deeply_nested_always' => 'deeply nested always prop',
'deeply_nested_lazy' => 'deeply nested lazy prop',
},
},
)
end
end
end

context 'partial except rendering' do
Expand Down

0 comments on commit e11e8c5

Please sign in to comment.