Skip to content

Docs: add Available since component #235

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

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions docs/.vitepress/availableSinceMarkdownPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import MarkdownIt from 'markdown-it'

export interface AvailableSinceParams {
rails?: string
core?: string
description?: string
}

function parseAvailableSinceParams(info: string): AvailableSinceParams {
const basicMatch = info.trim().match(/^available_since(?:\s+(.*))?$/)
if (!basicMatch) return {}

const allParams = basicMatch[1] || ''
const params: AvailableSinceParams = {}

// Parse out key=value pairs first
const keyValueMatches = [...allParams.matchAll(/([a-z]+)(?:=("[^"]*"|[^\s"]+))?/g)]
for (const [, key, value] of keyValueMatches) {
let cleanValue = value ? value.replace(/^"|"$/g, '') : true

if (key === 'rails') params.rails = cleanValue as string
if (key === 'core') params.core = cleanValue as string
if (key === 'description') params.description = cleanValue as string
}

return params
}

export function availableSinceMarkdownPlugin(md: MarkdownIt) {
md.block.ruler.before('paragraph', 'available_since_oneliner', (state, start, end, silent) => {
const line = state.getLines(start, start + 1, 0, false).trim()

const match = line.match(/^@available_since\s+(.+)$/)
if (!match) return false

if (silent) return true

const params = parseAvailableSinceParams(`available_since ${match[1]}`)
const token = state.push('available_since_oneliner', '', 0)

token.content = renderAvailableSince(params, md)
token.map = [start, start + 1]

state.line = start + 1
return true
})

// Render the one-liner available_since token
md.renderer.rules.available_since_oneliner = (tokens, idx) => {
return tokens[idx].content + '\n'
}
}

function renderAvailableSince(params: AvailableSinceParams, md: MarkdownIt): string {
const railsAttr = params.rails ? `rails="${md.utils.escapeHtml(params.rails)}"` : ''
const coreAttr = params.core ? `core="${md.utils.escapeHtml(params.core)}"` : ''
const descriptionAttr = params.description ? `description="${md.utils.escapeHtml(params.description)}"` : ''

return `<AvailableSince ${railsAttr} ${coreAttr} ${descriptionAttr} />`
}
2 changes: 2 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from 'vitepress'
import { availableSinceMarkdownPlugin } from './availableSinceMarkdownPlugin'
import { tabsMarkdownPlugin } from './vitepress-plugin-tabs/tabsMarkdownPlugin'

const title = 'Inertia Rails'
Expand All @@ -16,6 +17,7 @@ export default defineConfig({
markdown: {
config(md) {
md.use(tabsMarkdownPlugin)
md.use(availableSinceMarkdownPlugin)
},
},

Expand Down
257 changes: 257 additions & 0 deletions docs/.vitepress/theme/components/AvailableSince.vue

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/.vitepress/theme/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import AvailableSince from './AvailableSince.vue'

export {
AvailableSince,
}
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Theme } from 'vitepress'
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
import DefaultTheme from 'vitepress/theme'
import { h } from 'vue'
import { AvailableSince } from './components'
import { setupFrameworksTabs } from './frameworksTabs'
import './style.css'

Expand All @@ -15,6 +16,7 @@ export default {
},
enhanceApp({ app, router, siteData }) {
enhanceAppWithTabs(app)
app.component('AvailableSince', AvailableSince)
},
setup() {
setupFrameworksTabs()
Expand Down
9 changes: 8 additions & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ Use `component_path_resolver` to customize component path resolution when [`defa

### `deep_merge_shared_data`

@available_since rails=3.8.0

When enabled, props will be deep merged with shared data, combining hashes
with the same keys instead of replacing them.

**Default**: `false`


### `default_render`

Overrides Rails default rendering behavior to render using Inertia by default.
Expand All @@ -53,6 +56,8 @@ Overrides Rails default rendering behavior to render using Inertia by default.

### `encrypt_history`

@available_since rails=3.7.0 core=2.0.0

When enabled, you instruct Inertia to encrypt your app's history, it uses
the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto)
to encrypt the current page's data before pushing it to the history state.
Expand All @@ -61,11 +66,13 @@ to encrypt the current page's data before pushing it to the history state.

### `ssr_enabled` _(experimental)_

@available_since rails=3.6.0 core=2.0.0

Whether to use a JavaScript server to pre-render your JavaScript pages,
allowing your visitors to receive fully rendered HTML when they first visit
your application.

Requires a JS server to be available at `ssr_url`. [_Example_](https://github.com/ElMassimo/inertia-rails-ssr-template)
Requires a JavaScript server to be available at `ssr_url`. [_Example_](https://github.com/ElMassimo/inertia-rails-ssr-template)

**Default**: `false`

Expand Down
6 changes: 6 additions & 0 deletions docs/guide/deferred-props.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Deferred props

@available_since rails=3.6.0 core=2.0.0

Inertia's deferred props feature allows you to defer the loading of certain page data until after the initial page render. This can be useful for improving the perceived performance of your app by allowing the initial page render to happen as quickly as possible.

## Server side
Expand Down Expand Up @@ -39,6 +41,10 @@ end

In the example above, the `teams`, `projects`, and `tasks` props will be fetched in one request, while the `permissions` prop will be fetched in a separate request in parallel. Group names are arbitrary strings and can be anything you choose.

### Combining with mergeable props

Deferred props can be combined with mergeable props. You can learn more about this feature in the [Merging props](/guide/merging-props) section.

## Client side

On the client side, Inertia provides the `Deferred` component to help you manage deferred props. This component will automatically wait for the specified deferred props to be available before rendering its children.
Expand Down
60 changes: 54 additions & 6 deletions docs/guide/merging-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,76 @@ By default, Inertia overwrites props with the same name when reloading a page. H

## Server side

> `deep_merge` requires `@inertiajs/core` v2.0.8 or higher, and `inertia_rails` v3.8.0 or higher.
### Using `merge`

To specify that a prop should be merged, use the `merge` or `deep_merge` method on the prop's value.
@available_since rails=3.8.0 core=2.0.8

Use `merge` for merging simple arrays, and `deep_merge` for handling nested objects that include arrays or complex structures, such as pagination objects.
To specify that a prop should be merged, use the `merge` method on the prop's value. This is ideal for merging simple arrays.

On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value.

```ruby
class UsersController < ApplicationController
include Pagy::Backend

def index
pagy, records = pagy(User.all)
_pagy, records = pagy(User.all)

render inertia: {
# simple array:
users: InertiaRails.merge { records.as_json(...) },
# with match_on parameter for smart merging:
products: InertiaRails.merge(match_on: 'id') { Product.all.as_json(...) },
}
end
end
```

### Using `deep_merge`

@available_since rails=3.8.0 core=2.0.8

For handling nested objects that include arrays or complex structures, such as pagination objects, use the `deep_merge` method.

```ruby
class UsersController < ApplicationController
include Pagy::Backend

def index
pagy, records = pagy(User.all)

render inertia: {
# pagination object:
data: InertiaRails.deep_merge {
{
records: records.as_json(...),
pagy: pagy_metadata(pagy)
}
},
# nested objects with match_on:
categories: InertiaRails.deep_merge(match_on: %w[items.id tags.id]) {
{
items: Category.all.as_json(...),
tags: Tag.all.as_json(...)
}
}
}
end
end
```

On the client side, Inertia detects that this prop should be merged. If the prop returns an array, it will append the response to the current prop value. If it's an object, it will merge the response with the current prop value. If you have opted to `deepMerge`, Inertia ensures a deep merge of the entire structure.
If you have opted to use `deep_merge`, Inertia ensures a deep merge of the entire structure, including nested objects and arrays.

**Of note:** During the merging process, if the value is an array, the incoming items will be _appended_ to the existing array, not merged by index.
### Smart merging with `match_on`

@available_since rails=master core=2.0.13

By default, arrays are simply appended during merging. If you need to update specific items in an array or replace them based on a unique identifier, you can use the `match_on` parameter.

The `match_on` parameter enables smart merging by specifying a field to match on when merging arrays of objects:

- For `merge` with simple arrays, specify the object key to match on (e.g., `'id'`)
- For `deep_merge` with nested structures, use dot notation to specify the path (e.g., `'items.id'`)

You can also combine [deferred props](/guide/deferred-props) with mergeable props to defer the loading of the prop and ultimately mark it as mergeable once it's loaded.

Expand All @@ -54,6 +93,15 @@ class UsersController < ApplicationController
records: records.as_json(...),
pagy: pagy_metadata(pagy)
}
},
# with match_on parameter:
products: InertiaRails.defer(merge: true, match_on: 'id') { products.as_json(...) },
# nested objects with match_on:
categories: InertiaRails.defer(deep_merge: true, match_on: %w[items.id tags.id]) {
{
items: Category.all.as_json(...),
tags: Tag.all.as_json(...)
}
}
}
end
Expand Down
5 changes: 3 additions & 2 deletions lib/inertia_rails/defer_prop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ module InertiaRails
class DeferProp < IgnoreOnFirstLoadProp
DEFAULT_GROUP = 'default'

attr_reader :group
attr_reader :group, :match_on

def initialize(group: nil, merge: nil, deep_merge: nil, &block)
def initialize(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
raise ArgumentError, 'Cannot set both `deep_merge` and `merge` to true' if deep_merge && merge

super(&block)

@group = group || DEFAULT_GROUP
@merge = merge || deep_merge
@deep_merge = deep_merge
@match_on = match_on.nil? ? nil : Array(match_on)
end

def merge?
Expand Down
2 changes: 1 addition & 1 deletion lib/inertia_rails/generators/controller_template_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ControllerTemplateBase < Rails::Generators::NamedBase
default: Helper.guess_the_default_framework

class_option :typescript, type: :boolean, desc: 'Whether to use TypeScript',
default: Helper.guess_typescript
default: Helper.uses_typescript?

argument :actions, type: :array, default: [], banner: 'action action'

Expand Down
2 changes: 1 addition & 1 deletion lib/inertia_rails/generators/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def guess_the_default_framework(package_json_path = DEFAULT_PACKAGE_PATH)
end
end

def guess_typescript
def uses_typescript?
Rails.root.join('tsconfig.json').exist?
end

Expand Down
12 changes: 6 additions & 6 deletions lib/inertia_rails/inertia_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ def always(&block)
AlwaysProp.new(&block)
end

def merge(&block)
MergeProp.new(&block)
def merge(match_on: nil, &block)
MergeProp.new(match_on: match_on, &block)
end

def deep_merge(&block)
MergeProp.new(deep_merge: true, &block)
def deep_merge(match_on: nil, &block)
MergeProp.new(deep_merge: true, match_on: match_on, &block)
end

def defer(group: nil, merge: nil, deep_merge: nil, &block)
DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, &block)
def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
end
end
end
5 changes: 4 additions & 1 deletion lib/inertia_rails/merge_prop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

module InertiaRails
class MergeProp < BaseProp
def initialize(deep_merge: false, &block)
attr_reader :match_on

def initialize(deep_merge: false, match_on: nil, &block)
super(&block)
@deep_merge = deep_merge
@match_on = match_on.nil? ? nil : Array(match_on)
end

def merge?
Expand Down
30 changes: 20 additions & 10 deletions lib/inertia_rails/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,17 @@ def page
deferred_props = deferred_props_keys
default_page[:deferredProps] = deferred_props if deferred_props.present?

all_merge_props = merge_props_keys

deep_merge_props, merge_props = all_merge_props.partition do |key|
@props[key].deep_merge?
deep_merge_props, merge_props = all_merge_props.partition do |_key, prop|
prop.deep_merge?
end

default_page[:mergeProps] = merge_props if merge_props.present?
default_page[:deepMergeProps] = deep_merge_props if deep_merge_props.present?
match_props_on = all_merge_props.filter_map do |key, prop|
prop.match_on.map { |ms| "#{key}.#{ms}" } if prop.match_on.present?
end.flatten

default_page[:mergeProps] = merge_props.map(&:first) if merge_props.present?
default_page[:deepMergeProps] = deep_merge_props.map(&:first) if deep_merge_props.present?
default_page[:matchPropsOn] = match_props_on if match_props_on.present?

default_page
end
Expand Down Expand Up @@ -147,9 +150,16 @@ def deferred_props_keys
end
end

def merge_props_keys
@props.each_with_object([]) do |(key, prop), result|
result << key if prop.try(:merge?) && reset_keys.exclude?(key)
def all_merge_props
@all_merge_props ||= @props.select do |key, prop|
next unless prop.try(:merge?)
next if reset_keys.include?(key)
next if rendering_partial_component? && (
(partial_keys.present? && partial_keys.exclude?(key.name)) ||
(partial_except_keys.present? && partial_except_keys.include?(key.name))
)

true
end
end

Expand Down Expand Up @@ -180,7 +190,7 @@ def resolve_component(component)
def keep_prop?(prop, path)
return true if prop.is_a?(AlwaysProp)

if rendering_partial_component?
if rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
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)
Expand Down
4 changes: 4 additions & 0 deletions spec/dummy/app/controllers/inertia_render_test_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,14 @@ def always_props
def merge_props
render inertia: 'TestComponent', props: {
merge: InertiaRails.merge { 'merge prop' },
match_on: InertiaRails.merge(match_on: 'id') { [id: 1] },
deep_merge: InertiaRails.deep_merge { { deep: 'merge prop' } },
deep_match_on: InertiaRails.deep_merge(match_on: 'deep.id') { { deep: [id: 1] } },
regular: 'regular prop',
deferred_merge: InertiaRails.defer(merge: true) { 'deferred and merge prop' },
deferred_match_on: InertiaRails.defer(merge: true, match_on: 'id') { [id: 1] },
deferred_deep_merge: InertiaRails.defer(deep_merge: true) { { deep: 'deferred and merge prop' } },
deferred_deep_match_on: InertiaRails.defer(deep_merge: true, match_on: 'deep.id') { { deep: [id: 1] } },
deferred: InertiaRails.defer { 'deferred' },
}
end
Expand Down
Loading