diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..4eb6cff --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,41 @@ +name: Documentation Checks + +on: + push: + branches: + - main + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + +jobs: + check_formatting: + runs-on: ubuntu-latest + name: Check Documentation Formatting + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: './docs/package-lock.json' + + - name: Install dependencies + working-directory: ./docs + run: npm ci + + - name: Check formatting + working-directory: ./docs + run: npm run format:check diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4a6a9a9..518bc8c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,8 +4,15 @@ on: push: branches: - main - + paths-ignore: + - 'docs/**' + - '.github/workflows/docs.yml' + - '**.md' pull_request: + paths-ignore: + - 'docs/**' + - '.github/workflows/docs.yml' + - '**.md' jobs: lint: @@ -15,7 +22,7 @@ jobs: BUNDLE_JOBS: 4 BUNDLE_RETRY: 3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/docs/.prettierignore b/docs/.prettierignore new file mode 100644 index 0000000..7137f7a --- /dev/null +++ b/docs/.prettierignore @@ -0,0 +1,3 @@ +**/node_modules +**/dist +**/cache diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 170800b..019a431 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,10 +1,10 @@ -import { defineConfig } from "vitepress"; -import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"; +import { defineConfig } from 'vitepress' +import { tabsMarkdownPlugin } from './vitepress-plugin-tabs/tabsMarkdownPlugin' -const title = "Inertia Rails"; -const description = "Community documentation for Inertia.js Rails adapter"; -const site = "https://inertia-rails.netlify.app"; -const image = `${site}/og_image.jpg`; +const title = 'Inertia Rails' +const description = 'Community documentation for Inertia.js Rails adapter' +const site = 'https://inertia-rails.netlify.app' +const image = `${site}/og_image.jpg` // https://vitepress.dev/reference/site-config export default defineConfig({ @@ -13,47 +13,47 @@ export default defineConfig({ markdown: { config(md) { - md.use(tabsMarkdownPlugin); + md.use(tabsMarkdownPlugin) }, }, head: [ - ["link", { rel: "icon", href: "/favicon.ico", sizes: "32x32" }], - ["link", { rel: "icon", href: "/icon.svg", type: "image/svg+xml" }], + ['link', { rel: 'icon', href: '/favicon.ico', sizes: '32x32' }], + ['link', { rel: 'icon', href: '/icon.svg', type: 'image/svg+xml' }], - ["meta", { name: "twitter:card", content: "summary_large_image" }], - ["meta", { name: "twitter:site", content: site }], - ["meta", { name: "twitter:description", value: description }], - ["meta", { name: "twitter:image", content: image }], + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + ['meta', { name: 'twitter:site', content: site }], + ['meta', { name: 'twitter:description', value: description }], + ['meta', { name: 'twitter:image', content: image }], - ["meta", { property: "og:type", content: "website" }], - ["meta", { property: "og:locale", content: "en_US" }], - ["meta", { property: "og:site", content: site }], - ["meta", { property: "og:site_name", content: title }], - ["meta", { property: "og:image", content: image }], - ["meta", { property: "og:description", content: description }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:locale', content: 'en_US' }], + ['meta', { property: 'og:site', content: site }], + ['meta', { property: 'og:site_name', content: title }], + ['meta', { property: 'og:image', content: image }], + ['meta', { property: 'og:description', content: description }], ], themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ - { text: "Home", link: "/" }, - { text: "Guide", link: "/guide" }, - { text: "Cookbook", link: "/cookbook/integrating-shadcn-ui" }, + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide' }, + { text: 'Cookbook', link: '/cookbook/integrating-shadcn-ui' }, { - text: "Links", + text: 'Links', items: [ - { text: "Official Inertia.js docs", link: "https://inertiajs.com" }, + { text: 'Official Inertia.js docs', link: 'https://inertiajs.com' }, { - text: "Gems", + text: 'Gems', items: [ { - text: "inertia_rails", - link: "https://github.com/inertiajs/inertia-rails", + text: 'inertia_rails', + link: 'https://github.com/inertiajs/inertia-rails', }, { - text: "inertia_rails-contrib", - link: "https://github.com/skryukov/inertia_rails-contrib", + text: 'inertia_rails-contrib', + link: 'https://github.com/skryukov/inertia_rails-contrib', }, ], }, @@ -61,76 +61,93 @@ export default defineConfig({ }, ], - logo: "/logo.svg", + logo: '/logo.svg', sidebar: { - "/guide": [ + '/guide': [ { items: [ - { text: "Introduction", link: "/guide" }, - { text: "Demo app", link: "/guide/demo-application" }, + { text: 'Introduction', link: '/guide' }, + { text: 'Demo app', link: '/guide/demo-application' }, + { text: 'Upgrade guide', link: '/guide/upgrade-guide' }, ], }, { - text: "Installation", + text: 'Installation', items: [ - { text: "Server-side", link: "/guide/server-side-setup" }, - { text: "Client-side", link: "/guide/client-side-setup" }, + { text: 'Server-side', link: '/guide/server-side-setup' }, + { text: 'Client-side', link: '/guide/client-side-setup' }, ], }, { - text: "Core concepts", + text: 'Core concepts', items: [ - { text: "Who is it for", link: "/guide/who-is-it-for" }, - { text: "How it works", link: "/guide/how-it-works" }, - { text: "The protocol", link: "/guide/the-protocol" }, + { text: 'Who is it for', link: '/guide/who-is-it-for' }, + { text: 'How it works', link: '/guide/how-it-works' }, + { text: 'The protocol', link: '/guide/the-protocol' }, ], }, { - text: "The basics", + text: 'The basics', items: [ - { text: "Pages", link: "/guide/pages" }, - { text: "Responses", link: "/guide/responses" }, - { text: "Redirects", link: "/guide/redirects" }, - { text: "Routing", link: "/guide/routing" }, - { text: "Title & meta", link: "/guide/title-and-meta" }, - { text: "Links", link: "/guide/links" }, - { text: "Manual visits", link: "/guide/manual-visits" }, - { text: "Forms", link: "/guide/forms" }, - { text: "File uploads", link: "/guide/file-uploads" }, - { text: "Validation", link: "/guide/validation" }, - { text: "Shared data", link: "/guide/shared-data" }, + { text: 'Pages', link: '/guide/pages' }, + { text: 'Responses', link: '/guide/responses' }, + { text: 'Redirects', link: '/guide/redirects' }, + { text: 'Routing', link: '/guide/routing' }, + { text: 'Title & meta', link: '/guide/title-and-meta' }, + { text: 'Links', link: '/guide/links' }, + { text: 'Manual visits', link: '/guide/manual-visits' }, + { text: 'Forms', link: '/guide/forms' }, + { text: 'File uploads', link: '/guide/file-uploads' }, + { text: 'Validation', link: '/guide/validation' }, ], }, { - text: "Advanced", + text: 'Data & Props', items: [ - { text: "Events", link: "/guide/events" }, - { text: "Testing", link: "/guide/testing" }, - { text: "Partial reloads", link: "/guide/partial-reloads" }, - { text: "Scroll management", link: "/guide/scroll-management" }, - { text: "Authentication", link: "/guide/authentication" }, - { text: "Authorization", link: "/guide/authorization" }, - { text: "CSRF protection", link: "/guide/csrf-protection" }, - { text: "Error handling", link: "/guide/error-handling" }, - { text: "Asset versioning", link: "/guide/asset-versioning" }, - { text: "Progress indicators", link: "/guide/progress-indicators" }, - { text: "Remembering state", link: "/guide/remembering-state" }, - { text: "Code splitting", link: "/guide/code-splitting" }, + { text: 'Shared data', link: '/guide/shared-data' }, + { text: 'Partial reloads', link: '/guide/partial-reloads' }, + { text: 'Deferred props', link: '/guide/deferred-props' }, + { text: 'Polling', link: '/guide/polling' }, + { text: 'Prefetching', link: '/guide/prefetching' }, + { text: 'Load when visible', link: '/guide/load-when-visible' }, + { text: 'Merging props', link: '/guide/merging-props' }, + { text: 'Remembering state', link: '/guide/remembering-state' }, + ], + }, + { + text: 'Security', + items: [ + { text: 'Authentication', link: '/guide/authentication' }, + { text: 'Authorization', link: '/guide/authorization' }, + { text: 'CSRF protection', link: '/guide/csrf-protection' }, + { text: 'History encryption', link: '/guide/history-encryption' }, + ], + }, + { + text: 'Advanced', + items: [ + { text: 'Asset versioning', link: '/guide/asset-versioning' }, + { text: 'Code splitting', link: '/guide/code-splitting' }, + { text: 'Error handling', link: '/guide/error-handling' }, + { text: 'Events', link: '/guide/events' }, + { text: 'Progress indicators', link: '/guide/progress-indicators' }, + { text: 'Scroll management', link: '/guide/scroll-management' }, { - text: "Server-side rendering", - link: "/guide/server-side-rendering", + text: 'Server-side rendering', + link: '/guide/server-side-rendering', }, + { text: 'Testing', link: '/guide/testing' }, ], }, ], - "/cookbook": [ + '/cookbook': [ { items: [ { - text: "Integrations", + text: 'Integrations', items: [ - { text: "shadcn/ui", link: "/cookbook/integrating-shadcn-ui" }, + { text: 'shadcn/ui', link: '/cookbook/integrating-shadcn-ui' }, ], }, ], @@ -139,24 +156,24 @@ export default defineConfig({ }, search: { - provider: "algolia", + provider: 'algolia', options: { - appId: "NFLARHZ4Q4", - apiKey: "5e79b6038b027b5bb342ea5a31d0a2e8", - indexName: "inertia-rails", + appId: 'NFLARHZ4Q4', + apiKey: '5e79b6038b027b5bb342ea5a31d0a2e8', + indexName: 'inertia-rails', }, }, editLink: { pattern: - "https://github.com/skryukov/inertia_rails-contrib/edit/main/docs/:path", - text: "Edit this page on GitHub", + 'https://github.com/skryukov/inertia_rails-contrib/edit/main/docs/:path', + text: 'Edit this page on GitHub', }, socialLinks: [ - { icon: "github", link: "https://github.com/inertiajs/inertia-rails" }, - { icon: "x", link: "https://x.com/inertiajs" }, - { icon: "discord", link: "https://discord.gg/inertiajs" }, + { icon: 'github', link: 'https://github.com/inertiajs/inertia-rails' }, + { icon: 'x', link: 'https://x.com/inertiajs' }, + { icon: 'discord', link: 'https://discord.gg/inertiajs' }, ], }, -}); +}) diff --git a/docs/.vitepress/theme/frameworksTabs.ts b/docs/.vitepress/theme/frameworksTabs.ts index 4868801..626883d 100644 --- a/docs/.vitepress/theme/frameworksTabs.ts +++ b/docs/.vitepress/theme/frameworksTabs.ts @@ -1,24 +1,24 @@ -const localStorageKey = "vitepress:tabsSharedState"; -const ls = typeof localStorage !== "undefined" ? localStorage : null; +const localStorageKey = 'vitepress:tabsSharedState' +const ls = typeof localStorage !== 'undefined' ? localStorage : null const getLocalStorageValue = (): Record => { - const rawValue = ls?.getItem(localStorageKey); + const rawValue = ls?.getItem(localStorageKey) if (rawValue) { try { - return JSON.parse(rawValue); + return JSON.parse(rawValue) } catch {} } - return {}; -}; + return {} +} const setLocalStorageValue = (v: Record) => { - if (!ls) return; - ls.setItem(localStorageKey, JSON.stringify(v)); -}; + if (!ls) return + ls.setItem(localStorageKey, JSON.stringify(v)) +} export const setupFrameworksTabs = () => { - const v = getLocalStorageValue(); + const v = getLocalStorageValue() if (!v.frameworks) { - setLocalStorageValue({ frameworks: "React" }); + setLocalStorageValue({ frameworks: 'React' }) } -}; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index b21c9a1..fc1acce 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,22 +1,22 @@ // https://vitepress.dev/guide/custom-theme -import { h } from "vue"; -import type { Theme } from "vitepress"; -import DefaultTheme from "vitepress/theme"; -import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client"; -import { setupFrameworksTabs } from "./frameworksTabs"; -import "./style.css"; +import type { Theme } from 'vitepress' +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' +import DefaultTheme from 'vitepress/theme' +import { h } from 'vue' +import { setupFrameworksTabs } from './frameworksTabs' +import './style.css' export default { extends: DefaultTheme, Layout: () => { return h(DefaultTheme.Layout, null, { // https://vitepress.dev/guide/extending-default-theme#layout-slots - }); + }) }, enhanceApp({ app, router, siteData }) { - enhanceAppWithTabs(app); + enhanceAppWithTabs(app) }, setup() { - setupFrameworksTabs(); + setupFrameworksTabs() }, -} satisfies Theme; +} satisfies Theme diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 83bf35e..400f66d 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -153,11 +153,11 @@ padding: 0 !important; } -.plugin-tabs--content div[class*="language-"] { +.plugin-tabs--content div[class*='language-'] { background-color: var(--vp-code-block-bg) !important; } -.plugin-tabs--content div[class*="language-"]:first-child { +.plugin-tabs--content div[class*='language-']:first-child { border-top-left-radius: 0 !important; border-top-right-radius: 0 !important; } diff --git a/docs/.vitepress/vitepress-plugin-tabs/ruleBlockTab.ts b/docs/.vitepress/vitepress-plugin-tabs/ruleBlockTab.ts new file mode 100644 index 0000000..ea1b618 --- /dev/null +++ b/docs/.vitepress/vitepress-plugin-tabs/ruleBlockTab.ts @@ -0,0 +1,102 @@ +import type { RuleBlock } from 'markdown-it/lib/parser_block' + +const tabMarker = '=' +const tabMarkerCode = tabMarker.charCodeAt(0) +const minTabMarkerLen = 2 + +export const ruleBlockTab: RuleBlock = (state, startLine, endLine, silent) => { + let pos = state.bMarks[startLine] + state.tShift[startLine] + const max = state.eMarks[startLine] + + // @ts-expect-error markdown-it-container uses 'container' + if (state.parentType !== 'container') { + return false + } + + if (pos + minTabMarkerLen > max) { + return false + } + + const marker = state.src.charCodeAt(pos) + if (marker !== tabMarkerCode) { + return false + } + + // scan marker length + const mem = pos + pos = state.skipChars(pos + 1, marker) + const tabMarkerLen = pos - mem + + if (tabMarkerLen < minTabMarkerLen - 1) { + return false + } + + // for validation mode + if (silent) { + return true + } + + // search for the end of the block + let nextLine = startLine + let endStart = mem + let endPos = pos + + for (;;) { + nextLine++ + if (nextLine >= endLine) { + break // unclosed block is autoclosed + } + + endStart = state.bMarks[nextLine] + state.tShift[nextLine] + const max = state.eMarks[nextLine] + + if (endStart < max && state.sCount[nextLine] < state.blkIndent) { + // non-empty line with negative indent should stop the list: + // - ``` + // test + break + } + + const startCharCode = state.src.charCodeAt(endStart) + if (startCharCode !== tabMarkerCode) { + continue + } + + const p = state.skipChars(endStart + 1, marker) + if (p - endStart !== tabMarkerLen) { + continue + } + endPos = p + break + } + + const oldParent = state.parentType + const oldLineMax = state.lineMax + // @ts-expect-error use 'tab' for this rule + state.parentType = 'tab' + // this will prevent lazy continuations from ever going past our end marker + state.lineMax = nextLine + + state.src + .slice(pos, max) + .trimStart() + .split('|') + .forEach((label) => { + const startToken = state.push('tab_open', 'div', 1) + startToken.markup = state.src.slice(mem, pos) + startToken.block = true + startToken.info = label + startToken.map = [startLine, nextLine - 1] + + state.md.block.tokenize(state, startLine + 1, nextLine) + + const endToken = state.push('tab_close', 'div', -1) + endToken.markup = state.src.slice(endStart, endPos) + endToken.block = true + }) + + state.parentType = oldParent + state.lineMax = oldLineMax + state.line = nextLine + return true +} diff --git a/docs/.vitepress/vitepress-plugin-tabs/tabsMarkdownPlugin.ts b/docs/.vitepress/vitepress-plugin-tabs/tabsMarkdownPlugin.ts new file mode 100644 index 0000000..d372397 --- /dev/null +++ b/docs/.vitepress/vitepress-plugin-tabs/tabsMarkdownPlugin.ts @@ -0,0 +1,47 @@ +import type MarkdownIt from 'markdown-it' +import container from 'markdown-it-container' +import type Renderer from 'markdown-it/lib/renderer' +import type Token from 'markdown-it/lib/token' +import { ruleBlockTab } from './ruleBlockTab' + +type Params = { + shareStateKey: string | undefined +} + +const parseTabsParams = (input: string): Params => { + const match = input.match(/key:(\S+)/) + return { + shareStateKey: match?.[1], + } +} + +export const tabsMarkdownPlugin = (md: MarkdownIt) => { + md.use(container, 'tabs', { + render(tokens: Token[], index: number) { + const token = tokens[index] + if (token.nesting === 1) { + const params = parseTabsParams(token.info) + const shareStateKeyProp = params.shareStateKey + ? `sharedStateKey="${md.utils.escapeHtml(params.shareStateKey)}"` + : '' + return `\n` + } else { + return `\n` + } + }, + }) + + md.block.ruler.after('container_tabs', 'tab', ruleBlockTab) + const renderTab: Renderer.RenderRule = (tokens, index) => { + const token = tokens[index] + if (token.nesting === 1) { + const label = token.info + const labelProp = `label="${md.utils.escapeHtml(label)}"` + return `\n` + } else { + return `\n` + } + } + md.renderer.rules['tab_open'] = renderTab + md.renderer.rules['tab_close'] = renderTab +} diff --git a/docs/cookbook/integrating-shadcn-ui.md b/docs/cookbook/integrating-shadcn-ui.md index f80de12..79664bd 100644 --- a/docs/cookbook/integrating-shadcn-ui.md +++ b/docs/cookbook/integrating-shadcn-ui.md @@ -42,40 +42,35 @@ Installing Inertia's Rails adapter Let's configure our project to work seamlessly with `shadcn/ui`. Choose your path based on whether you're using TypeScript or JavaScript. - :::tabs key:languages == TypeScript You'll need to configure two files. First, update your `tsconfig.app.json`: -```json5 +```json lines { "compilerOptions": { // ... "baseUrl": ".", "paths": { - "@/*": [ - "./app/frontend/*" - ] + "@/*": ["./app/frontend/*"] } - }, + } // ... } ``` Then, set up your `tsconfig.json` to match `shadcn/ui`'s requirements (note the `baseUrl` and `paths` properties are different from the `tsconfig.app.json`): -```json5 +```json lines { //... "compilerOptions": { /* Required for shadcn-ui/ui */ "baseUrl": "./app/frontend", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } } } diff --git a/docs/guide/client-side-setup.md b/docs/guide/client-side-setup.md index 216f979..ff3bfa9 100644 --- a/docs/guide/client-side-setup.md +++ b/docs/guide/client-side-setup.md @@ -10,13 +10,7 @@ Once you have your [server-side framework configured](/guide/server-side-setup.m First, install the Inertia client-side adapter corresponding to your framework of choice. :::tabs key:frameworks -== Vue 2 - -```shell -npm install @inertiajs/vue2 vue@^2 -``` - -== Vue 3 +== Vue ```shell npm install @inertiajs/vue3 vue @@ -28,7 +22,7 @@ npm install @inertiajs/vue3 vue npm install @inertiajs/react react react-dom ``` -== Svelte +== Svelte 4|Svelte 5 ```shell npm install @inertiajs/svelte svelte @@ -41,29 +35,7 @@ npm install @inertiajs/svelte svelte Next, update your main JavaScript file to boot your Inertia app. To accomplish this, we'll initialize the client-side framework with the base Inertia component. :::tabs key:frameworks -== Vue 2 - -```js -// frontend/entrypoints/inertia.js -import Vue from 'vue' -import { createInertiaApp } from '@inertiajs/vue2' - -createInertiaApp({ - resolve: (name) => { - const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) - return pages[`../pages/${name}.vue`] - }, - setup({ el, App, props, plugin }) { - Vue.use(plugin) - - new Vue({ - render: (h) => h(App, props), - }).$mount(el) - }, -}) -``` - -== Vue 3 +== Vue ```js // frontend/entrypoints/inertia.js @@ -103,7 +75,7 @@ createInertiaApp({ }) ``` -== Svelte +== Svelte 4 ```js // frontend/entrypoints/inertia.js @@ -120,6 +92,24 @@ createInertiaApp({ }) ``` +== Svelte 5 + +```js +// frontend/entrypoints/inertia.js +import { createInertiaApp } from '@inertiajs/svelte' +import { mount } from 'svelte' + +createInertiaApp({ + resolve: (name) => { + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] + }, + setup({ el, App, props }) { + mount(App, { target: el, props }) + }, +}) +``` + ::: The `setup` callback receives everything necessary to initialize the client-side framework, including the root Inertia `App` component. @@ -129,34 +119,13 @@ The `setup` callback receives everything necessary to initialize the client-side The `resolve` callback tells Inertia how to load a page component. It receives a page name (string), and returns a page component module. How you implement this callback depends on which bundler (Vite or Webpack) you're using. :::tabs key:frameworks -== Vue 2 +== Vue ```js // Vite // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('../pages/**/*.vue', {eager: true}) - return pages[`../pages/${name}.vue`] - }, - // ... -}) - -// Webpacker/Shakapacker -// javascript/packs/inertia.js -createInertiaApp({ - resolve: name => require(`../pages/${name}`), - // ... -}) -``` - -== Vue 3 - -```js -// Vite -// frontend/entrypoints/inertia.js -createInertiaApp({ - resolve: name => { + resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) return pages[`../pages/${name}.vue`] }, @@ -166,7 +135,7 @@ createInertiaApp({ // Webpacker/Shakapacker // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}`), + resolve: (name) => require(`../pages/${name}`), // ... }) ``` @@ -177,8 +146,8 @@ createInertiaApp({ // Vite // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('../pages/**/*.jsx', {eager: true}) + resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.jsx', { eager: true }) return pages[`../pages/${name}.jsx`] }, //... @@ -187,19 +156,19 @@ createInertiaApp({ // Webpacker/Shakapacker // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}`), + resolve: (name) => require(`../pages/${name}`), //... }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // Vite // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('../pages/**/*.svelte', {eager: true}) + resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) return pages[`../pages/${name}.svelte`] }, //... @@ -208,7 +177,7 @@ createInertiaApp({ // Webpacker/Shakapacker // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}.svelte`), + resolve: (name) => require(`../pages/${name}.svelte`), //... }) ``` diff --git a/docs/guide/code-splitting.md b/docs/guide/code-splitting.md index c664251..b62f32f 100644 --- a/docs/guide/code-splitting.md +++ b/docs/guide/code-splitting.md @@ -11,27 +11,12 @@ To enable code splitting you'll need to tweak the resolve callback in your `crea Vite enables code splitting (or lazy-loading as they call it) by default when using their `import.meta.glob()` function, so simply omit the `{ eager: true }` option, or set it to false, to disable eager loading. :::tabs key:frameworks -== Vue 2 +== Vue ```js // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('../pages/**/*.vue', {eager: true}) // [!code --] - return pages[`../pages/${name}.vue`] // [!code --] - const pages = import.meta.glob('../pages/**/*.vue') // [!code ++] - return pages[`../pages/${name}.vue`]() // [!code ++] - }, - //... -}) -``` - -== Vue 3 - -```js -// frontend/entrypoints/inertia.js -createInertiaApp({ - resolve: name => { + resolve: (name) => { const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) // [!code --] return pages[`../pages/${name}.vue`] // [!code --] const pages = import.meta.glob('../pages/**/*.vue') // [!code ++] @@ -46,7 +31,7 @@ createInertiaApp({ ```js // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { + resolve: (name) => { const pages = import.meta.glob('../pages/**/*.jsx', { eager: true }) // [!code --] return pages[`../pages/${name}.jsx`] // [!code --] const pages = import.meta.glob('../pages/**/*.jsx') // [!code ++] @@ -56,12 +41,12 @@ createInertiaApp({ }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // frontend/entrypoints/inertia.js createInertiaApp({ - resolve: name => { + resolve: (name) => { const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) // [!code --] return pages[`../pages/${name}.svelte`] // [!code --] const pages = import.meta.glob('../pages/**/*.svelte') // [!code ++] @@ -95,24 +80,13 @@ Next, create a `.babelrc` file in your project with the following configuration: Finally, update the `resolve` callback in your app's initialization code to use `import` instead of `require`. :::tabs key:frameworks -== Vue 2 - -```js -// javascript/packs/inertia.js -createInertiaApp({ - resolve: name => require(`../pages/${name}`), // [!code ii] - resolve: name => import(`../pages/${name}`), // [!code ++] - //... -}) -``` - -== Vue 3 +== Vue ```js // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}`), // [!code ii] - resolve: name => import(`../pages/${name}`), // [!code ++] + resolve: (name) => require(`../pages/${name}`), // [!code ii] + resolve: (name) => import(`../pages/${name}`), // [!code ++] //... }) ``` @@ -122,19 +96,19 @@ createInertiaApp({ ```js // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}`), // [!code ii] - resolve: name => import(`../pages/${name}`), // [!code ++] + resolve: (name) => require(`../pages/${name}`), // [!code ii] + resolve: (name) => import(`../pages/${name}`), // [!code ++] //... }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // javascript/packs/inertia.js createInertiaApp({ - resolve: name => require(`../pages/${name}.svelte`), // [!code ii] - resolve: name => import(`../pages/${name}.svelte`), // [!code ++] + resolve: (name) => require(`../pages/${name}.svelte`), // [!code ii] + resolve: (name) => import(`../pages/${name}.svelte`), // [!code ++] //... }) ``` diff --git a/docs/guide/csrf-protection.md b/docs/guide/csrf-protection.md index 81df703..e882de3 100644 --- a/docs/guide/csrf-protection.md +++ b/docs/guide/csrf-protection.md @@ -7,19 +7,7 @@ Inertia's Rails adapter automatically includes the proper CSRF token when making However, if you need to handle CSRF protection manually, one approach is to include the CSRF token as a prop on every response. You can then use the token when making Inertia requests. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', { - _token: this.$page.props.csrf_token, - name: 'John Doe', - email: 'john.doe@example.com', -}) -``` - -== Vue 3 +== Vue ```js import { router, usePage } from '@inertiajs/vue3' @@ -47,7 +35,7 @@ router.post('/users', { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { page, router } from '@inertiajs/svelte' diff --git a/docs/guide/deferred-props.md b/docs/guide/deferred-props.md new file mode 100644 index 0000000..7918d79 --- /dev/null +++ b/docs/guide/deferred-props.md @@ -0,0 +1,195 @@ +# Deferred props + +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 + +To defer a prop, you can use the defer method when returning your response. This method receives a callback that returns the prop data. The callback will be executed in a separate request after the initial page render. + +```ruby +class UsersController < ApplicationController + def index + render inertia: 'Users/Index', props: { + users: -> { User.all }, + roles: -> { Role.all }, + permissions: InertiaRails.defer { Permission.all }, + + # Also works with a lambda: + # permissions: InertiaRails.defer(-> { Permission.all }), + + # Also works with a simple value, + # but this way the prop is always evaluated, + # even if not included: + # permissions: InertiaRails.defer(Permission.all), + } + end +end +``` + +### Grouping requests + +By default, all deferred props get fetched in one request after the initial page is rendered, but you can choose to fetch data in parallel by grouping props together. + +```ruby +class UsersController < ApplicationController + def index + render inertia: 'Users/Index', props: { + users: -> { User.all }, + roles: -> { Role.all }, + permissions: InertiaRails.defer { Permission.all }, + # using block: + teams: InertiaRails.defer(group: 'attributes') { Team.all }, + # using lambda: + projects: InertiaRails.defer(-> { Project.all }, group: 'attributes'), + tasks: InertiaRails.defer(-> { Task.all }, group: 'attributes'), + } + end +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. + +## 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. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Deferred } from '@inertiajs/react' + +export default () => ( + Loading...}> + + +) +``` + +== Svelte 4 + +```svelte + + + + +
Loading...
+
+ + {#each permissions as permission} + + {/each} +
+``` + +== Svelte 5 + +```svelte + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + {#each permissions as permission} + + {/each} +
+``` + +::: + +If you need to wait for multiple deferred props to become available, you can specify an array to the `data` prop. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Deferred } from '@inertiajs/react' + +export default () => ( + Loading...}> + + +) +``` + +== Svelte 4 + +```svelte + + + + +
Loading...
+
+ + +
+``` + +== Svelte 5 + +```svelte + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + +
+``` + +::: diff --git a/docs/guide/error-handling.md b/docs/guide/error-handling.md index 05a19a7..04faffc 100644 --- a/docs/guide/error-handling.md +++ b/docs/guide/error-handling.md @@ -31,46 +31,7 @@ end You may have noticed we're returning an `Error` page component in the example above. You'll need to actually create this component, which will serve as the generic error page for your application. Here's an example error component you can use as a starting point. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue
-

{status}: {title}

+

{status}: {title}

{description}
``` +== Svelte 5 + +```svelte + + +
+

{titles[status]}

+
{description[status]}
+
+``` + ::: diff --git a/docs/guide/events.md b/docs/guide/events.md index 3afdfc7..8ce9f96 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -7,17 +7,7 @@ Inertia provides an event system that allows you to "hook into" the various life To register an event listener, use the `router.on()` method. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('start', (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -37,7 +27,7 @@ router.on('start', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -52,17 +42,7 @@ router.on('start', (event) => { Under the hood, Inertia uses native browser events, so you can also interact with Inertia events using the typical event methods you may already be familiar with - just be sure to prepend `inertia:` to the event name. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -document.addEventListener('inertia:start', (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -82,7 +62,7 @@ document.addEventListener('inertia:start', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -99,20 +79,7 @@ document.addEventListener('inertia:start', (event) => { When you register an event listener, Inertia automatically returns a callback that can be invoked to remove the event listener. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -let removeStartEventListener = router.on('start', (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) -}) - -// Remove the listener... -removeStartEventListener() -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -138,7 +105,7 @@ let removeStartEventListener = router.on('start', (event) => { removeStartEventListener() ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -156,24 +123,7 @@ removeStartEventListener() Combined with hooks, you can automatically remove the event listener when components unmount. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -export default { - mounted() { - this.$once( - 'hook:destroyed', - router.on('start', (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) - }), - ) - }, -} -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -199,7 +149,7 @@ useEffect(() => { }, []) ``` -== Svelte +== Svelte 4 ```js import { router } from '@inertiajs/svelte' @@ -212,27 +162,24 @@ onMount(() => { }) ``` -::: - -Alternatively, if you're using native browser events, you can remove the event listener using `removeEventListener()`. - -:::tabs key:frameworks -== Vue 2 +== Svelte 5 ```js -import { router } from '@inertiajs/vue2' +import { router } from '@inertiajs/svelte' -let startEventListener = (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) -} +$effect(() => { + return router.on('start', (event) => { + console.log(`Starting a visit to ${event.detail.visit.url}`) + }) +}) +``` -document.addEventListener('inertia:start', startEventListener) +::: -// Remove the listener... -document.removeEventListener('inertia:start', startEventListener) -``` +Alternatively, if you're using native browser events, you can remove the event listener using `removeEventListener()`. -== Vue 3 +:::tabs key:frameworks +== Vue ```js import { router } from '@inertiajs/vue3' @@ -262,7 +209,7 @@ document.addEventListener('inertia:start', startEventListener) document.removeEventListener('inertia:start', startEventListener) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -284,19 +231,7 @@ document.removeEventListener('inertia:start', startEventListener) Some events, such as `before`, `invalid`, and `error`, support cancellation, allowing you to prevent Inertia's default behavior. Just like native events, the event will be cancelled if only one event listener calls `event.preventDefault()`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('before', (event) => { - if (!confirm('Are you sure you want to navigate away?')) { - event.preventDefault() - } -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -320,7 +255,7 @@ router.on('before', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -337,17 +272,7 @@ router.on('before', (event) => { For convenience, if you register your event listener using `router.on()`, you can cancel the event by returning `false` from the listener. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('before', (event) => { - return confirm('Are you sure you want to navigate away?') -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -367,7 +292,7 @@ router.on('before', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -386,17 +311,7 @@ Note, browsers do not allow cancelling the native `popstate` event, so preventin The `before` event fires when a request is about to be made to the server. This is useful for intercepting visits. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('before', (event) => { - console.log(`About to make a visit to ${event.detail.visit.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -416,7 +331,7 @@ router.on('before', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -431,17 +346,7 @@ router.on('before', (event) => { The primary purpose of this event is to allow you to prevent a visit from happening. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('before', (event) => { - return confirm('Are you sure you want to navigate away?') -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -461,7 +366,7 @@ router.on('before', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -478,17 +383,7 @@ router.on('before', (event) => { The `start` event fires when a request to the server has started. This is useful for displaying loading indicators. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('start', (event) => { - console.log(`Starting a visit to ${event.detail.visit.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -508,7 +403,7 @@ router.on('start', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -527,17 +422,7 @@ The `start` event is not cancelable. The `progress` event fires as progress increments during file uploads. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('progress', (event) => { - this.form.progress = event.detail.progress.percentage -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -557,7 +442,7 @@ router.on('progress', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -576,17 +461,7 @@ The `progress` event is not cancelable. The `success` event fires on successful page visits, unless validation errors are present. However, this does not include history visits. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('success', (event) => { - console.log(`Successfully made a visit to ${event.detail.page.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -606,7 +481,7 @@ router.on('success', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -625,17 +500,7 @@ The `success` event is not cancelable. The `error` event fires when validation errors are present on "successful" page visits. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('error', (errors) => { - console.log(errors) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -655,7 +520,7 @@ router.on('error', (errors) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -676,18 +541,7 @@ The invalid event fires when a non-Inertia response is received from the server, This event is fired for all response types, including `200`, `400`, and `500` response codes. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('invalid', (event) => { - console.log(`An invalid Inertia response was received.`) - console.log(event.detail.response) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -709,7 +563,7 @@ router.on('invalid', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -725,19 +579,7 @@ router.on('invalid', (event) => { You may cancel the `invalid` event to prevent Inertia from showing the non-Inertia response modal. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('invalid', (event) => { - event.preventDefault() - - // Handle the invalid response yourself... -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -761,7 +603,7 @@ router.on('invalid', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -780,18 +622,7 @@ router.on('invalid', (event) => { The `exception` event fires on unexpected XHR errors such as network interruptions. In addition, this event fires for errors generated when resolving page components. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('exception', (event) => { - console.log(`An unexpected error occurred during an Inertia visit.`) - console.log(event.detail.error) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -813,7 +644,7 @@ router.on('exception', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -829,18 +660,7 @@ router.on('exception', (event) => { You may cancel the `exception` event to prevent the error from being thrown. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('exception', (event) => { - event.preventDefault() - // Handle the error yourself -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -862,7 +682,7 @@ router.on('exception', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -882,17 +702,7 @@ This event will _not_ fire for XHR requests that receive `400` and `500` level r The `finish` event fires after an XHR request has completed for both "successful" and "unsuccessful" responses. This event is useful for hiding loading indicators. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('finish', (event) => { - NProgress.done() -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -912,7 +722,7 @@ router.on('finish', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -931,17 +741,7 @@ The `finish` event is not cancelable. The `navigate` event fires on successful page visits, as well as when navigating through history. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.on('navigate', (event) => { - console.log(`Navigated to ${event.detail.page.url}`) -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -961,7 +761,7 @@ router.on('navigate', (event) => { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' diff --git a/docs/guide/file-uploads.md b/docs/guide/file-uploads.md index 5e588f1..5046a48 100644 --- a/docs/guide/file-uploads.md +++ b/docs/guide/file-uploads.md @@ -7,17 +7,7 @@ When making Inertia requests that include files (even nested files), Inertia wil If you would like the request to always use a `FormData` object regardless of whether a file is present in the data, you may provide the `forceFormData` option when making the request. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - forceFormData: true, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -37,7 +27,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -59,42 +49,7 @@ You can learn more about the `FormData` interface via its [MDN documentation](ht Let's examine a complete file upload example using Inertia. This example includes both a `name` text input and an `avatar` file input. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue
- $form.avatar = e.target.files[0]} /> + ($form.avatar = e.target.files[0])} /> + {#if $form.progress} + + {$form.progress.percentage}% + + {/if} + +
+``` + +== Svelte 5 + +```svelte + + +
+ + ($form.avatar = e.target.files[0])} /> {#if $form.progress} {$form.progress.percentage}% @@ -200,24 +181,7 @@ However, some frameworks, such as Laravel and Rails, support form method spoofin > For more info see [`Rack::MethodOverride`](https://github.com/rack/rack/blob/main/lib/rack/method_override.rb). :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post(`/users/${user.id}`, { - _method: 'put', - avatar: form.avatar, -}) - -// or - -form.post(`/users/${user.id}`, { - headers: { 'X-HTTP-METHOD-OVERRIDE': 'put' }, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -251,7 +215,7 @@ form.post(`/users/${user.id}`, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' diff --git a/docs/guide/forms.md b/docs/guide/forms.md index fb7d9d4..0c25f01 100644 --- a/docs/guide/forms.md +++ b/docs/guide/forms.md @@ -5,44 +5,7 @@ While it's possible to make classic HTML form submissions with Inertia, it's not recommended since they cause full-page reloads. Instead, it's better to intercept form submissions and then make the [request using Inertia](/guide/manual-visits.md). :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue - + - + - + - + + + + +``` + +== Svelte 5 + +```svelte + + +
+ + + + + + + +
@@ -188,42 +189,7 @@ For a full discussion on handling and displaying [validation](/guide/validation. Since working with forms is so common, Inertia includes a form helper designed to help reduce the amount of boilerplate code needed for handling typical form submissions. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue
@@ -326,23 +292,44 @@ function submit() {
``` -::: +== Svelte 5 -To submit the form, you may use the `get`, `post`, `put`, `patch` and `delete` methods. +```svelte + + +
+ + {#if $form.errors.email} +
{$form.errors.email}
+ {/if} + + {#if $form.errors.password} +
{$form.errors.password}
+ {/if} + Remember Me + +
``` -== Vue 3 +::: + +To submit the form, you may use the `get`, `post`, `put`, `patch` and `delete` methods. + +:::tabs key:frameworks +== Vue ```js form.submit(method, url, options) @@ -366,7 +353,7 @@ patch(url, options) destroy(url, options) ``` -== Svelte +== Svelte 4|Svelte 5 ```js $form.submit(method, url, options) @@ -382,16 +369,7 @@ $form.delete(url, options) The submit methods support all of the typical [visit options](/guide/manual-visits.md), such as `preserveState`, `preserveScroll`, and event callbacks, which can be helpful for performing tasks on successful form submissions. For example, you might use the `onSuccess` callback to reset inputs to their original state. :::tabs key:frameworks -== Vue 2 - -```js -form.post('/profile', { - preserveScroll: true, - onSuccess: () => form.reset('password'), -}) -``` - -== Vue 3 +== Vue ```js form.post('/profile', { @@ -411,7 +389,7 @@ onSuccess: () => reset('password'), }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js $form.post('/profile', { @@ -425,18 +403,7 @@ $form.post('/profile', { If you need to modify the form data before it's sent to the server, you can do so via the `transform()` method. :::tabs key:frameworks -== Vue 2 - -```js -form - .transform((data) => ({ - ...data, - remember: data.remember ? 'on' : '', - })) - .post('/login') -``` - -== Vue 3 +== Vue ```js form @@ -458,7 +425,7 @@ transform((data) => ({ })) ``` -== Svelte +== Svelte 4|Svelte 5 ```js $form @@ -474,13 +441,7 @@ $form You can use the `processing` property to track if a form is currently being submitted. This can be helpful for preventing double form submissions by disabling the submit button. :::tabs key:frameworks -== Vue 2 - -```vue - -``` - -== Vue 3 +== Vue ```vue @@ -494,7 +455,7 @@ const { processing } = useForm({ ... }) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte @@ -505,15 +466,7 @@ const { processing } = useForm({ ... }) If your form is uploading files, the current progress event is available via the `progress` property, allowing you to easily display the upload progress. :::tabs key:frameworks -== Vue 2 - -```vue - - {{ form.progress.percentage }}% - -``` - -== Vue 3 +== Vue ```vue @@ -533,7 +486,7 @@ const { progress } = useForm({ ... }) )} ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte {#if $form.progress} @@ -548,13 +501,7 @@ const { progress } = useForm({ ... }) If there are form validation errors, they are available via the `errors` property. When building Rails powered Inertia applications, form errors will automatically be populated when your application throws instances of `ActiveRecord::RecordInvalid`, such as when using `#save!`. :::tabs key:frameworks -== Vue 2 - -```vue -
{{ form.errors.email }}
-``` - -== Vue 3 +== Vue ```vue
{{ form.errors.email }}
@@ -568,7 +515,7 @@ const { errors } = useForm({ ... }) {errors.email &&
{errors.email}
} ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte {#if $form.errors.email} @@ -584,17 +531,7 @@ const { errors } = useForm({ ... }) To determine if a form has any errors, you may use the `hasErrors` property. To clear form errors, use the `clearErrors()` method. :::tabs key:frameworks -== Vue 2 - -```js -// Clear all errors... -form.clearErrors() - -// Clear errors for specific fields... -form.clearErrors('field', 'anotherfield') -``` - -== Vue 3 +== Vue ```js // Clear all errors... @@ -616,7 +553,7 @@ clearErrors() clearErrors('field', 'anotherfield') ``` -== Svelte +== Svelte 4|Svelte 5 ```js // Clear all errors... @@ -631,20 +568,7 @@ $form.clearErrors('field', 'anotherfield') If you're using a client-side input validation libraries or do client-side validation manually, you can set your own errors on the form using the `setErrors()` method. :::tabs key:frameworks -== Vue 2 - -```js -// Set a single error... -form.setError('field', 'Your error message.') - -// Set multiple errors at once... -form.setError({ - foo: 'Your error message for the foo field.', - bar: 'Some other error for the bar field.', -}) -``` - -== Vue 3 +== Vue ```js // Set a single error... @@ -672,7 +596,7 @@ setError({ }); ``` -== Svelte +== Svelte 4|Svelte 5 ```js // Set a single error @@ -695,17 +619,7 @@ When a form has been successfully submitted, the `wasSuccessful` property will b To reset the form's values back to their default values, you can use the `reset()` method. :::tabs key:frameworks -== Vue 2 - -```js -// Reset the form... -form.reset() - -// Reset specific fields... -form.reset('field', 'anotherfield') -``` - -== Vue 3 +== Vue ```js // Reset the form... @@ -727,7 +641,7 @@ reset() reset('field', 'anotherfield') ``` -== Svelte +== Svelte 4|Svelte 5 ```js // Reset the form... @@ -742,23 +656,7 @@ $form.reset('field', 'anotherfield') If your form's default values become outdated, you can use the `defaults()` method to update them. Then, the form will be reset to the correct values the next time the `reset()` method is invoked. :::tabs key:frameworks -== Vue 2 - -```js -// Set the form's current values as the new defaults... -form.defaults() - -// Update the default value of a single field... -form.defaults('email', 'updated-default@example.com') - -// Update the default value of multiple fields... -form.defaults({ - name: 'Updated Example', - email: 'updated-default@example.com', -}) -``` - -== Vue 3 +== Vue ```js // Set the form's current values as the new defaults... @@ -792,7 +690,7 @@ setDefaults({ }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // Set the form's current values as the new defaults... @@ -813,13 +711,7 @@ $form.defaults({ To determine if a form has any changes, you may use the `isDirty` property. :::tabs key:frameworks -== Vue 2 - -```vue -
There are unsaved form changes.
-``` - -== Vue 3 +== Vue ```vue
There are unsaved form changes.
@@ -833,7 +725,7 @@ const { isDirty } = useForm({ ... }) {isDirty &&
There are unsaved form changes.
} ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte {#if $form.isDirty} @@ -846,13 +738,7 @@ const { isDirty } = useForm({ ... }) To cancel a form submission, use the `cancel()` method. :::tabs key:frameworks -== Vue 2 - -```vue -form.cancel() -``` - -== Vue 3 +== Vue ```vue form.cancel() @@ -866,7 +752,7 @@ const { cancel } = useForm({ ... }) cancel() ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte $form.cancel() @@ -877,18 +763,9 @@ $form.cancel() To instruct Inertia to store a form's data and errors in [history state](/guide/remembering-state.md), you can provide a unique form key as the first argument when instantiating your form. :::tabs key:frameworks -== Vue 2 - -```vue -import { useForm } from '@inertiajs/vue2' - -form: useForm('CreateUser', data) -form: useForm(`EditUser:${this.user.id}`, data) -``` - -== Vue 3 +== Vue -```vue +```js import { useForm } from '@inertiajs/vue3' const form = useForm('CreateUser', data) @@ -897,16 +774,16 @@ const form = useForm(`EditUser:${user.id}`, data) == React -```jsx +```js import { useForm } from '@inertiajs/react' const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js import { useForm } from '@inertiajs/svelte' const form = useForm('CreateUser', data) diff --git a/docs/guide/history-encryption.md b/docs/guide/history-encryption.md new file mode 100644 index 0000000..d2efe01 --- /dev/null +++ b/docs/guide/history-encryption.md @@ -0,0 +1,58 @@ +# History encryption + +Imagine a scenario where your user is authenticated, browses privileged information on your site, then logs out. If they press the back button, they can still see the privileged information that is stored in the window's history state. This is a security risk. To prevent this, Inertia.js provides a history encryption feature. + +## How it works + +When 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. We store the corresponding key in the browser's session storage. When the user navigates back to a page, we decrypt the data using the key stored in the session storage. + +Once you instruct Inertia to clear your history state, we simply clear the existing key from session storage roll a new one. If we attempt to decrypt the history state with the new key, it will fail an Inertia will make a fresh request back to your server for the page data. + +> [!NOTE] +> History encryption relies on `window.crypto.subtle` which is only available in secure environments (sites with SSL enabled). + +## Opting in + +History encryption is an opt-in feature. There are several methods for enabling it: + +### Global encryption + +If you'd like to enable history encryption globally, set the `encrypt_history` config value to `true`. + +You are able to opt out of encryption on specific pages by passing `false` to the `encrypt_history` option: + +```ruby +render inertia: 'Homepage', props: {}, encrypt_history: false +``` + +### Per-request encryption + +To encrypt the history of an individual request, simply pass `true` to the `encrypt_history` option: + +```ruby +render inertia: 'Dashboard', props: {}, encrypt_history: true +``` + +### Controller-level encryption + +You can also enable history encryption for all actions in a controller by setting the `encrypt_history` config value in the controller: + +```ruby +class DashboardController < ApplicationController + inertia_config(encrypt_history: true) + + # ... +end +``` + +## Clearing history + +To clear the history state, you can pass the `clear_history` option to the `render` method: + +```ruby +render inertia: 'Dashboard', props: {}, clear_history: true +``` + +Once the response has rendered on the client, the encryption key will be rotated, rendering the previous history state unreadable. + +You can also clear history on the client site by calling `router.clearHistory()`. diff --git a/docs/guide/links.md b/docs/guide/links.md index 91370b1..6c517f2 100644 --- a/docs/guide/links.md +++ b/docs/guide/links.md @@ -7,21 +7,16 @@ To create links to other pages within an Inertia app, you will typically use the To create an Inertia link, use the Inertia `` component. Any attributes you provide to this component will be proxied to the underlying HTML tag. :::tabs key:frameworks - -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Home -``` - -== Vue 3 +== Vue ```vue + -Home + ``` == React @@ -29,13 +24,15 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' -Home +export default () => Home ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + Home @@ -43,33 +40,25 @@ import { inertia, Link } from '@inertiajs/svelte' ``` > [!TIP] -> The `use:inertia` directive can be applied to any HTML element. +> The `use:inertia` action can be applied to any HTML element. ::: By default, Inertia renders links as anchor `` elements. However, you can change the tag using the `as` prop. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Logout - -// Renders as... - -``` - -== Vue 3 +== Vue ```vue + -Logout - -// Renders as... - + ``` == React @@ -77,50 +66,49 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' -Logout +export default () => ( + + Logout + +) // Renders as... - +// ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia } from '@inertiajs/svelte' + - +Logout -// Renders as... - + + ``` -> [!NOTE] -> Svelte does not support dynamic elements yet, but you can use the `inertia` directive to achieve the same results. - ::: -> [!WARNING] -> Creating `POST/PUT/PATCH/DELETE` anchor `` links is discouraged as it causes "Open Link in New Tab / Window" accessibility issues. Instead, consider using a more appropriate element, such as a ` + + +Logout ``` ::: @@ -148,30 +142,18 @@ import { inertia, Link } from '@inertiajs/svelte' When making `POST` or `PUT` requests, you may wish to add additional data to the request. You can accomplish this using the `data` prop. The provided data can be an `object` or `FormData` instance. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Save -``` - -== Vue 3 +== Vue ```vue + -Save + ``` == React @@ -179,17 +161,26 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Save - +export default () => ( + + Save + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' - - + + + Save ``` @@ -201,20 +192,16 @@ import { inertia, Link } from '@inertiajs/svelte' The `headers` prop allows you to add custom headers to an Inertia link. However, the headers Inertia uses internally to communicate its state to the server take priority and therefore cannot be overwritten. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Save -``` - -== Vue 3 +== Vue ```vue + -Save + ``` == React @@ -222,19 +209,23 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Save - +export default () => ( + + Save + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + - + -Save +Save ``` ::: @@ -244,20 +235,16 @@ import { inertia, Link } from '@inertiajs/svelte' The `replace` prop allows you to specify the browser's history behavior. By default, page visits push (new) state (`window.history.pushState`) into the history; however, it's also possible to replace state (`window.history.replaceState`) by setting the `replace` prop to `true`. This will cause the visit to replace the current history state instead of adding a new history state to the stack. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Home -``` - -== Vue 3 +== Vue ```vue + -Home + ``` == React @@ -265,17 +252,21 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Home - +export default () => ( + + Home + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + -Home +Home Home ``` @@ -287,24 +278,18 @@ import { inertia, Link } from '@inertiajs/svelte' You can preserve a page component's local state using the `preserveState` prop. This will prevent a page component from fully re-rendering. The `preserveState` prop is especially helpful on pages that contain forms, since you can avoid manually repopulating input fields and can also maintain a focused input. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - - - -Search -``` - -== Vue 3 +== Vue ```vue + - + ``` == React @@ -312,21 +297,31 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - +export default () => ( + <> + -Search + + Search + + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + - + - + -Search +Search ``` ::: @@ -336,20 +331,16 @@ import { inertia, Link } from '@inertiajs/svelte' You can use the `preserveScroll` prop to prevent Inertia from automatically resetting the scroll position when making a page visit. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Home -``` - -== Vue 3 +== Vue ```vue + -Home + ``` == React @@ -357,17 +348,21 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Home - +export default () => ( + + Home + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + -Home +Home Home ``` @@ -381,20 +376,16 @@ For more information on managing scroll position, please consult the documentati The `only` prop allows you to specify that only a subset of a page's props (data) should be retrieved from the server on subsequent visits to that page. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Show active -``` - -== Vue 3 +== Vue ```vue + -Show active + ``` == React @@ -402,17 +393,21 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Show active - +export default () => ( + + Show active + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + -Show active +Show active Show active ``` @@ -426,58 +421,32 @@ For more information on this topic, please consult the complete documentation on It's often desirable to set an active state for navigation links based on the current page. This can be accomplished when using Inertia by inspecting the `page` object and doing string comparisons against the `page.url` and `page.component` properties. :::tabs key:frameworks -== Vue 2 +== Vue ```vue -import { Link } from '@inertiajs/vue2' - -// URL exact match... -Users - -// Component exact match... -Users - -// URL starts with (/users, /users/create, /users/1, etc.)... -Users - -// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... -Users -``` + -== Vue 3 + ``` == React @@ -485,37 +454,67 @@ import { Link } from '@inertiajs/vue3' ```jsx import { usePage } from '@inertiajs/react' -const { url, component } = usePage() - -// URL exact match... -Users - -// Component exact match... -Users - -// URL starts with (/users, /users/create, /users/1, etc.)... -Users - -// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... -Users -``` - -== Svelte +export default () => { + const { url, component } = usePage() + + return ( + <> + // URL exact match... + + Users + + // Component exact match... + + Users + + // URL starts with (/users, /users/create, /users/1, etc.)... + + Users + + // Component starts with (Users/Index, Users/Create, Users/Show, etc.)... + + Users + + + ) +} +``` + +== Svelte 4|Svelte 5 ```svelte -import { page } from '@inertiajs/svelte' - -// URL exact match... -Users - -// Component exact match... -Users - -// URL starts with (/users, /users/create, /users/1, etc.)... -Users - -// Component starts with (Users/Index, Users/Create, Users/Show, etc.)... -Users + + + ``` ::: @@ -523,3 +522,7 @@ import { page } from '@inertiajs/svelte' You can perform exact match comparisons (`===`), `startsWith()` comparisons (useful for matching a subset of pages), or even more complex comparisons using regular expressions. Using this approach, you're not limited to just setting class names. You can use this technique to conditionally render any markup on active state, such as different link text or even an SVG icon that represents the link is active. + +## Data loading attribute + +While a link is making an active request, a `data-loading` attribute is added to the link element. This allows you to style the link while it's in a loading state. The attribute is removed once the request is complete. diff --git a/docs/guide/load-when-visible.md b/docs/guide/load-when-visible.md new file mode 100644 index 0000000..a2752e9 --- /dev/null +++ b/docs/guide/load-when-visible.md @@ -0,0 +1,354 @@ +# Load when visible + +Inertia supports lazy loading data on scroll using the Intersection Observer API. It provides the `WhenVisible` component as a convenient way to load data when an element becomes visible in the viewport. + +The `WhenVisible` component accepts a `data` prop that specifies the key of the prop to load. It also accepts a `fallback` prop that specifies a component to render while the data is loading. The `WhenVisible` component should wrap the component that depends on the data. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { WhenVisible } from '@inertiajs/react' + +export default () => ( + Loading...}> + + +) +``` + +== Svelte 4 + +```svelte + + + + +
Loading...
+
+ + {#each permissions as permission} + + {/each} +
+``` + +== Svelte 5 + +```svelte + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + {#each permissions as permission} + + {/each} +
+``` + +::: + +If you'd like to load multiple props when an element becomes visible, you can provide an array to the `data` prop. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { WhenVisible } from '@inertiajs/react' + +export default () => ( + Loading...}> + + +) +``` + +== Svelte 4 + +```svelte + + + + +
Loading...
+
+ + +
+``` + +== Svelte 5 + +```svelte + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + +
+``` + +::: + +## Loading before visible + +If you'd like to start loading data before the element is visible, you can provide a value to the `buffer` prop. The buffer value is a number that represents the number of pixels before the element is visible. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { WhenVisible } from '@inertiajs/react' + +export default () => ( + Loading...}> + + +) +``` + +== Svelte 4 + +```svelte + + + + +
Loading...
+
+ + {#each permissions as permission} + + {/each} +
+``` + +== Svelte 5 + +```svelte + + + + {#snippet fallback()} +
Loading...
+ {/snippet} + + {#each permissions as permission} + + {/each} +
+``` + +::: + +In the above example, the data will start loading 500 pixels before the element is visible. + +By default, the `WhenVisible` component wraps the fallback template in a `div` element so it can ensure the element is visible in the viewport. If you want to customize the wrapper element, you can provide the `as` prop. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { WhenVisible } from '@inertiajs/react' + +export default () => ( + + + +) +``` + +== Svelte 4 + +```svelte + + + + + +``` + +== Svelte 5 + +```svelte + + + + + +``` + +::: + +## Always trigger + +By default, the `WhenVisible` component will only trigger once when the element becomes visible. If you want to always trigger the data loading when the element is visible, you can provide the `always` prop. + +This is useful when you want to load data every time the element becomes visible, such as when the element is at the end of an infinite scroll list and you want to load more data. + +Note that if the data loading request is already in flight, the component will wait until it is finished to start the next request if the element is still visible in the viewport. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { WhenVisible } from '@inertiajs/react' + +export default () => ( + + + +) +``` + +== Svelte 4 + +```svelte + + + + + +``` + +== Svelte 5 + +```svelte + + + + + +``` + +::: diff --git a/docs/guide/manual-visits.md b/docs/guide/manual-visits.md index c705218..0142bb2 100644 --- a/docs/guide/manual-visits.md +++ b/docs/guide/manual-visits.md @@ -3,33 +3,7 @@ In addition to [creating links](/guide/links.md), it's also possible to manually make Inertia visits / requests programmatically via JavaScript. This is accomplished via the `router.visit()` method. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { - method: 'get', - data: {}, - replace: false, - preserveState: false, - preserveScroll: false, - only: [], - headers: {}, - errorBag: null, - forceFormData: false, - onCancelToken: (cancelToken) => {}, - onCancel: () => {}, - onBefore: (visit) => {}, - onStart: (visit) => {}, - onProgress: (progress) => {}, - onSuccess: (page) => {}, - onError: (errors) => {}, - onFinish: (visit) => {}, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -41,9 +15,17 @@ router.visit(url, { preserveState: false, preserveScroll: false, only: [], + except: [], headers: {}, errorBag: null, forceFormData: false, + queryStringArrayFormat: 'brackets', + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, @@ -52,6 +34,8 @@ router.visit(url, { onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, }) ``` @@ -67,9 +51,17 @@ router.visit(url, { preserveState: false, preserveScroll: false, only: [], + except: [], headers: {}, errorBag: null, forceFormData: false, + queryStringArrayFormat: 'brackets', + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, @@ -78,10 +70,12 @@ router.visit(url, { onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -93,9 +87,17 @@ router.visit(url, { preserveState: false, preserveScroll: false, only: [], + except: [], headers: {}, errorBag: null, forceFormData: false, + queryStringArrayFormat: 'brackets', + async: false, + showProgress: true, + fresh: false, + reset: [], + preserveUrl: false, + prefetch: false, onCancelToken: (cancelToken) => {}, onCancel: () => {}, onBefore: (visit) => {}, @@ -104,6 +106,8 @@ router.visit(url, { onSuccess: (page) => {}, onError: (errors) => {}, onFinish: (visit) => {}, + onPrefetching: () => {}, + onPrefetched: () => {}, }) ``` @@ -112,20 +116,7 @@ router.visit(url, { However, it's generally more convenient to use one of Inertia's shortcut request methods. These methods share all the same options as `router.visit()`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.get(url, data, options) -router.post(url, data, options) -router.put(url, data, options) -router.patch(url, data, options) -router.delete(url, options) -router.reload(options) // Uses the current URL -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -151,7 +142,7 @@ router.delete(url, options) router.reload(options) // Uses the current URL ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -173,15 +164,7 @@ The `reload()` method is a convenient, shorthand method that automatically visit When making manual visits, you may use the `method` option to set the request's HTTP method to `get`, `post`, `put`, `patch` or `delete`. The default method is `get`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { method: 'post' }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -197,7 +180,7 @@ import { router } from '@inertiajs/react' router.visit(url, { method: 'post' }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -215,21 +198,7 @@ router.visit(url, { method: 'post' }) You may use the `data` option to add data to the request. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit('/users', { - method: 'post', - data: { - name: 'John Doe', - email: 'john.doe@example.com', - }, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -257,7 +226,7 @@ router.visit('/users', { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -276,18 +245,7 @@ router.visit('/users', { For convenience, the `get()`, `post()`, `put()`, and `patch()` methods all accept data as their second argument. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', { - name: 'John Doe', - email: 'john.doe@example.com', -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -309,7 +267,7 @@ router.post('/users', { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js @@ -328,19 +286,7 @@ email: 'john.doe@example.com', The `headers` option allows you to add custom headers to a request. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - headers: { - 'Custom-Header': 'value', - }, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -364,7 +310,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -386,17 +332,7 @@ router.post('/users', data, { When making visits / requests that include files, Inertia will automatically convert the request data into a `FormData` object. If you would like the request to always use a `FormData` object, you may use the `forceFormData` option. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/companies', data, { - forceFormData: true, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -416,7 +352,7 @@ router.post('/companies', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -435,15 +371,7 @@ For more information on uploading files, please consult the dedicated [file uplo When making visits, Inertia automatically adds a new entry into the browser history. However, it's also possible to replace the current history entry by setting the `replace` option to `true`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.get('/users', { search: 'John' }, { replace: true }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -459,7 +387,7 @@ import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { replace: true }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -478,21 +406,12 @@ By default, page visits to the same page create a fresh page component instance. However, in some situations, it's necessary to preserve the page component state. For example, when submitting a form, you need to preserve your form data in the event that form validation fails on the server. - For this reason, the `post`, `put`, `patch`, `delete`, and `reload` methods all set the `preserveState` option to `true` by default. You can instruct Inertia to preserve the component's state when using the `get` method by setting the `preserveState` option to `true`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.get('/users', { search: 'John' }, { preserveState: true }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -508,7 +427,7 @@ import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { preserveState: true }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -523,15 +442,7 @@ You can also lazily evaluate the `preserveState` option based on the response by If you'd like to only preserve state if the response includes validation errors, set the `preserveState` option to `"errors"`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.get('/users', { search: 'John' }, { preserveState: 'errors' }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -547,7 +458,7 @@ import { router } from '@inertiajs/react' router.get('/users', { search: 'John' }, { preserveState: 'errors' }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -560,17 +471,7 @@ router.get('/users', { search: 'John' }, { preserveState: 'errors' }) You can also lazily evaluate the `preserveState` option based on the response by providing a callback. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - preserveState: (page) => page.props.someProp === 'value', -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -590,7 +491,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -609,15 +510,7 @@ When navigating between pages, Inertia mimics default browser behavior by automa You can disable this behaviour by setting the `preserveScroll` option to `false`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { preserveScroll: false }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -633,7 +526,7 @@ import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: false }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -646,15 +539,7 @@ router.visit(url, { preserveScroll: false }) If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to `"errors"`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { preserveScroll: 'errors' }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -670,7 +555,7 @@ import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: 'errors' }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -683,17 +568,7 @@ router.visit(url, { preserveScroll: 'errors' }) You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - preserveScroll: (page) => page.props.someProp === 'value', -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -713,7 +588,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -732,15 +607,7 @@ For more information regarding this feature, please consult the [scroll manageme The `only` option allows you to request a subset of the props (data) from the server on subsequent visits to the same page, thus making your application more efficient since it does not need to retrieve data that the page is not interested in refreshing. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit('/users', { search: 'John' }, { only: ['users'] }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -756,7 +623,7 @@ import { router } from '@inertiajs/react' router.visit('/users', { search: 'John' }, { only: ['users'] }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -773,20 +640,7 @@ For more information on this feature, please consult the [partial reloads](/guid You can cancel a visit using a cancel token, which Inertia automatically generates and provides via the `onCancelToken()` callback prior to making the visit. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - onCancelToken: (cancelToken) => (this.cancelToken = cancelToken), -}) - -// Cancel the visit... -this.cancelToken.cancel() -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -812,7 +666,7 @@ router.post('/users', data, { this.cancelToken.cancel() ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -834,23 +688,7 @@ The `onCancel()` and `onFinish()` event callbacks will be executed when a visit In addition to Inertia's [global events](/guide/events.md), Inertia also provides a number of per-visit event callbacks. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - onBefore: (visit) => {}, - onStart: (visit) => {}, - onProgress: (progress) => {}, - onSuccess: (page) => {}, - onError: (errors) => {}, - onCancel: () => {}, - onFinish: (visit) => {}, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -882,7 +720,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -903,17 +741,7 @@ router.post('/users', data, { Returning `false` from the `onBefore()` callback will cause the visit to be cancelled. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.delete(`/users/${user.id}`, { - onBefore: () => confirm('Are you sure you want to delete this user?'), -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -933,7 +761,7 @@ router.delete(`/users/${user.id}`, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -948,23 +776,7 @@ router.delete(`/users/${user.id}`, { It's also possible to return a promise from the `onSuccess()` and `onError()` callbacks. When doing so, the "finish" event will be delayed until the promise has resolved. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post(url, { - onSuccess: () => { - return Promise.all([this.doThing(), this.doAnotherThing()]) - }, - onFinish: (visit) => { - // This won't be called until doThing() - // and doAnotherThing() have finished. - }, -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -996,7 +808,7 @@ router.post(url, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' diff --git a/docs/guide/merging-props.md b/docs/guide/merging-props.md new file mode 100644 index 0000000..1ab6b03 --- /dev/null +++ b/docs/guide/merging-props.md @@ -0,0 +1,76 @@ +# Merging props + +By default, Inertia overwrites props with the same name when reloading a page. However, there are instances, such as pagination or infinite scrolling, where that is not the desired behavior. In these cases, you can merge props instead of overwriting them. + +## Server side + +To specify that a prop should be merged, you can use the `merge` method on the prop value. + +```ruby +class UsersController < ApplicationController + include Pagy::Backend + + def index + _pagy, records = pagy(User.all) + + render inertia: 'Users/Index', props: { + results: InertiaRails.merge(records), + + # Also works with block: + # results: InertiaRails.merge { records }, + + # Also works with a lambda: + # results: InertiaRails.merge(-> { records }), + } + 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. + +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. + +```ruby +class UsersController < ApplicationController + include Pagy::Backend + + def index + render inertia: 'Users/Index', props: { + results: InertiaRails.defer { pagy(User.all)[1] }.merge, + } + end +end +``` + +## Resetting props + +On the client side, you can indicate to the server that you would like to reset the prop. This is useful when you want to clear the prop value before merging new data, such as when the user enters a new search query on a paginated list. + +The `reset` request option accepts an array of the props keys you would like to reset. + +:::tabs key:frameworks +== Vue + +```js +import { router } from '@inertiajs/vue3' + +router.reload({ reset: ['results'] }) +``` + +== React + +```js +import { router } from '@inertiajs/react' + +router.reload({ reset: ['results'] }) +``` + +== Svelte 4|Svelte 5 + +```js +import { router } from '@inertiajs/svelte' + +router.reload({ reset: ['results'] }) +``` + +::: diff --git a/docs/guide/pages.md b/docs/guide/pages.md index 30b9c2f..d380bcb 100644 --- a/docs/guide/pages.md +++ b/docs/guide/pages.md @@ -9,34 +9,7 @@ In addition, all of the data needed for the page can be retrieved before the pag Inertia pages are simply JavaScript components. If you have ever written a Vue, React, or Svelte component, you will feel right at home. As you can see in the example below, pages receive data from your application's controllers as props. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue + + Welcome + + + +

Welcome

+

Hello {user.name}, welcome to your first Inertia app!

+
+``` + +== Svelte 5 + +```svelte + + + + Welcome + + - - Welcome - -

Welcome

+

Welcome

Hello {user.name}, welcome to your first Inertia app!

``` @@ -104,41 +97,14 @@ class UsersController < ApplicationController end ``` -See [the responses documentation](/guide/responses) for more information on how to return Inertia responses from your controllers. +See [the responses documentation](/guide/responses) for more information on how to return Inertia responses from your controllers. ## Creating layouts While not required, for most projects it makes sense to create a site layout that all of your pages can extend. You may have noticed in our page example above that we're wrapping the page content within a `` component. Here's an example of such a component: :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue + +
+
+ Home + About + Contact +
+
+ {@render children()} +
+
+``` + ::: As you can see, there is nothing Inertia specific within this template. This is just a typical component. @@ -208,34 +195,7 @@ While it's simple to implement layouts as children of page components, it forces For example, maybe you have an audio player on a podcast website that you want to continue playing as users navigate the site. Or, maybe you simply want to maintain the scroll position in your sidebar navigation between page visits. In these situations, the solution is to leverage Inertia's persistent layouts. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue ``` @@ -268,7 +228,7 @@ import Layout from '../Layout' const Home = ({ user }) => { return ( <> -

Welcome

+

Welcome

Hello {user.name}, welcome to your first Inertia app!

) @@ -279,7 +239,7 @@ Home.layout = (page) => export default Home ``` -== Svelte +== Svelte 4 ```svelte -

Welcome

+

Welcome

+

Hello {user.name}, welcome to your first Inertia app!

``` -::: - -You can also create more complex layout arrangements using nested layouts. - -:::tabs key:frameworks -== Vue 2 +== Svelte 5 -```vue - +```svelte + - // Using shorthand syntax... - layout: [SiteLayout, NestedLayout], +

Welcome

- props: { - user: Object, - }, -} - +

Hello {user.name}, welcome to your first Inertia app!

``` -If you're using Vue 2.7 or Vue 3, you can alternatively use the [defineOptions plugin](https://vue-macros.dev/macros/define-options.html) to define a layout within ` -``` +You can also create more complex layout arrangements using nested layouts. -== Vue 3 +:::tabs key:frameworks +== Vue ```vue ``` -If you're using Vue 2.7 or Vue 3, you can alternatively use the [defineOptions plugin](https://vue-macros.dev/macros/define-options.html) to define a layout within ` @@ -414,7 +359,31 @@ export default Home export let user -

Welcome

+

Welcome

+ +

Hello {user.name}, welcome to your first Inertia app!

+``` + +== Svelte 5 + +```svelte + + + + +

Welcome

+

Hello {user.name}, welcome to your first Inertia app!

``` @@ -425,24 +394,7 @@ export default Home If you're using persistent layouts, you may find it convenient to define the default page layout in the `resolve()` callback of your application's main JavaScript file. :::tabs key:frameworks -== Vue 2 - -```js -// frontend/entrypoints/inertia.js -import Layout from '../Layout' - -createInertiaApp({ - resolve: (name) => { - const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) - let page = pages[`../pages/${name}.vue`] - page.default.layout = page.default.layout || Layout - return page - }, - // ... -}) -``` - -== Vue 3 +== Vue ```js // frontend/entrypoints/inertia.js @@ -477,7 +429,7 @@ createInertiaApp({ }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // frontend/entrypoints/inertia.js @@ -500,24 +452,7 @@ This will automatically set the page layout to `Layout` if a layout has not alre You can even go a step further and conditionally set the default page layout based on the page `name`, which is available to the `resolve()` callback. For example, maybe you don't want the default layout to be applied to your public pages. :::tabs key:frameworks -== Vue 2 - -```js -// frontend/entrypoints/inertia.js -import Layout from '../Layout' - -createInertiaApp({ - resolve: (name) => { - const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) - let page = pages[`../pages/${name}.vue`] - page.default.layout = name.startsWith('Public/') ? undefined : Layout - return page - }, - // ... -}) -``` - -== Vue 3 +== Vue ```js // frontend/entrypoints/inertia.js @@ -553,7 +488,7 @@ createInertiaApp({ }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js // frontend/entrypoints/inertia.js @@ -567,7 +502,6 @@ createInertiaApp({ default: page.default, layout: name.startsWith('Public/') ? undefined : Layout, } - return page }, // ... }) diff --git a/docs/guide/partial-reloads.md b/docs/guide/partial-reloads.md index 40b7470..871e769 100644 --- a/docs/guide/partial-reloads.md +++ b/docs/guide/partial-reloads.md @@ -12,17 +12,7 @@ As an example, consider a "user index" page that includes a list of users, as we To perform a partial reload, use the `only` visit option to specify which data the server should return. This option should be an array of keys which correspond to the keys of the props. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { - only: ['users'], -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -42,7 +32,7 @@ router.visit(url, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -56,21 +46,8 @@ router.visit(url, { ## Except certain props -> [!WARNING] -> The `except` option is not yet supported by the Inertia Rails. - :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { - except: ['users'], -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -90,7 +67,7 @@ router.visit(url, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -109,17 +86,9 @@ In addition to the only visit option you can also use the except option to speci Since partial reloads can only be made to the same page component the user is already on, it almost always makes sense to just use the `router.reload()` method, which automatically uses the current URL. :::tabs key:frameworks -== Vue 2 +== Vue -```vue -import { router } from '@inertiajs/vue2' - -router.reload({ only: ['users'] }) -``` - -== Vue 3 - -```vue +```js import { router } from '@inertiajs/vue3' router.reload({ only: ['users'] }) @@ -127,15 +96,15 @@ router.reload({ only: ['users'] }) == React -```jsx +```js import { router } from '@inertiajs/react' router.reload({ only: ['users'] }) ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js import { router } from '@inertiajs/svelte' router.reload({ only: ['users'] }) @@ -148,20 +117,16 @@ router.reload({ only: ['users'] }) It's also possible to perform partial reloads with Inertia links using the `only` property. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Show active -``` - -== Vue 3 +== Vue ```vue + -Show active + ``` == React @@ -169,17 +134,21 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Show active - +export default () => ( + + Show active + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + -Show active +Show active Show active ``` @@ -203,13 +172,42 @@ end When Inertia performs a request, it will determine which data is required and only then will it evaluate the closure. This can significantly increase the performance of pages that contain a lot of optional data. -Additionally, Inertia provides an `InertiaRails.lazy` method to specify that a prop should never be included unless explicitly requested using the `only` option: +Additionally, Inertia provides an `InertiaRails.optional` method to specify that a prop should never be included unless explicitly requested using the `only` option: + +```ruby +class UsersController < ApplicationController + def index + render inertia: 'Users/Index', props: { + users: InertiaRails.optional { User.all }, + + # Also works with a lambda: + # users: InertiaRails.optional(-> { User.all }), + + # Also works with a simple value, + # but this way the prop is always evaluated, + # even if not included: + # users: InertiaRails.optional(User.all), + } + end +end +``` + +> [!NOTE] +> Prior to Inertia.js v2, the method `InertiaRails.lazy` was used. It is now deprecated and has been replaced by `InertiaRails.optional`. Please update your code accordingly to ensure compatibility with the latest version. + +On the inverse, you can use the `InertiaRails.always` method to specify that a prop should always be included, even if it has not been explicitly required in a partial reload. ```ruby class UsersController < ApplicationController def index render inertia: 'Users/Index', props: { - users: InertiaRails.lazy(-> { User.all }), + users: InertiaRails.always(User.all), + + # Also works with block: + # users: InertiaRails.always { User.all }, + + # Also works with a lambda: + # users: InertiaRails.always(-> { User.all }), } end end @@ -234,7 +232,12 @@ class UsersController < ApplicationController # NEVER included on standard visits # OPTIONALLY included on partial reloads # ONLY evaluated when needed - users: InertiaRails.lazy(-> { User.all }), + users: InertiaRails.optional { User.all }, + + # ALWAYS included on standard visits + # ALWAYS included on partial reloads + # ALWAYS evaluated + users: InertiaRails.always(User.all), } end end diff --git a/docs/guide/polling.md b/docs/guide/polling.md new file mode 100644 index 0000000..ec035ae --- /dev/null +++ b/docs/guide/polling.md @@ -0,0 +1,196 @@ +# Polling + +## Poll helper + +Polling your server for new information on the current page is common, so Inertia provides a poll helper designed to help reduce the amount of boilerplate code. In addition, the poll helper will automatically stop polling when the page is unmounted. + +The only required argument is the polling interval in milliseconds. + +:::tabs key:frameworks +== Vue + +```js +import { usePoll } from '@inertiajs/vue3' +usePoll(2000) +``` + +== React + +```js +import { usePoll } from '@inertiajs/react' +usePoll(2000) +``` + +== Svelte 4|Svelte 5 + +```js +import { usePoll } from '@inertiajs/svelte' +usePoll(2000) +``` + +::: + +If you need to pass additional request options to the poll helper, you can pass any of the `router.reload` options as the second parameter. + +:::tabs key:frameworks +== Vue + +```js +import { usePoll } from '@inertiajs/vue3' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + }, +}) +``` + +== React + +```js +import { usePoll } from '@inertiajs/react' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + }, +}) +``` + +== Svelte 4|Svelte 5 + +```js +import { usePoll } from '@inertiajs/svelte' + +usePoll(2000, { + onStart() { + console.log('Polling request started') + }, + onFinish() { + console.log('Polling request finished') + }, +}) +``` + +::: + +If you'd like more control over the polling behavior, the poll helper provides `stop` and `start` methods that allow you to manually start and stop polling. You can pass the `autoStart: false` option to the poll helper to prevent it from automatically starting polling when the component is mounted. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { usePoll } from '@inertiajs/react' + +export default () => { + const { start, stop } = usePoll( + 2000, + {}, + { + autoStart: false, + }, + ) + return ( +
+ + +
+ ) +} +``` + +== Svelte 4|Svelte 5 + +```svelte + + + + +``` + +::: + +## Throttling + +By default, the poll helper will throttle requests by 90% when the browser tab is in the background. If you'd like to disable this behavior, you can pass the `keepAlive` option to the poll helper. + +:::tabs key:frameworks +== Vue + +```js +import { usePoll } from '@inertiajs/vue3' + +usePoll( + 2000, + {}, + { + keepAlive: true, + }, +) +``` + +== React + +```js +import { usePoll } from '@inertiajs/react' + +usePoll( + 2000, + {}, + { + keepAlive: true, + }, +) +``` + +== Svelte 4|Svelte 5 + +```js +import { usePoll } from '@inertiajs/svelte' + +usePoll( + 2000, + {}, + { + keepAlive: true, + }, +) +``` + +::: diff --git a/docs/guide/prefetching.md b/docs/guide/prefetching.md new file mode 100644 index 0000000..0a227e6 --- /dev/null +++ b/docs/guide/prefetching.md @@ -0,0 +1,342 @@ +# Prefetching + +Inertia supports prefetching data for pages that are likely to be visited next. This can be useful for improving the perceived performance of your app by allowing the data to be fetched in the background while the user is still interacting with the current page. + +## Link prefetching + +To prefetch data for a page, you can use the `prefetch` method on the Inertia link component. By default, Inertia will prefetch the data for the page when the user hovers over the link after more than 75ms. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + + Users + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + +Users +``` + +::: + +By default, data is cached for 30 seconds before being evicted. You can customize this behavior by passing a `cacheFor` prop to the `Link` component. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + <> + + Users + + + Users + + + Users + + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + +Users +Users +Users +``` + +::: + +You can also start prefetching on `mousedown` by passing the `click` value to the `prefetch` prop. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + + Users + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + +Users +``` + +::: + +If you're confident that the user will visit a page next, you can prefetch the data on mount as well. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + + Users + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + +Users +``` + +::: + +You can also combine strategies by passing an array of values to the `prefetch` prop. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + + Users + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + +Users +``` + +::: + +## Programmatic prefetching + +You can also prefetch data programmatically using `router.prefetch`. The signature is identical to `router.visit` with the exception of a third argument that allows you to specify prefetch options. + +When the `cacheFor` option is not specified, it defaults to 30 seconds. + +```js +router.prefetch('/users', { method: 'get', data: { page: 2 } }) + +router.prefetch( + '/users', + { method: 'get', data: { page: 2 } }, + { cacheFor: '1m' }, +) +``` + +To make this even easier, Inertia offers a prefetch helper. This helper provides some additional insight into the request, such as the last updated timestamp and if the request is currently prefetching. + +:::tabs key:frameworks +== Vue + +```js +import { usePrefetch } from '@inertiajs/vue3' + +const { lastUpdatedAt, isPrefetching, isPrefetched } = usePrefetch( + '/users', + { method: 'get', data: { page: 2 } }, + { cacheFor: '1m' }, +) +``` + +== React + +```js +import { usePrefetch } from '@inertiajs/react' + +const { lastUpdatedAt, isPrefetching, isPrefetched } = usePrefetch( + '/users', + { method: 'get', data: { page: 2 } }, + { cacheFor: '1m' }, +) +``` + +== Svelte 4|Svelte 5 + +```js +import { usePrefetch } from '@inertiajs/svelte' + +const { lastUpdatedAt, isPrefetching, isPrefetched } = usePrefetch( + '/users', + { method: 'get', data: { page: 2 } }, + { cacheFor: '1m' }, +) +``` + +::: + +## Flushing prefetch cache + +You can flush the prefetch cache by calling `router.flushAll`. This will remove all cached data for all pages. + +If you want to flush the cache for a specific page, you can pass the page URL and options to the `router.flush` method. + +Furthermore, if you are using the prefetch helper, it will return a `flush` method for you to use for that specific page. + +```js +// Flush all prefetch cache +router.flushAll() + +// Flush cache for a specific page +router.flush('/users', { method: 'get', data: { page: 2 } }) + +// Flush cache for a specific page +const { flush } = usePrefetch('/users', { method: 'get', data: { page: 2 } }) +flush() +``` + +## Stale while revalidate + +By default, Inertia will fetch a fresh copy of the data when the user visits the page if the cached data is older than the cache duration. You can customize this behavior by passing a tuple to the `cacheFor` prop. + +The first value in the array represents the number of seconds the cache is considered fresh, while the second value defines how long it can be served as stale data before fetching data from the server is necessary. + +:::tabs key:frameworks +== Vue + +```vue + + + +``` + +== React + +```jsx +import { Link } from '@inertiajs/react' + +export default () => ( + + Users + +) +``` + +== Svelte 4|Svelte 5 + +```svelte + + + + Users + +``` + +::: + +### How it works + +If a request is made within the fresh period (before the first value), the cache is returned immediately without making a request to the server. + +If a request is made during the stale period (between the two values), the stale value is served to the user, and a request is made in the background to refresh the cached value. Once the value is returned, the data is merged into the page so the user has the most recent data. + +If a request is made after the second value, the cache is considered expired, and the value is fetched from the sever as a regular request. diff --git a/docs/guide/progress-indicators.md b/docs/guide/progress-indicators.md index 35219af..e918ae8 100644 --- a/docs/guide/progress-indicators.md +++ b/docs/guide/progress-indicators.md @@ -1,6 +1,6 @@ # Progress indicators -Since Inertia requests are made via XHR, there would typically not be a browser loading indicator when navigating from one page to another. To solve this, Inertia displays a progress indicator at the top of the page whenever you make an Inertia visit. +Since Inertia requests are made via XHR, there would typically not be a browser loading indicator when navigating from one page to another. To solve this, Inertia displays a progress indicator at the top of the page whenever you make an Inertia visit. However, [asynchronous requests](#visit-options) do not show the progress indicator unless explicitly configured. Of course, if you prefer, you can disable Inertia's default loading indicator and provide your own custom implementation. We'll discuss both approaches below. @@ -67,14 +67,7 @@ After installation, you'll need to add the [NProgress styles](https://github.com Next, import both `NProgress` and the Inertia `router` into your application. :::tabs key:frameworks -== Vue 2 - -```js -import NProgress from 'nprogress' -import { router } from '@inertiajs/vue2' -``` - -== Vue 3 +== Vue ```js import NProgress from 'nprogress' @@ -88,7 +81,7 @@ import NProgress from 'nprogress' import { router } from '@inertiajs/react' ``` -== Svelte +== Svelte 4|Svelte 5 ```js import NProgress from 'nprogress' @@ -201,11 +194,11 @@ That's it, you now have a beautiful custom page loading indicator! For convenience, here is the full source code of the final version of our custom loading indicator. :::tabs key:frameworks -== Vue 2 +== Vue ```js import NProgress from 'nprogress' -import { router } from '@inertiajs/vue2' +import { router } from '@inertiajs/vue3' let timeout = null @@ -234,11 +227,11 @@ router.on('finish', (event) => { }) ``` -== Vue 3 +== React ```js import NProgress from 'nprogress' -import { router } from '@inertiajs/vue3' +import { router } from '@inertiajs/react' let timeout = null @@ -267,11 +260,11 @@ router.on('finish', (event) => { }) ``` -== React +== Svelte 4|Svelte 5 ```js import NProgress from 'nprogress' -import { router } from '@inertiajs/react' +import { router } from '@inertiajs/svelte' let timeout = null @@ -300,37 +293,27 @@ router.on('finish', (event) => { }) ``` -== Svelte +::: -```js -import NProgress from 'nprogress' -import { router } from '@inertiajs/svelte' +## Visit Options -let timeout = null +In addition to these configurations, Inertia.js provides two visit options to control the loading indicator on a per-request basis: `showProgress` and `async`. These options offer greater control over how Inertia.js handles asynchronous requests and manages progress indicators. -router.on('start', () => { - timeout = setTimeout(() => NProgress.start(), 250) -}) +### `showProgress` -router.on('progress', (event) => { - if (NProgress.isStarted() && event.detail.progress.percentage) { - NProgress.set((event.detail.progress.percentage / 100) * 0.9) - } -}) +The `showProgress` option provides fine-grained control over the visibility of the loading indicator during requests. -router.on('finish', (event) => { - clearTimeout(timeout) - if (!NProgress.isStarted()) { - return - } else if (event.detail.visit.completed) { - NProgress.done() - } else if (event.detail.visit.interrupted) { - NProgress.set(0) - } else if (event.detail.visit.cancelled) { - NProgress.done() - NProgress.remove() - } -}) +```js +router.get('/settings', {}, { showProgress: false }) ``` -::: +### `async` + +The `async` option allows you to perform asynchronous requests without displaying the default progress indicator. It can be used in combination with the `showProgress` option. + +```js +// Disable the progress indicator +router.get('/settings', {}, { async: true }) +// Enable the progress indicator with async requests +router.get('/settings', {}, { async: true, showProgress: true }) +``` diff --git a/docs/guide/remembering-state.md b/docs/guide/remembering-state.md index 1a444eb..1d17104 100644 --- a/docs/guide/remembering-state.md +++ b/docs/guide/remembering-state.md @@ -8,27 +8,23 @@ To mitigate this issue, you can tell Inertia which local component state to save ## Saving local state -To save local component state to the history state, use the `remember` feature to tell Inertia which data it should remember. +To save local component state to the history state, use the "useRemember" hook to tell Inertia which data it should remember. :::tabs key:frameworks -== Vue 2 +== Vue -```vue -{ // Option 1: Object... remember: { data: ['form'], }, // Option 2: Array... -remember: ['form'], // Option 3: String... remember: 'form', data() { return { -form: { first_name: null, last_name: null, // ... }, } }, } -``` - -== Vue 3 +```js +import { useRemember } from '@inertiajs/vue3' -```vue -import { useRemember } from '@inertiajs/vue3' const form = useRemember({ -first_name: null, last_name: null, }) +const form = useRemember({ + first_name: null, + last_name: null, +}) ``` == React -```jsx +```js import { useRemember } from '@inertiajs/react' export default function Profile() { @@ -42,12 +38,12 @@ export default function Profile() { } ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte -import { remember } from '@inertiajs/svelte' +```js +import { useRemember } from '@inertiajs/svelte' -let form = remember({ +const form = useRemember({ first_name: null, last_name: null, }) @@ -64,26 +60,7 @@ Now, whenever your local `form` state changes, Inertia will automatically save t If your page contains multiple components that use the remember functionality provided by Inertia, you need to provide a unique key for each component so that Inertia knows which data to restore to each component. :::tabs key:frameworks -== Vue 2 - -```js -{ - remember: { - data: ['form'], - key: 'Users/Create', - }, - data() { - return { - form: { - first_name: null, - last_name: null, - }, - } - }, -} -``` - -== Vue 3 +== Vue ```js import { useRemember } from '@inertiajs/vue3' @@ -113,12 +90,12 @@ export default function Profile() { } ``` -== Svelte +== Svelte 4|Svelte 5 ```js -import { page, remember } from '@inertiajs/svelte' +import { page, useRemember } from '@inertiajs/svelte' -let form = remember( +let form = useRemember( { first_name: null, last_name: null, @@ -132,26 +109,7 @@ let form = remember( If you have multiple instances of the same component on the page using the remember functionality, be sure to also include a unique key for each component instance, such as a model identifier. :::tabs key:frameworks -== Vue 2 - -```js -{ - remember: { - data: ['form'], - key: 'Users/Create', - }, - data() { - return { - form: { - first_name: null, - last_name: null, - }, - } - }, -} -``` - -== Vue 3 +== Vue ```js import { useRemember } from '@inertiajs/vue3' @@ -183,12 +141,12 @@ export default function Profile() { } ``` -== Svelte +== Svelte 4|Svelte 5 ```js -import { page, remember } from '@inertiajs/svelte' +import { page, useRemember } from '@inertiajs/svelte' -let form = remember( +let form = useRemember( { first_name: $page.props.user.first_name, last_name: $page.props.user.last_name, @@ -204,28 +162,7 @@ let form = remember( If you're using the Inertia [form helper](/guide/forms.md#form-helper), you can pass a unique form key as the first argument when instantiating your form. This will cause the form data and errors to automatically be remembered. :::tabs key:frameworks -== Vue 2 - -```js -{ - remember: { - data: ['form'], - key() { - return `Users/Edit:${this.user.id}` - } - }, - data() { - return { - form: { - first_name: this.user.first_name, - last_name: this.user.last_name, - }, - } - }, -} -``` - -== Vue 3 +== Vue ```js import { useForm } from '@inertiajs/vue3' @@ -243,7 +180,7 @@ const form = useForm('CreateUser', data) const form = useForm(`EditUser:${user.id}`, data) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { useForm } from '@inertiajs/svelte' @@ -256,24 +193,12 @@ const form = useForm(`EditUser:${user.id}`, data) ## Manually saving state -The `remember` property in Vue 2, and the `useRemember` hook in Vue 3, React, and Svelte all watch for data changes and automatically save those changes to the history state. Then, Inertia will restore the data on page load. +The `useRemember` hook watches for data changes and automatically saves them to the history state. When navigating back to the page, Inertia will restore this data. However, it's also possible to manage this manually using the underlying `remember()` and `restore()` methods in Inertia. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -// Save local component state to history state... -router.remember(data, 'my-key') - -// Restore local component state from history state... -let data = router.restore('my-key') -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -297,7 +222,7 @@ router.remember(data, 'my-key') let data = router.restore('my-key') ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' diff --git a/docs/guide/responses.md b/docs/guide/responses.md index 0db475c..c67d2d7 100644 --- a/docs/guide/responses.md +++ b/docs/guide/responses.md @@ -62,8 +62,8 @@ class EventsController < ApplicationController end end ``` -This renders the `app/frontend/pages/events/index.(jsx|vue|svelte)` page component and passes the `@events` instance variable as the `events` prop. +This renders the `app/frontend/pages/events/index.(jsx|vue|svelte)` page component and passes the `@events` instance variable as the `events` prop. Setting the `default_render` configuration value to `true` establishes this as the default behavior: @@ -76,7 +76,7 @@ end ```ruby class EventsController < ApplicationController use_inertia_instance_props - + def index @events = Event.all end @@ -125,7 +125,7 @@ You can then access this variable like a regular local variable. ## Rails generators -Gem `inertia_rails-contrib` provides a number of generators to help you get started with Inertia in your Rails application. You can generate controllers or use scaffolds to create a new resource with Inertia responses. +Gem `inertia_rails-contrib` provides a number of generators to help you get started with Inertia in your Rails application. You can generate controllers or use scaffolds to create a new resource with Inertia responses. ### Scaffold generator diff --git a/docs/guide/scroll-management.md b/docs/guide/scroll-management.md index 2969f22..c5139c5 100644 --- a/docs/guide/scroll-management.md +++ b/docs/guide/scroll-management.md @@ -11,15 +11,7 @@ In addition, Inertia keeps track of the scroll position of each page and automat Sometimes it's desirable to prevent the default scroll resetting when making visits. You can disable this behaviour by setting the `preserveScroll` option to `false`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { preserveScroll: false }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -35,7 +27,7 @@ import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: false }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -48,15 +40,7 @@ router.visit(url, { preserveScroll: false }) If you'd like to only preserve the scroll position if the response includes validation errors, set the `preserveScroll` option to `"errors"`. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.visit(url, { preserveScroll: 'errors' }) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -72,7 +56,7 @@ import { router } from '@inertiajs/react' router.visit(url, { preserveScroll: 'errors' }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -85,17 +69,7 @@ router.visit(url, { preserveScroll: 'errors' }) You can also lazily evaluate the `preserveScroll` option based on the response by providing a callback. :::tabs key:frameworks -== Vue 2 - -```js -import { router } from '@inertiajs/vue2' - -router.post('/users', data, { - preserveScroll: (page) => page.props.someProp === 'value', -}) -``` - -== Vue 3 +== Vue ```js import { router } from '@inertiajs/vue3' @@ -115,7 +89,7 @@ router.post('/users', data, { }) ``` -== Svelte +== Svelte 4|Svelte 5 ```js import { router } from '@inertiajs/svelte' @@ -130,20 +104,16 @@ router.post('/users', data, { When using an [Inertia link](/guide/links), you can preserve the scroll position using the `preserveScroll` prop. :::tabs key:frameworks -== Vue 2 - -```vue -import { Link } from '@inertiajs/vue2' - -Home -``` - -== Vue 3 +== Vue ```vue + -Home + ``` == React @@ -151,17 +121,21 @@ import { Link } from '@inertiajs/vue3' ```jsx import { Link } from '@inertiajs/react' - - Home - +export default () => ( + + Home + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte -import { inertia, Link } from '@inertiajs/svelte' + -Home +Home Home ``` diff --git a/docs/guide/server-side-rendering.md b/docs/guide/server-side-rendering.md index 802a07e..27c49bc 100644 --- a/docs/guide/server-side-rendering.md +++ b/docs/guide/server-side-rendering.md @@ -10,13 +10,7 @@ Server-side rendering pre-renders your JavaScript pages on the server, allowing First, install the additional dependencies required for server-side rendering. This is only necessary for the Vue adapters, so you can skip this step if you're using React or Svelte. :::tabs key:frameworks -== Vue 2 - -```shell -npm install vue-server-renderer -``` - -== Vue 3 +== Vue ```shell npm install @vue/server-renderer @@ -28,7 +22,7 @@ npm install @vue/server-renderer // No additional dependencies required ``` -== Svelte +== Svelte 4|Svelte 5 ```shell // No additional dependencies required @@ -43,33 +37,7 @@ Next, we'll create a `app/frontend/ssr/ssr.js` file within the Rails project tha This file is going to look very similar to your regular inertia initialization file, except it's not going to run in the browser, but rather in Node.js. Here's a complete example. :::tabs key:frameworks -== Vue 2 - -```js -import { createInertiaApp } from '@inertiajs/vue2' -import createServer from '@inertiajs/vue2/server' -import Vue from 'vue' -import { createRenderer } from 'vue-server-renderer' - -createServer((page) => - createInertiaApp({ - page, - render: createRenderer().renderToString, - resolve: (name) => { - const pages = import.meta.glob('../pages/**/*.vue', { eager: true }) - return pages[`../pages/${name}.vue`] - }, - setup({ App, props, plugin }) { - Vue.use(plugin) - return new Vue({ - render: (h) => h(App, props), - }) - }, - }), -) -``` - -== Vue 3 +== Vue ```js import { createInertiaApp } from '@inertiajs/vue3' @@ -114,20 +82,43 @@ createServer((page) => ) ``` -== Svelte +== Svelte 4 ```js import { createInertiaApp } from '@inertiajs/svelte' import createServer from '@inertiajs/svelte/server' -const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) +createServer((page) => + createInertiaApp({ + page, + resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) + return pages[`../pages/${name}.svelte`] + }, + setup({ App, props }) { + return App.render(props) + }, + }), +) +``` + +== Svelte 5 + +```js +import { createInertiaApp } from '@inertiajs/svelte' +import createServer from '@inertiajs/svelte/server' +import { render } from 'svelte/server' createServer((page) => createInertiaApp({ page, resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) return pages[`../pages/${name}.svelte`] }, + setup({ App, props }) { + return render(App, { props }) + }, }), ) ``` @@ -180,13 +171,7 @@ Since your website is now being server-side rendered, you can instruct your clie To enable client-side hydration, update your initialization file: :::tabs key:frameworks -== Vue 2 - -```js -// No changes required -``` - -== Vue 3 +== Vue ```js // frontend/entrypoints/inertia.js @@ -228,9 +213,29 @@ createInertiaApp({ }) ``` -== Svelte +== Svelte 4 + +```js +// frontend/entrypoints/inertia.js +import { createInertiaApp } from '@inertiajs/svelte' + +createInertiaApp({ + resolve: (name) => { + const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) + return pages[`../pages/${name}.svelte`] + }, + setup({ el, App, props }) { + new App({ target: el, props }) // [!code --] + new App({ target: el, props, hydrate: true }) // [!code ++] + }, +}) +``` + +You will also need to set the `hydratable` compiler option to `true` in your `vite.config.js` file: + ```js +// vite.config.js import { svelte } from '@sveltejs/vite-plugin-svelte' import laravel from 'laravel-vite-plugin' import { defineConfig } from 'vite' @@ -243,9 +248,9 @@ export default defineConfig({ refresh: true, }), svelte(), // [!code --] - svelte({ + svelte({ // [!code ++] // [!code ++] - compilerOptions: { + compilerOptions: { // [!code ++] // [!code ++] hydratable: true, // [!code ++] }, // [!code ++] @@ -254,25 +259,28 @@ export default defineConfig({ }) ``` -You'll also need to enable hydration in your initialization file: +== Svelte 5 + ```js // frontend/entrypoints/inertia.js import { createInertiaApp } from '@inertiajs/svelte' +import { mount } from 'svelte' // [!code --] +import { hydrate, mount } from 'svelte' // [!code ++] createInertiaApp({ resolve: (name) => { - const pages = import.meta.glob('../pages/**/*.svelte', { eager: true }) - return pages[`../pages/${name}.svelte`] + const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) + return pages[`./Pages/${name}.svelte`] }, setup({ el, App, props }) { - // [!code --] - new App({ target: el, props }) // [!code --] - }, // [!code --] - setup({ el, App }) { - // [!code ++] - new App({ target: el, hydrate: true }) // [!code ++] - }, // [!code ++] + mount(App, { target: el, props }) // [!code --] + if (el.dataset.serverRendered === 'true') { // [!code ++] + hydrate(App, { target: el, props }) // [!code ++] + } else { // [!code ++] + mount(App, { target: el, props }) // [!code ++] + } // [!code ++] + }, }) ``` diff --git a/docs/guide/server-side-setup.md b/docs/guide/server-side-setup.md index da0dc74..e6fa8ed 100644 --- a/docs/guide/server-side-setup.md +++ b/docs/guide/server-side-setup.md @@ -25,6 +25,7 @@ bin/rails generate inertia:install ``` This command will: + - Check for Vite Rails and install it if not present - Ask if you want to use TypeScript - Ask you to choose your preferred frontend framework (React, Vue, or Svelte) @@ -37,7 +38,7 @@ This command will: > To use TypeScript with Svelte, you need to install `@inertiajs/svelte` version `1.3.0-beta.1` or higher. You can use the `--inertia-version` option to specify the version. > [!NOTE] -> `inertia_rails-contrib` doesn't include [Rails scaffold generators](/guide/responses#rails-generators) for TypeScript yet. +> The `inertia_rails-contrib` gem doesn't include [Rails scaffold generators](/guide/responses#rails-generators) for TypeScript yet. Example output: diff --git a/docs/guide/shared-data.md b/docs/guide/shared-data.md index c79d11a..e3781e9 100644 --- a/docs/guide/shared-data.md +++ b/docs/guide/shared-data.md @@ -8,7 +8,7 @@ Inertia's Rails adapter comes with the `shared_data` controller method. This met ```ruby class EventsController < ApplicationController - # share syncronously + # share synchronously inertia_share app_name: env['app.name'] # share lazily, evaluated at render time @@ -36,30 +36,7 @@ end Once you have shared the data server-side, you will be able to access it within any of your pages or components. Here's an example of how to access shared data in a layout component. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -== Vue 3 +== Vue ```vue - - Your page title - - + ``` == React @@ -40,13 +33,15 @@ import { Head } from '@inertiajs/vue3' ```jsx import { Head } from '@inertiajs/react' - - Your page title - - +export default () => ( + + Your page title + + +) ``` -== Svelte +== Svelte 4|Svelte 5 ```svelte @@ -65,20 +60,16 @@ Title shorthand If you only need to add a `` to the document `<head>`, you may simply pass the title as a prop to the `<Head>` component. :::tabs key:frameworks -== Vue 2 - -```vue -import { Head } from '@inertiajs/vue2' - -<Head title="Your page title" /> -``` - -== Vue 3 +== Vue ```vue +<script setup> import { Head } from '@inertiajs/vue3' +</script> -<Head title="Your page title" /> +<template> + <Head title="Your page title" /> +</template> ``` == React @@ -86,12 +77,12 @@ import { Head } from '@inertiajs/vue3' ```jsx import { Head } from '@inertiajs/react' -<Head title="Your page title" /> +export default () => <Head title="Your page title" /> ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js // Not supported ``` @@ -111,20 +102,16 @@ createInertiaApp({ After defining the title callback, the callback will automatically be invoked when you set a title using the `<Head>` component. :::tabs key:frameworks -== Vue 2 - -```vue -import { Head } from '@inertiajs/vue2' - -<Head title="Home" /> -``` - -== Vue 3 +== Vue ```vue +<script setup> import { Head } from '@inertiajs/vue3' +</script> -<Head title="Home" /> +<template> + <Head title="Home" /> +</template> ``` == React @@ -132,12 +119,12 @@ import { Head } from '@inertiajs/vue3' ```jsx import { Head } from '@inertiajs/react' -<Head title="Home" /> +export default () => <Head title="Home" /> ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js // Not supported ``` @@ -145,31 +132,25 @@ import { Head } from '@inertiajs/react' Which, in this example, will result in the following `<title>` tag. -```js +```html <title>Home - My App ``` The `title` callback will also be invoked when you set the title using a `` tag within your `<Head>` component. :::tabs key:frameworks -== Vue 2 - -```vue -import { Head } from '@inertiajs/vue2' - -<Head> - <title>Home - -``` - -== Vue 3 +== Vue ```vue + - - Home - + ``` == React @@ -177,14 +158,16 @@ import { Head } from '@inertiajs/vue3' ```jsx import { Head } from '@inertiajs/react' - - Home - +export default () => ( + + Home + +) ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js // Not supported ``` @@ -195,78 +178,79 @@ import { Head } from '@inertiajs/react' It's possible to have multiple instances of the `` component throughout your application. For example, your layout can set some default `` elements, and then your individual pages can override those defaults. :::tabs key:frameworks -== Vue 2 +== Vue ```vue -// Layout.vue import { Head } from '@inertiajs/vue2' - - - My app - - - - -// About.vue import { Head } from '@inertiajs/vue2' - - - About - My app - - -``` - -== Vue 3 - -```vue -// Layout.vue import { Head } from '@inertiajs/vue3' + + - - My app - - - + -// About.vue import { Head } from '@inertiajs/vue3' + + - - About - My app - - + ``` == React ```jsx // Layout.jsx - import { Head } from '@inertiajs/react' - - My app - - - +export default () => ( + + My app + + + +) // About.jsx - import { Head } from '@inertiajs/react' - - About - My app - - +export default () => ( + + About - My app + + +) ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js // Not supported ``` @@ -289,36 +273,10 @@ The code example above will render the following HTML. When building a real application, it can sometimes be helpful to create a custom head component that extends Inertia's `` component. This gives you a place to set app-wide defaults, such as appending the app name to the page title. :::tabs key:frameworks -== Vue 2 - -```vue - - - - - -``` - -== Vue 3 +== Vue ```vue - - + ``` == React @@ -383,12 +334,12 @@ import AppHead from './AppHead' ```jsx import AppHead from './AppHead' - +export default () => ``` -== Svelte +== Svelte 4|Svelte 5 -```svelte +```js // Not supported ``` diff --git a/docs/guide/upgrade-guide.md b/docs/guide/upgrade-guide.md new file mode 100644 index 0000000..dfb32c6 --- /dev/null +++ b/docs/guide/upgrade-guide.md @@ -0,0 +1,68 @@ +# Upgrade guide for v2.0 + +> [!NOTE] +> Inertia.js v2.0 is still in beta and these docs are a work-in-progress. Please report bugs on +> https://github.com/inertiajs/inertia https://github.com/inertiajs/inertia-rails and https://github.com/skryukov/inertia-rails_contrib + +## What's new + +Inertia.js v2.0 is a huge step forward for Inertia! The core library has been completely rewritten to architecturally support asynchronous requests, enabling a whole set of new features, including: + +- [Polling](/guide/polling) +- [Prefetching](/guide/prefetching) +- [Deferred props](/guide/deferred-props) +- [Infinite scrolling](/guide/merging-props) +- [Lazy loading data on scroll](/guide/load-when-visible) + +Additionally, for security sensitive projects, Inertia now offers a [history encryption API](/guide/history-encryption), allowing you to clear page data from history state when logging out of an application. + +## Upgrade dependencies + +To upgrade to the Inertia.js v2.0 beta, first use npm to install the client-side adapter of your choice: + +:::tabs key:frameworks +== Vue + +```vue +npm install @inertiajs/vue3@next +``` + +== React + +```jsx +npm install @inertiajs/react@next +``` + +== Svelte 4|Svelte 5 + +```svelte +npm install @inertiajs/svelte@next +``` + +::: + +Next, upgrade the `inertia-rails` gem to use the unreleased version of the gem from [the PR branch](https://github.com/inertiajs/inertia-rails/pull/132): + +```ruby +# See the PR https://github.com/inertiajs/inertia-rails/pull/132 +gem 'inertia_rails', github: 'skryukov/inertia-rails', branch: 'v2/all' +``` + +## Breaking changes + +While a significant release, Inertia.js v2.0 doesn't introduce many breaking changes. Here's a list of all the breaking changes: + +### Dropped Vue 2 support + +The Vue 2 adapter has been removed. Vue 2 reached End of Life on December 3, 2023, so this felt like it was time. + +### Svelte adapter + +- Dropped support for Svelte 3 as it reached End of Life on June 20, 2023. +- The `remember` helper has been renamed to `useRemember` to be consistent with other helpers. +- Updated `setup` callback in `inertia.js`. You need to pass `props` when initializing the `App` component. [See the updated guide](/guide/client-side-setup#initialize-the-inertia-app) +- The `setup` callback is now required in `ssr.js`. [See the updated guide](/guide/server-side-rendering#add-server-entry-point) + +### Partial reloads are now async + +Previously partial reloads in Inertia were synchronous, just like all Inertia requests. In v2.0, partial reloads are now asynchronous. Generally this is desirable, but if you were relying on these requests being synchronous, you may need to adjust your code. diff --git a/docs/guide/validation.md b/docs/guide/validation.md index 487720a..9172322 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -39,53 +39,7 @@ In order for your server-side validation errors to be available client-side, you Since validation errors are made available client-side as page component props, you can conditionally display them based on their existence. Remember, when using Rails server adapter, the `errors` prop will automatically be available to your page. :::tabs key:frameworks -== Vue 2 - -```vue - - - -``` - -> [!NOTE] -> When using the Vue adapters, you may also access the errors via the `$page.props.errors` object. - -== Vue 3 +== Vue ```vue