Repository: inertiajs/inertia Branch: master Commit: c2e81025e817 Files: 1604 Total size: 2.9 MB Directory structure: gitextract_nfe091gr/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── ISSUE_TEMPLATE/ │ │ ├── 0-bug-report.yml │ │ ├── 1-bug-report-react.yml │ │ ├── 2-bug-report-vue.yml │ │ ├── 3-bug-report-svelte.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── SUPPORT.md │ └── workflows/ │ ├── build.yml │ ├── coding-standards.yml │ ├── es2020-compatibility.yml │ ├── playwright-chromium.yml │ ├── playwright-firefox.yml │ ├── playwright-webkit.yml │ ├── publish.yml │ ├── test-app-quality.yml │ └── update-changelog.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package.json ├── packages/ │ ├── core/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── build.js │ │ ├── package.json │ │ ├── readme.md │ │ ├── src/ │ │ │ ├── config.ts │ │ │ ├── debounce.ts │ │ │ ├── debug.ts │ │ │ ├── dialog.ts │ │ │ ├── domUtils.ts │ │ │ ├── encryption.ts │ │ │ ├── eventHandler.ts │ │ │ ├── events.ts │ │ │ ├── files.ts │ │ │ ├── formData.ts │ │ │ ├── formObject.ts │ │ │ ├── head.ts │ │ │ ├── history.ts │ │ │ ├── index.ts │ │ │ ├── infiniteScroll/ │ │ │ │ ├── data.ts │ │ │ │ ├── elements.ts │ │ │ │ ├── queryString.ts │ │ │ │ └── scrollPreservation.ts │ │ │ ├── infiniteScroll.ts │ │ │ ├── initialVisit.ts │ │ │ ├── intersectionObservers.ts │ │ │ ├── modal.ts │ │ │ ├── navigationEvents.ts │ │ │ ├── navigationType.ts │ │ │ ├── objectUtils.ts │ │ │ ├── page.ts │ │ │ ├── poll.ts │ │ │ ├── polls.ts │ │ │ ├── prefetched.ts │ │ │ ├── progress-component.ts │ │ │ ├── progress.ts │ │ │ ├── queue.ts │ │ │ ├── request.ts │ │ │ ├── requestParams.ts │ │ │ ├── requestStream.ts │ │ │ ├── resetFormFields.ts │ │ │ ├── response.ts │ │ │ ├── router.ts │ │ │ ├── scroll.ts │ │ │ ├── server.ts │ │ │ ├── sessionStorage.ts │ │ │ ├── time.ts │ │ │ ├── types.ts │ │ │ ├── url.ts │ │ │ └── useFormUtils.ts │ │ └── tsconfig.json │ ├── react/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── build.js │ │ ├── package.json │ │ ├── readme.md │ │ ├── resources/ │ │ │ └── boost/ │ │ │ ├── guidelines/ │ │ │ │ └── core.blade.php │ │ │ └── skills/ │ │ │ └── inertia-react-development/ │ │ │ └── SKILL.blade.php │ │ ├── src/ │ │ │ ├── App.ts │ │ │ ├── Deferred.ts │ │ │ ├── Form.ts │ │ │ ├── Head.ts │ │ │ ├── HeadContext.ts │ │ │ ├── InfiniteScroll.ts │ │ │ ├── Link.ts │ │ │ ├── PageContext.ts │ │ │ ├── WhenVisible.ts │ │ │ ├── createInertiaApp.ts │ │ │ ├── index.ts │ │ │ ├── react.ts │ │ │ ├── server.ts │ │ │ ├── types.ts │ │ │ ├── useForm.ts │ │ │ ├── usePage.ts │ │ │ ├── usePoll.ts │ │ │ ├── usePrefetch.ts │ │ │ └── useRemember.ts │ │ ├── test-app/ │ │ │ ├── Layouts/ │ │ │ │ ├── NestedLayout.tsx │ │ │ │ ├── Prefetch.tsx │ │ │ │ ├── SWR.tsx │ │ │ │ ├── SiteLayout.tsx │ │ │ │ ├── WithScrollRegion.tsx │ │ │ │ └── WithoutScrollRegion.tsx │ │ │ ├── Pages/ │ │ │ │ ├── Article.tsx │ │ │ │ ├── ClientSideVisit/ │ │ │ │ │ ├── Page1.tsx │ │ │ │ │ ├── Page2.tsx │ │ │ │ │ ├── Props.tsx │ │ │ │ │ └── Sequential.tsx │ │ │ │ ├── ComplexMergeSelective.tsx │ │ │ │ ├── CustomConfig.tsx │ │ │ │ ├── DeepMergeProps.tsx │ │ │ │ ├── DeferredProps/ │ │ │ │ │ ├── BackButton/ │ │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ │ └── PageB.tsx │ │ │ │ │ ├── InstantReload.tsx │ │ │ │ │ ├── ManyGroups.tsx │ │ │ │ │ ├── Page1.tsx │ │ │ │ │ ├── Page2.tsx │ │ │ │ │ ├── Page3.tsx │ │ │ │ │ ├── PartialReloads.tsx │ │ │ │ │ ├── RapidNavigation.tsx │ │ │ │ │ ├── ReloadWithoutOptionalChaining.tsx │ │ │ │ │ ├── WithErrors.tsx │ │ │ │ │ ├── WithPartialReload.tsx │ │ │ │ │ ├── WithQueryParams.tsx │ │ │ │ │ └── WithReload.tsx │ │ │ │ ├── Dump.tsx │ │ │ │ ├── ErrorModal.tsx │ │ │ │ ├── Events.tsx │ │ │ │ ├── Flash/ │ │ │ │ │ ├── ClientSideVisits.tsx │ │ │ │ │ ├── Events.tsx │ │ │ │ │ ├── InitialFlash.tsx │ │ │ │ │ ├── Partial.tsx │ │ │ │ │ ├── RouterFlash.tsx │ │ │ │ │ ├── WithDeferred.tsx │ │ │ │ │ └── WithInfiniteScroll.tsx │ │ │ │ ├── FormComponent/ │ │ │ │ │ ├── ChildComponent.tsx │ │ │ │ │ ├── Context/ │ │ │ │ │ │ ├── ChildComponent.tsx │ │ │ │ │ │ ├── DeeplyNestedComponent.tsx │ │ │ │ │ │ ├── Default.tsx │ │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ │ ├── MethodsTestComponent.tsx │ │ │ │ │ │ ├── Multiple.tsx │ │ │ │ │ │ ├── NestedComponent.tsx │ │ │ │ │ │ └── OutsideFormComponent.tsx │ │ │ │ │ ├── DataMethods.tsx │ │ │ │ │ ├── DefaultValue.tsx │ │ │ │ │ ├── DisableWhileProcessing.tsx │ │ │ │ │ ├── DottedKeys.tsx │ │ │ │ │ ├── Elements.tsx │ │ │ │ │ ├── EmptyAction.tsx │ │ │ │ │ ├── Errors.tsx │ │ │ │ │ ├── Events.tsx │ │ │ │ │ ├── FormTarget.tsx │ │ │ │ │ ├── Headers.tsx │ │ │ │ │ ├── InvalidateTags.tsx │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ ├── MixedKeySerialization.tsx │ │ │ │ │ ├── Options.tsx │ │ │ │ │ ├── Precognition/ │ │ │ │ │ │ ├── BeforeValidation.tsx │ │ │ │ │ │ ├── Callbacks.tsx │ │ │ │ │ │ ├── Cancel.tsx │ │ │ │ │ │ ├── Default.tsx │ │ │ │ │ │ ├── DynamicArrayInputs.tsx │ │ │ │ │ │ ├── ErrorSync.tsx │ │ │ │ │ │ ├── Files.tsx │ │ │ │ │ │ ├── Headers.tsx │ │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ │ ├── Transform.tsx │ │ │ │ │ │ ├── TransformKeys.tsx │ │ │ │ │ │ ├── WithAllErrors.tsx │ │ │ │ │ │ ├── WithAllErrorsConfig.tsx │ │ │ │ │ │ └── WithoutAllErrors.tsx │ │ │ │ │ ├── Progress.tsx │ │ │ │ │ ├── Ref.tsx │ │ │ │ │ ├── Reset.tsx │ │ │ │ │ ├── ResetAttributes/ │ │ │ │ │ │ ├── ResetOnError.tsx │ │ │ │ │ │ ├── ResetOnErrorFields.tsx │ │ │ │ │ │ ├── ResetOnSuccess.tsx │ │ │ │ │ │ └── ResetOnSuccessFields.tsx │ │ │ │ │ ├── SetDefaultsOnSuccess.tsx │ │ │ │ │ ├── SubmitButton.tsx │ │ │ │ │ ├── SubmitComplete/ │ │ │ │ │ │ ├── Defaults.tsx │ │ │ │ │ │ ├── Redirect.tsx │ │ │ │ │ │ └── Reset.tsx │ │ │ │ │ ├── Transform.tsx │ │ │ │ │ ├── UppercaseMethod.tsx │ │ │ │ │ ├── ViewTransition.tsx │ │ │ │ │ └── Wayfinder.tsx │ │ │ │ ├── FormHelper/ │ │ │ │ │ ├── Data.tsx │ │ │ │ │ ├── Dirty.tsx │ │ │ │ │ ├── EffectCount.tsx │ │ │ │ │ ├── EmptyForm.tsx │ │ │ │ │ ├── Errors.tsx │ │ │ │ │ ├── ErrorsClearOnResubmit.tsx │ │ │ │ │ ├── Events.tsx │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ ├── Nested.tsx │ │ │ │ │ ├── Precognition/ │ │ │ │ │ │ ├── BeforeValidation.tsx │ │ │ │ │ │ ├── Callbacks.tsx │ │ │ │ │ │ ├── Cancel.tsx │ │ │ │ │ │ ├── Compatibility.tsx │ │ │ │ │ │ ├── Default.tsx │ │ │ │ │ │ ├── DynamicArrayInputs.tsx │ │ │ │ │ │ ├── ErrorSync.tsx │ │ │ │ │ │ ├── Files.tsx │ │ │ │ │ │ ├── Headers.tsx │ │ │ │ │ │ ├── Instantiate.tsx │ │ │ │ │ │ ├── Methods.tsx │ │ │ │ │ │ ├── Transform.tsx │ │ │ │ │ │ ├── TransformKeys.tsx │ │ │ │ │ │ ├── WithAllErrors.tsx │ │ │ │ │ │ ├── WithAllErrorsConfig.tsx │ │ │ │ │ │ └── WithoutAllErrors.tsx │ │ │ │ │ ├── RememberEdit.tsx │ │ │ │ │ ├── RememberIndex.tsx │ │ │ │ │ ├── Transform.tsx │ │ │ │ │ └── TypeScript/ │ │ │ │ │ ├── Any.tsx │ │ │ │ │ ├── Child.tsx │ │ │ │ │ ├── CircularlReferences.tsx │ │ │ │ │ ├── Data.tsx │ │ │ │ │ ├── DynamicInputName.tsx │ │ │ │ │ ├── Errors.tsx │ │ │ │ │ ├── Generic.tsx │ │ │ │ │ ├── Nullable.tsx │ │ │ │ │ ├── NullableNestedObject.tsx │ │ │ │ │ ├── OptionalProps.tsx │ │ │ │ │ ├── Parent.tsx │ │ │ │ │ ├── Precognition.tsx │ │ │ │ │ └── ValidationKey.tsx │ │ │ │ ├── Head/ │ │ │ │ │ ├── Conditional.tsx │ │ │ │ │ ├── Dataset.tsx │ │ │ │ │ ├── Mixed.tsx │ │ │ │ │ ├── Reactive.tsx │ │ │ │ │ ├── WithTitle.tsx │ │ │ │ │ └── WithoutTitle.tsx │ │ │ │ ├── Head.tsx │ │ │ │ ├── History/ │ │ │ │ │ ├── Page.tsx │ │ │ │ │ └── Version.tsx │ │ │ │ ├── HistoryQuota/ │ │ │ │ │ └── Page.tsx │ │ │ │ ├── HistoryThrottle.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── InfiniteScroll/ │ │ │ │ │ ├── CustomElement.tsx │ │ │ │ │ ├── CustomTriggersRef.tsx │ │ │ │ │ ├── CustomTriggersRefObject.tsx │ │ │ │ │ ├── CustomTriggersSelector.tsx │ │ │ │ │ ├── DataTable.tsx │ │ │ │ │ ├── Deferred.tsx │ │ │ │ │ ├── DualContainers.tsx │ │ │ │ │ ├── DualSibling.tsx │ │ │ │ │ ├── Empty.tsx │ │ │ │ │ ├── Filtering.tsx │ │ │ │ │ ├── FilteringManual.tsx │ │ │ │ │ ├── FilteringReset.tsx │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ ├── HorizontalScroll.tsx │ │ │ │ │ ├── InfiniteScrollWithLink.tsx │ │ │ │ │ ├── InvisibleFirstChild.tsx │ │ │ │ │ ├── Links.tsx │ │ │ │ │ ├── Manual.tsx │ │ │ │ │ ├── ManualAfter.tsx │ │ │ │ │ ├── OverflowX.tsx │ │ │ │ │ ├── PreserveUrl.tsx │ │ │ │ │ ├── ProgrammaticRef.tsx │ │ │ │ │ ├── ReloadUnrelated.tsx │ │ │ │ │ ├── RememberState.tsx │ │ │ │ │ ├── Reverse.tsx │ │ │ │ │ ├── ReverseShortContent.tsx │ │ │ │ │ ├── ScrollContainer.tsx │ │ │ │ │ ├── ShortContent.tsx │ │ │ │ │ ├── Toggles.tsx │ │ │ │ │ ├── TriggerBoth.tsx │ │ │ │ │ ├── TriggerEndBuffer.tsx │ │ │ │ │ ├── TriggerStartBuffer.tsx │ │ │ │ │ ├── UpdateQueryString.tsx │ │ │ │ │ └── UserCard.tsx │ │ │ │ ├── Links/ │ │ │ │ │ ├── AsComponent.tsx │ │ │ │ │ ├── AsElement.tsx │ │ │ │ │ ├── AsWarning.tsx │ │ │ │ │ ├── AsWarningFalse.tsx │ │ │ │ │ ├── AutomaticCancellation.tsx │ │ │ │ │ ├── CancelSyncRequest.tsx │ │ │ │ │ ├── Data/ │ │ │ │ │ │ ├── AutoConverted.tsx │ │ │ │ │ │ ├── FormData.tsx │ │ │ │ │ │ └── Object.tsx │ │ │ │ │ ├── DataLoading.tsx │ │ │ │ │ ├── Headers.tsx │ │ │ │ │ ├── Location.tsx │ │ │ │ │ ├── Method.tsx │ │ │ │ │ ├── PartialReloads.tsx │ │ │ │ │ ├── PathTraversal.tsx │ │ │ │ │ ├── PreserveScroll.tsx │ │ │ │ │ ├── PreserveScrollFalse.tsx │ │ │ │ │ ├── PreserveState.tsx │ │ │ │ │ ├── PreserveUrl.tsx │ │ │ │ │ ├── PropUpdate.tsx │ │ │ │ │ ├── Reactivity.tsx │ │ │ │ │ ├── Replace.tsx │ │ │ │ │ ├── ScrollRegionList.tsx │ │ │ │ │ └── UrlFragments.tsx │ │ │ │ ├── MatchPropsOnKey.tsx │ │ │ │ ├── MergeNestedProps.tsx │ │ │ │ ├── MergeProps.tsx │ │ │ │ ├── NavigateNonInertia.tsx │ │ │ │ ├── NetworkError.tsx │ │ │ │ ├── OnceProps/ │ │ │ │ │ ├── ClientSideVisit.tsx │ │ │ │ │ ├── CustomKeyPageA.tsx │ │ │ │ │ ├── CustomKeyPageB.tsx │ │ │ │ │ ├── DeferredPageA.tsx │ │ │ │ │ ├── DeferredPageB.tsx │ │ │ │ │ ├── DeferredPageC.tsx │ │ │ │ │ ├── MergePageA.tsx │ │ │ │ │ ├── MergePageB.tsx │ │ │ │ │ ├── OptionalPageA.tsx │ │ │ │ │ ├── OptionalPageB.tsx │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ ├── PageB.tsx │ │ │ │ │ ├── PageC.tsx │ │ │ │ │ ├── PageD.tsx │ │ │ │ │ ├── PageE.tsx │ │ │ │ │ ├── PartialReloadA.tsx │ │ │ │ │ ├── PartialReloadB.tsx │ │ │ │ │ ├── SlowDeferredPageA.tsx │ │ │ │ │ ├── SlowDeferredPageB.tsx │ │ │ │ │ ├── TtlPageA.tsx │ │ │ │ │ ├── TtlPageB.tsx │ │ │ │ │ └── TtlPageC.tsx │ │ │ │ ├── PersistentLayouts/ │ │ │ │ │ ├── RenderFunction/ │ │ │ │ │ │ ├── Nested/ │ │ │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ │ │ └── PageB.tsx │ │ │ │ │ │ └── Simple/ │ │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ │ └── PageB.tsx │ │ │ │ │ └── Shorthand/ │ │ │ │ │ ├── Nested/ │ │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ │ └── PageB.tsx │ │ │ │ │ └── Simple/ │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ └── PageB.tsx │ │ │ │ ├── Poll/ │ │ │ │ │ ├── Hook.tsx │ │ │ │ │ ├── HookManual.tsx │ │ │ │ │ ├── RouterManual.tsx │ │ │ │ │ └── UnchangedData.tsx │ │ │ │ ├── Prefetch/ │ │ │ │ │ ├── AfterError.tsx │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── Page.tsx │ │ │ │ │ ├── PreserveState.tsx │ │ │ │ │ ├── SWR.tsx │ │ │ │ │ ├── Tags.tsx │ │ │ │ │ ├── TestPage.tsx │ │ │ │ │ └── Wayfinder.tsx │ │ │ │ ├── PreserveEqualProps.tsx │ │ │ │ ├── Progress.tsx │ │ │ │ ├── ProgressComponent.tsx │ │ │ │ ├── Reload/ │ │ │ │ │ ├── Concurrent.tsx │ │ │ │ │ └── ConcurrentWithData.tsx │ │ │ │ ├── Remember/ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ ├── ComponentA.tsx │ │ │ │ │ │ └── ComponentB.tsx │ │ │ │ │ ├── Default.tsx │ │ │ │ │ ├── FormHelper/ │ │ │ │ │ │ ├── Default.tsx │ │ │ │ │ │ ├── Password.tsx │ │ │ │ │ │ └── Remember.tsx │ │ │ │ │ ├── MultipleComponents.tsx │ │ │ │ │ ├── Object.tsx │ │ │ │ │ └── Router.tsx │ │ │ │ ├── SSR/ │ │ │ │ │ ├── Page1.tsx │ │ │ │ │ ├── Page2.tsx │ │ │ │ │ └── PageWithScriptElement.tsx │ │ │ │ ├── ScrollAfterRender.tsx │ │ │ │ ├── ScrollRegionPreserveUrl.tsx │ │ │ │ ├── ScrollSmooth.tsx │ │ │ │ ├── ScrollableParent.tsx │ │ │ │ ├── TypeScriptCreateInertiaApp.ts │ │ │ │ ├── TypeScriptFlash.tsx │ │ │ │ ├── TypeScriptProps.tsx │ │ │ │ ├── ViewTransition/ │ │ │ │ │ ├── FormErrors.tsx │ │ │ │ │ ├── PageA.tsx │ │ │ │ │ └── PageB.tsx │ │ │ │ ├── Visits/ │ │ │ │ │ ├── AfterError.tsx │ │ │ │ │ ├── AutomaticCancellation.tsx │ │ │ │ │ ├── Data/ │ │ │ │ │ │ ├── AutoConverted.tsx │ │ │ │ │ │ ├── FormData.tsx │ │ │ │ │ │ └── Object.tsx │ │ │ │ │ ├── ErrorBags.tsx │ │ │ │ │ ├── Headers.tsx │ │ │ │ │ ├── Location.tsx │ │ │ │ │ ├── Method.tsx │ │ │ │ │ ├── PartialReloads.tsx │ │ │ │ │ ├── PreserveScroll.tsx │ │ │ │ │ ├── PreserveScrollFalse.tsx │ │ │ │ │ ├── PreserveState.tsx │ │ │ │ │ ├── ReloadOnMount.tsx │ │ │ │ │ ├── Replace.tsx │ │ │ │ │ ├── UrlFragments.tsx │ │ │ │ │ └── Wayfinder.tsx │ │ │ │ ├── WhenVisible.tsx │ │ │ │ ├── WhenVisibleArrayReload.tsx │ │ │ │ ├── WhenVisibleBackButton.tsx │ │ │ │ ├── WhenVisibleFetching.tsx │ │ │ │ ├── WhenVisibleMergeParams.tsx │ │ │ │ ├── WhenVisibleParamsUpdate.tsx │ │ │ │ └── WhenVisibleReload.tsx │ │ │ ├── app.tsx │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── ssr.tsx │ │ │ ├── tsconfig.json │ │ │ ├── types.d.ts │ │ │ └── vite.config.ts │ │ └── tsconfig.json │ ├── svelte/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── package.json │ │ ├── readme.md │ │ ├── resources/ │ │ │ └── boost/ │ │ │ ├── guidelines/ │ │ │ │ └── core.blade.php │ │ │ └── skills/ │ │ │ └── inertia-svelte-development/ │ │ │ └── SKILL.blade.php │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── App.svelte │ │ │ │ ├── Deferred.svelte │ │ │ │ ├── Form.svelte │ │ │ │ ├── InfiniteScroll.svelte │ │ │ │ ├── Link.svelte │ │ │ │ ├── Render.svelte │ │ │ │ ├── WhenVisible.svelte │ │ │ │ └── formContext.ts │ │ │ ├── createInertiaApp.ts │ │ │ ├── index.ts │ │ │ ├── link.ts │ │ │ ├── page.ts │ │ │ ├── server.ts │ │ │ ├── types.ts │ │ │ ├── useForm.ts │ │ │ ├── usePoll.ts │ │ │ ├── usePrefetch.ts │ │ │ └── useRemember.ts │ │ ├── svelte.config.js │ │ ├── test-app/ │ │ │ ├── .gitignore │ │ │ ├── Layouts/ │ │ │ │ ├── NestedLayout.svelte │ │ │ │ ├── Prefetch.svelte │ │ │ │ ├── SWR.svelte │ │ │ │ ├── SiteLayout.svelte │ │ │ │ ├── WithScrollRegion.svelte │ │ │ │ └── WithoutScrollRegion.svelte │ │ │ ├── Pages/ │ │ │ │ ├── Article.svelte │ │ │ │ ├── ClientSideVisit/ │ │ │ │ │ ├── Page1.svelte │ │ │ │ │ ├── Page2.svelte │ │ │ │ │ ├── Props.svelte │ │ │ │ │ └── Sequential.svelte │ │ │ │ ├── ComplexMergeSelective.svelte │ │ │ │ ├── CustomConfig.svelte │ │ │ │ ├── DeepMergeProps.svelte │ │ │ │ ├── DeferredProps/ │ │ │ │ │ ├── BackButton/ │ │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ │ └── PageB.svelte │ │ │ │ │ ├── InstantReload.svelte │ │ │ │ │ ├── ManyGroups.svelte │ │ │ │ │ ├── Page1.svelte │ │ │ │ │ ├── Page2.svelte │ │ │ │ │ ├── Page3.svelte │ │ │ │ │ ├── PartialReloads.svelte │ │ │ │ │ ├── RapidNavigation.svelte │ │ │ │ │ ├── ReloadResults.svelte │ │ │ │ │ ├── ReloadWithoutOptionalChaining.svelte │ │ │ │ │ ├── WithErrors.svelte │ │ │ │ │ ├── WithQueryParams.svelte │ │ │ │ │ └── WithReload.svelte │ │ │ │ ├── Dump.svelte │ │ │ │ ├── ErrorModal.svelte │ │ │ │ ├── Events.svelte │ │ │ │ ├── Flash/ │ │ │ │ │ ├── ClientSideVisits.svelte │ │ │ │ │ ├── Events.svelte │ │ │ │ │ ├── InitialFlash.svelte │ │ │ │ │ ├── Partial.svelte │ │ │ │ │ ├── RouterFlash.svelte │ │ │ │ │ ├── WithDeferred.svelte │ │ │ │ │ └── WithInfiniteScroll.svelte │ │ │ │ ├── FormComponent/ │ │ │ │ │ ├── Context/ │ │ │ │ │ │ ├── ChildComponent.svelte │ │ │ │ │ │ ├── DeeplyNestedComponent.svelte │ │ │ │ │ │ ├── Default.svelte │ │ │ │ │ │ ├── Methods.svelte │ │ │ │ │ │ ├── MethodsTestComponent.svelte │ │ │ │ │ │ ├── Multiple.svelte │ │ │ │ │ │ ├── NestedComponent.svelte │ │ │ │ │ │ └── OutsideFormComponent.svelte │ │ │ │ │ ├── DataMethods.svelte │ │ │ │ │ ├── DefaultValue.svelte │ │ │ │ │ ├── DisableWhileProcessing.svelte │ │ │ │ │ ├── DottedKeys.svelte │ │ │ │ │ ├── Elements.svelte │ │ │ │ │ ├── EmptyAction.svelte │ │ │ │ │ ├── Errors.svelte │ │ │ │ │ ├── Events.svelte │ │ │ │ │ ├── FormTarget.svelte │ │ │ │ │ ├── Headers.svelte │ │ │ │ │ ├── InvalidateTags.svelte │ │ │ │ │ ├── Methods.svelte │ │ │ │ │ ├── MixedKeySerialization.svelte │ │ │ │ │ ├── Options.svelte │ │ │ │ │ ├── Precognition/ │ │ │ │ │ │ ├── BeforeValidation.svelte │ │ │ │ │ │ ├── Callbacks.svelte │ │ │ │ │ │ ├── Cancel.svelte │ │ │ │ │ │ ├── Default.svelte │ │ │ │ │ │ ├── DynamicArrayInputs.svelte │ │ │ │ │ │ ├── ErrorSync.svelte │ │ │ │ │ │ ├── Files.svelte │ │ │ │ │ │ ├── Headers.svelte │ │ │ │ │ │ ├── Methods.svelte │ │ │ │ │ │ ├── Transform.svelte │ │ │ │ │ │ ├── TransformKeys.svelte │ │ │ │ │ │ ├── WithAllErrors.svelte │ │ │ │ │ │ ├── WithAllErrorsConfig.svelte │ │ │ │ │ │ └── WithoutAllErrors.svelte │ │ │ │ │ ├── Progress.svelte │ │ │ │ │ ├── Ref.svelte │ │ │ │ │ ├── Reset.svelte │ │ │ │ │ ├── ResetAttributes/ │ │ │ │ │ │ ├── ResetOnError.svelte │ │ │ │ │ │ ├── ResetOnErrorFields.svelte │ │ │ │ │ │ ├── ResetOnSuccess.svelte │ │ │ │ │ │ └── ResetOnSuccessFields.svelte │ │ │ │ │ ├── SetDefaultsOnSuccess.svelte │ │ │ │ │ ├── SubmitButton.svelte │ │ │ │ │ ├── SubmitComplete/ │ │ │ │ │ │ ├── Defaults.svelte │ │ │ │ │ │ ├── Redirect.svelte │ │ │ │ │ │ └── Reset.svelte │ │ │ │ │ ├── Transform.svelte │ │ │ │ │ ├── UppercaseMethod.svelte │ │ │ │ │ ├── ViewTransition.svelte │ │ │ │ │ └── Wayfinder.svelte │ │ │ │ ├── FormHelper/ │ │ │ │ │ ├── Data.svelte │ │ │ │ │ ├── Dirty.svelte │ │ │ │ │ ├── EmptyForm.svelte │ │ │ │ │ ├── Errors.svelte │ │ │ │ │ ├── ErrorsClearOnResubmit.svelte │ │ │ │ │ ├── Events.svelte │ │ │ │ │ ├── Methods.svelte │ │ │ │ │ ├── Nested.svelte │ │ │ │ │ ├── Precognition/ │ │ │ │ │ │ ├── BeforeValidation.svelte │ │ │ │ │ │ ├── Callbacks.svelte │ │ │ │ │ │ ├── Cancel.svelte │ │ │ │ │ │ ├── Compatibility.svelte │ │ │ │ │ │ ├── Default.svelte │ │ │ │ │ │ ├── DynamicArrayInputs.svelte │ │ │ │ │ │ ├── ErrorSync.svelte │ │ │ │ │ │ ├── Files.svelte │ │ │ │ │ │ ├── Headers.svelte │ │ │ │ │ │ ├── Instantiate.svelte │ │ │ │ │ │ ├── Methods.svelte │ │ │ │ │ │ ├── Transform.svelte │ │ │ │ │ │ ├── TransformKeys.svelte │ │ │ │ │ │ ├── WithAllErrors.svelte │ │ │ │ │ │ ├── WithAllErrorsConfig.svelte │ │ │ │ │ │ └── WithoutAllErrors.svelte │ │ │ │ │ ├── RememberEdit.svelte │ │ │ │ │ ├── RememberIndex.svelte │ │ │ │ │ ├── ReservedKeys.svelte │ │ │ │ │ ├── Transform.svelte │ │ │ │ │ └── TypeScript/ │ │ │ │ │ ├── Any.svelte │ │ │ │ │ ├── Child.svelte │ │ │ │ │ ├── CircularReferences.svelte │ │ │ │ │ ├── Data.svelte │ │ │ │ │ ├── DynamicInputName.svelte │ │ │ │ │ ├── Errors.svelte │ │ │ │ │ ├── FormDataCallback.svelte │ │ │ │ │ ├── Generic.svelte │ │ │ │ │ ├── Nullable.svelte │ │ │ │ │ ├── NullableNestedObject.svelte │ │ │ │ │ ├── OptionalProps.svelte │ │ │ │ │ ├── Parent.svelte │ │ │ │ │ ├── Precognition.svelte │ │ │ │ │ ├── ValidationKey.svelte │ │ │ │ │ ├── WrapperChild.svelte │ │ │ │ │ └── WrapperParent.svelte │ │ │ │ ├── History/ │ │ │ │ │ ├── Page.svelte │ │ │ │ │ └── Version.svelte │ │ │ │ ├── HistoryQuota/ │ │ │ │ │ └── Page.svelte │ │ │ │ ├── HistoryThrottle.svelte │ │ │ │ ├── Home.svelte │ │ │ │ ├── InfiniteScroll/ │ │ │ │ │ ├── CustomElement.svelte │ │ │ │ │ ├── CustomTriggersRef.svelte │ │ │ │ │ ├── CustomTriggersRefObject.svelte │ │ │ │ │ ├── CustomTriggersSelector.svelte │ │ │ │ │ ├── DataTable.svelte │ │ │ │ │ ├── Deferred.svelte │ │ │ │ │ ├── DualContainers.svelte │ │ │ │ │ ├── DualSibling.svelte │ │ │ │ │ ├── Empty.svelte │ │ │ │ │ ├── Filtering.svelte │ │ │ │ │ ├── FilteringManual.svelte │ │ │ │ │ ├── FilteringReset.svelte │ │ │ │ │ ├── Grid.svelte │ │ │ │ │ ├── HorizontalScroll.svelte │ │ │ │ │ ├── InfiniteScrollWithLink.svelte │ │ │ │ │ ├── InvisibleFirstChild.svelte │ │ │ │ │ ├── Links.svelte │ │ │ │ │ ├── Manual.svelte │ │ │ │ │ ├── ManualAfter.svelte │ │ │ │ │ ├── OverflowX.svelte │ │ │ │ │ ├── PreserveUrl.svelte │ │ │ │ │ ├── ProgrammaticRef.svelte │ │ │ │ │ ├── ReloadUnrelated.svelte │ │ │ │ │ ├── RememberState.svelte │ │ │ │ │ ├── Reverse.svelte │ │ │ │ │ ├── ReverseShortContent.svelte │ │ │ │ │ ├── ScrollContainer.svelte │ │ │ │ │ ├── ShortContent.svelte │ │ │ │ │ ├── Toggles.svelte │ │ │ │ │ ├── TriggerBoth.svelte │ │ │ │ │ ├── TriggerEndBuffer.svelte │ │ │ │ │ ├── TriggerStartBuffer.svelte │ │ │ │ │ ├── UpdateQueryString.svelte │ │ │ │ │ └── UserCard.svelte │ │ │ │ ├── Links/ │ │ │ │ │ ├── AsWarning.svelte │ │ │ │ │ ├── AsWarningFalse.svelte │ │ │ │ │ ├── AutomaticCancellation.svelte │ │ │ │ │ ├── CancelSyncRequest.svelte │ │ │ │ │ ├── Data/ │ │ │ │ │ │ ├── AutoConverted.svelte │ │ │ │ │ │ ├── FormData.svelte │ │ │ │ │ │ └── Object.svelte │ │ │ │ │ ├── DataLoading.svelte │ │ │ │ │ ├── Headers.svelte │ │ │ │ │ ├── Location.svelte │ │ │ │ │ ├── Method.svelte │ │ │ │ │ ├── PartialReloads.svelte │ │ │ │ │ ├── PathTraversal.svelte │ │ │ │ │ ├── PreserveScroll.svelte │ │ │ │ │ ├── PreserveScrollFalse.svelte │ │ │ │ │ ├── PreserveState.svelte │ │ │ │ │ ├── PreserveUrl.svelte │ │ │ │ │ ├── PropUpdate.svelte │ │ │ │ │ ├── Reactivity.svelte │ │ │ │ │ ├── Replace.svelte │ │ │ │ │ ├── ScrollRegionList.svelte │ │ │ │ │ └── UrlFragments.svelte │ │ │ │ ├── MatchPropsOnKey.svelte │ │ │ │ ├── MergeNestedProps.svelte │ │ │ │ ├── MergeProps.svelte │ │ │ │ ├── NavigateNonInertia.svelte │ │ │ │ ├── NetworkError.svelte │ │ │ │ ├── OnceProps/ │ │ │ │ │ ├── ClientSideVisit.svelte │ │ │ │ │ ├── CustomKeyPageA.svelte │ │ │ │ │ ├── CustomKeyPageB.svelte │ │ │ │ │ ├── DeferredPageA.svelte │ │ │ │ │ ├── DeferredPageB.svelte │ │ │ │ │ ├── DeferredPageC.svelte │ │ │ │ │ ├── MergePageA.svelte │ │ │ │ │ ├── MergePageB.svelte │ │ │ │ │ ├── OptionalPageA.svelte │ │ │ │ │ ├── OptionalPageB.svelte │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ ├── PageB.svelte │ │ │ │ │ ├── PageC.svelte │ │ │ │ │ ├── PageD.svelte │ │ │ │ │ ├── PageE.svelte │ │ │ │ │ ├── PartialReloadA.svelte │ │ │ │ │ ├── PartialReloadB.svelte │ │ │ │ │ ├── SlowDeferredPageA.svelte │ │ │ │ │ ├── SlowDeferredPageB.svelte │ │ │ │ │ ├── TtlPageA.svelte │ │ │ │ │ ├── TtlPageB.svelte │ │ │ │ │ └── TtlPageC.svelte │ │ │ │ ├── PersistentLayouts/ │ │ │ │ │ ├── RenderFunction/ │ │ │ │ │ │ ├── Nested/ │ │ │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ │ │ └── PageB.svelte │ │ │ │ │ │ └── Simple/ │ │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ │ └── PageB.svelte │ │ │ │ │ └── Shorthand/ │ │ │ │ │ ├── Nested/ │ │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ │ └── PageB.svelte │ │ │ │ │ └── Simple/ │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ └── PageB.svelte │ │ │ │ ├── Poll/ │ │ │ │ │ ├── Hook.svelte │ │ │ │ │ ├── HookManual.svelte │ │ │ │ │ ├── RouterManual.svelte │ │ │ │ │ └── UnchangedData.svelte │ │ │ │ ├── Prefetch/ │ │ │ │ │ ├── AfterError.svelte │ │ │ │ │ ├── Form.svelte │ │ │ │ │ ├── Page.svelte │ │ │ │ │ ├── PreserveState.svelte │ │ │ │ │ ├── SWR.svelte │ │ │ │ │ ├── Tags.svelte │ │ │ │ │ ├── TestPage.svelte │ │ │ │ │ └── Wayfinder.svelte │ │ │ │ ├── PreserveEqualProps.svelte │ │ │ │ ├── ProgressComponent.svelte │ │ │ │ ├── Reload/ │ │ │ │ │ ├── Concurrent.svelte │ │ │ │ │ └── ConcurrentWithData.svelte │ │ │ │ ├── Remember/ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ ├── ComponentA.svelte │ │ │ │ │ │ └── ComponentB.svelte │ │ │ │ │ ├── Default.svelte │ │ │ │ │ ├── FormHelper/ │ │ │ │ │ │ ├── Default.svelte │ │ │ │ │ │ ├── Password.svelte │ │ │ │ │ │ └── Remember.svelte │ │ │ │ │ ├── MultipleComponents.svelte │ │ │ │ │ ├── Object.svelte │ │ │ │ │ └── Router.svelte │ │ │ │ ├── SSR/ │ │ │ │ │ ├── Page1.svelte │ │ │ │ │ ├── Page2.svelte │ │ │ │ │ └── PageWithScriptElement.svelte │ │ │ │ ├── ScrollAfterRender.svelte │ │ │ │ ├── ScrollRegionPreserveUrl.svelte │ │ │ │ ├── ScrollSmooth.svelte │ │ │ │ ├── ScrollableParent.svelte │ │ │ │ ├── Svelte/ │ │ │ │ │ └── PropsAndPageStore.svelte │ │ │ │ ├── TypeScriptCreateInertiaApp.ts │ │ │ │ ├── TypeScriptFlash.svelte │ │ │ │ ├── TypeScriptProps.svelte │ │ │ │ ├── ViewTransition/ │ │ │ │ │ ├── FormErrors.svelte │ │ │ │ │ ├── PageA.svelte │ │ │ │ │ └── PageB.svelte │ │ │ │ ├── Visits/ │ │ │ │ │ ├── AfterError.svelte │ │ │ │ │ ├── AutomaticCancellation.svelte │ │ │ │ │ ├── Data/ │ │ │ │ │ │ ├── AutoConverted.svelte │ │ │ │ │ │ ├── FormData.svelte │ │ │ │ │ │ └── Object.svelte │ │ │ │ │ ├── ErrorBags.svelte │ │ │ │ │ ├── Headers.svelte │ │ │ │ │ ├── Location.svelte │ │ │ │ │ ├── Method.svelte │ │ │ │ │ ├── PartialReloads.svelte │ │ │ │ │ ├── PreserveScroll.svelte │ │ │ │ │ ├── PreserveScrollFalse.svelte │ │ │ │ │ ├── PreserveState.svelte │ │ │ │ │ ├── ReloadOnMount.svelte │ │ │ │ │ ├── Replace.svelte │ │ │ │ │ ├── UrlFragments.svelte │ │ │ │ │ └── Wayfinder.svelte │ │ │ │ ├── WhenVisible.svelte │ │ │ │ ├── WhenVisibleArrayReload.svelte │ │ │ │ ├── WhenVisibleBackButton.svelte │ │ │ │ ├── WhenVisibleFetching.svelte │ │ │ │ ├── WhenVisibleMergeParams.svelte │ │ │ │ ├── WhenVisibleParamsUpdate.svelte │ │ │ │ └── WhenVisibleReload.svelte │ │ │ ├── app.ts │ │ │ ├── eslint.config.js │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── ssr.ts │ │ │ ├── svelte-html.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── types.d.ts │ │ │ ├── vite-env.d.ts │ │ │ └── vite.config.js │ │ ├── tsconfig.json │ │ ├── vite-with-deps.config.js │ │ └── vite.config.js │ └── vue3/ │ ├── .gitignore │ ├── LICENSE │ ├── build.js │ ├── package.json │ ├── readme.md │ ├── resources/ │ │ └── boost/ │ │ ├── guidelines/ │ │ │ └── core.blade.php │ │ └── skills/ │ │ └── inertia-vue-development/ │ │ └── SKILL.blade.php │ ├── src/ │ │ ├── app.ts │ │ ├── createInertiaApp.ts │ │ ├── deferred.ts │ │ ├── form.ts │ │ ├── head.ts │ │ ├── index.ts │ │ ├── infiniteScroll.ts │ │ ├── link.ts │ │ ├── remember.ts │ │ ├── server.ts │ │ ├── types.ts │ │ ├── useForm.ts │ │ ├── usePoll.ts │ │ ├── usePrefetch.ts │ │ ├── useRemember.ts │ │ └── whenVisible.ts │ ├── test-app/ │ │ ├── .gitignore │ │ ├── Layouts/ │ │ │ ├── NestedLayout.vue │ │ │ ├── Prefetch.vue │ │ │ ├── SWR.vue │ │ │ ├── SiteLayout.vue │ │ │ ├── WithScrollRegion.vue │ │ │ └── WithoutScrollRegion.vue │ │ ├── Pages/ │ │ │ ├── Article.vue │ │ │ ├── ClientSideVisit/ │ │ │ │ ├── Page1.vue │ │ │ │ ├── Page2.vue │ │ │ │ ├── Props.vue │ │ │ │ └── Sequential.vue │ │ │ ├── ComplexMergeSelective.vue │ │ │ ├── CustomConfig.vue │ │ │ ├── DeepMergeProps.vue │ │ │ ├── DeferredProps/ │ │ │ │ ├── BackButton/ │ │ │ │ │ ├── PageA.vue │ │ │ │ │ └── PageB.vue │ │ │ │ ├── InstantReload.vue │ │ │ │ ├── ManyGroups.vue │ │ │ │ ├── Page1.vue │ │ │ │ ├── Page2.vue │ │ │ │ ├── Page3.vue │ │ │ │ ├── PartialReloads.vue │ │ │ │ ├── RapidNavigation.vue │ │ │ │ ├── ReloadWithoutOptionalChaining.vue │ │ │ │ ├── WithErrors.vue │ │ │ │ ├── WithQueryParams.vue │ │ │ │ └── WithReload.vue │ │ │ ├── Dump.vue │ │ │ ├── ErrorModal.vue │ │ │ ├── Events.vue │ │ │ ├── Flash/ │ │ │ │ ├── ClientSideVisits.vue │ │ │ │ ├── Events.vue │ │ │ │ ├── InitialFlash.vue │ │ │ │ ├── Partial.vue │ │ │ │ ├── RouterFlash.vue │ │ │ │ ├── WithDeferred.vue │ │ │ │ └── WithInfiniteScroll.vue │ │ │ ├── FormComponent/ │ │ │ │ ├── Context/ │ │ │ │ │ ├── ChildComponent.vue │ │ │ │ │ ├── DeeplyNestedComponent.vue │ │ │ │ │ ├── Default.vue │ │ │ │ │ ├── Methods.vue │ │ │ │ │ ├── MethodsTestComponent.vue │ │ │ │ │ ├── Multiple.vue │ │ │ │ │ ├── NestedComponent.vue │ │ │ │ │ └── OutsideFormComponent.vue │ │ │ │ ├── DataMethods.vue │ │ │ │ ├── DefaultValue.vue │ │ │ │ ├── DisableWhileProcessing.vue │ │ │ │ ├── DottedKeys.vue │ │ │ │ ├── Elements.vue │ │ │ │ ├── EmptyAction.vue │ │ │ │ ├── Errors.vue │ │ │ │ ├── Events.vue │ │ │ │ ├── FormTarget.vue │ │ │ │ ├── Headers.vue │ │ │ │ ├── InvalidateTags.vue │ │ │ │ ├── Methods.vue │ │ │ │ ├── MixedKeySerialization.vue │ │ │ │ ├── Options.vue │ │ │ │ ├── Precognition/ │ │ │ │ │ ├── BeforeValidation.vue │ │ │ │ │ ├── Callbacks.vue │ │ │ │ │ ├── Cancel.vue │ │ │ │ │ ├── Default.vue │ │ │ │ │ ├── DynamicArrayInputs.vue │ │ │ │ │ ├── ErrorSync.vue │ │ │ │ │ ├── Files.vue │ │ │ │ │ ├── Headers.vue │ │ │ │ │ ├── Methods.vue │ │ │ │ │ ├── Transform.vue │ │ │ │ │ ├── TransformKeys.vue │ │ │ │ │ ├── WithAllErrors.vue │ │ │ │ │ ├── WithAllErrorsConfig.vue │ │ │ │ │ └── WithoutAllErrors.vue │ │ │ │ ├── Progress.vue │ │ │ │ ├── Ref.vue │ │ │ │ ├── Reset.vue │ │ │ │ ├── ResetAttributes/ │ │ │ │ │ ├── ResetOnError.vue │ │ │ │ │ ├── ResetOnErrorFields.vue │ │ │ │ │ ├── ResetOnSuccess.vue │ │ │ │ │ └── ResetOnSuccessFields.vue │ │ │ │ ├── SetDefaultsOnSuccess.vue │ │ │ │ ├── SubmitButton.vue │ │ │ │ ├── SubmitComplete/ │ │ │ │ │ ├── Defaults.vue │ │ │ │ │ ├── Redirect.vue │ │ │ │ │ └── Reset.vue │ │ │ │ ├── Transform.vue │ │ │ │ ├── UppercaseMethod.vue │ │ │ │ ├── ViewTransition.vue │ │ │ │ └── Wayfinder.vue │ │ │ ├── FormHelper/ │ │ │ │ ├── Data.vue │ │ │ │ ├── Dirty.vue │ │ │ │ ├── EmptyForm.vue │ │ │ │ ├── Errors.vue │ │ │ │ ├── ErrorsClearOnResubmit.vue │ │ │ │ ├── Events.vue │ │ │ │ ├── Methods.vue │ │ │ │ ├── Nested.vue │ │ │ │ ├── NestedError.vue │ │ │ │ ├── OptionsApi.vue │ │ │ │ ├── Precognition/ │ │ │ │ │ ├── BeforeValidation.vue │ │ │ │ │ ├── Callbacks.vue │ │ │ │ │ ├── Cancel.vue │ │ │ │ │ ├── Compatibility.vue │ │ │ │ │ ├── Default.vue │ │ │ │ │ ├── DynamicArrayInputs.vue │ │ │ │ │ ├── ErrorSync.vue │ │ │ │ │ ├── Files.vue │ │ │ │ │ ├── Headers.vue │ │ │ │ │ ├── Instantiate.vue │ │ │ │ │ ├── Methods.vue │ │ │ │ │ ├── Transform.vue │ │ │ │ │ ├── TransformKeys.vue │ │ │ │ │ ├── WithAllErrors.vue │ │ │ │ │ ├── WithAllErrorsConfig.vue │ │ │ │ │ └── WithoutAllErrors.vue │ │ │ │ ├── RememberEdit.vue │ │ │ │ ├── RememberIndex.vue │ │ │ │ ├── ReservedKeys.vue │ │ │ │ ├── Transform.vue │ │ │ │ └── TypeScript/ │ │ │ │ ├── Any.vue │ │ │ │ ├── Child.vue │ │ │ │ ├── CircularReferences.vue │ │ │ │ ├── Data.vue │ │ │ │ ├── DynamicInputName.vue │ │ │ │ ├── Errors.vue │ │ │ │ ├── FormDataCallback.vue │ │ │ │ ├── Generic.vue │ │ │ │ ├── Nullable.vue │ │ │ │ ├── NullableNestedObject.vue │ │ │ │ ├── OptionalProps.vue │ │ │ │ ├── Parent.vue │ │ │ │ ├── Precognition.vue │ │ │ │ ├── ValidationKey.vue │ │ │ │ ├── WrapperChild.vue │ │ │ │ └── WrapperParent.vue │ │ │ ├── Head/ │ │ │ │ ├── Conditional.vue │ │ │ │ ├── Dataset.vue │ │ │ │ ├── Mixed.vue │ │ │ │ ├── Reactive.vue │ │ │ │ ├── WithTitle.vue │ │ │ │ └── WithoutTitle.vue │ │ │ ├── Head.vue │ │ │ ├── History/ │ │ │ │ ├── Page.vue │ │ │ │ └── Version.vue │ │ │ ├── HistoryQuota/ │ │ │ │ └── Page.vue │ │ │ ├── HistoryThrottle.vue │ │ │ ├── Home.vue │ │ │ ├── InfiniteScroll/ │ │ │ │ ├── CustomElement.vue │ │ │ │ ├── CustomTriggersRef.vue │ │ │ │ ├── CustomTriggersRefObject.vue │ │ │ │ ├── CustomTriggersSelector.vue │ │ │ │ ├── DataTable.vue │ │ │ │ ├── Deferred.vue │ │ │ │ ├── DualContainers.vue │ │ │ │ ├── DualSibling.vue │ │ │ │ ├── Empty.vue │ │ │ │ ├── Filtering.vue │ │ │ │ ├── FilteringManual.vue │ │ │ │ ├── FilteringReset.vue │ │ │ │ ├── Grid.vue │ │ │ │ ├── HorizontalScroll.vue │ │ │ │ ├── InfiniteScrollWithLink.vue │ │ │ │ ├── InvisibleFirstChild.vue │ │ │ │ ├── Links.vue │ │ │ │ ├── Manual.vue │ │ │ │ ├── ManualAfter.vue │ │ │ │ ├── ManualReverse.vue │ │ │ │ ├── ManualToggle.vue │ │ │ │ ├── OverflowX.vue │ │ │ │ ├── PreserveUrl.vue │ │ │ │ ├── ProgrammaticRef.vue │ │ │ │ ├── ReloadUnrelated.vue │ │ │ │ ├── RememberState.vue │ │ │ │ ├── Reverse.vue │ │ │ │ ├── ReverseShortContent.vue │ │ │ │ ├── ScrollContainer.vue │ │ │ │ ├── ShortContent.vue │ │ │ │ ├── Toggles.vue │ │ │ │ ├── TriggerBoth.vue │ │ │ │ ├── TriggerEndBuffer.vue │ │ │ │ ├── TriggerStartBuffer.vue │ │ │ │ ├── TriggerToggle.vue │ │ │ │ ├── UpdateQueryString.vue │ │ │ │ └── UserCard.vue │ │ │ ├── Links/ │ │ │ │ ├── AsComponent.vue │ │ │ │ ├── AsElement.vue │ │ │ │ ├── AsWarning.vue │ │ │ │ ├── AsWarningFalse.vue │ │ │ │ ├── AutomaticCancellation.vue │ │ │ │ ├── CancelSyncRequest.vue │ │ │ │ ├── Data/ │ │ │ │ │ ├── AutoConverted.vue │ │ │ │ │ ├── FormData.vue │ │ │ │ │ └── Object.vue │ │ │ │ ├── DataLoading.vue │ │ │ │ ├── Headers.vue │ │ │ │ ├── Location.vue │ │ │ │ ├── Method.vue │ │ │ │ ├── PartialReloads.vue │ │ │ │ ├── PathTraversal.vue │ │ │ │ ├── PreserveScroll.vue │ │ │ │ ├── PreserveScrollFalse.vue │ │ │ │ ├── PreserveState.vue │ │ │ │ ├── PreserveUrl.vue │ │ │ │ ├── PropUpdate.vue │ │ │ │ ├── Reactivity.vue │ │ │ │ ├── Replace.vue │ │ │ │ ├── ScrollRegionList.vue │ │ │ │ └── UrlFragments.vue │ │ │ ├── MatchPropsOnKey.vue │ │ │ ├── MergeNestedProps.vue │ │ │ ├── MergeProps.vue │ │ │ ├── NavigateNonInertia.vue │ │ │ ├── NetworkError.vue │ │ │ ├── OnceProps/ │ │ │ │ ├── ClientSideVisit.vue │ │ │ │ ├── CustomKeyPageA.vue │ │ │ │ ├── CustomKeyPageB.vue │ │ │ │ ├── DeferredPageA.vue │ │ │ │ ├── DeferredPageB.vue │ │ │ │ ├── DeferredPageC.vue │ │ │ │ ├── MergePageA.vue │ │ │ │ ├── MergePageB.vue │ │ │ │ ├── OptionalPageA.vue │ │ │ │ ├── OptionalPageB.vue │ │ │ │ ├── PageA.vue │ │ │ │ ├── PageB.vue │ │ │ │ ├── PageC.vue │ │ │ │ ├── PageD.vue │ │ │ │ ├── PageE.vue │ │ │ │ ├── PartialReloadA.vue │ │ │ │ ├── PartialReloadB.vue │ │ │ │ ├── SlowDeferredPageA.vue │ │ │ │ ├── SlowDeferredPageB.vue │ │ │ │ ├── TtlPageA.vue │ │ │ │ ├── TtlPageB.vue │ │ │ │ └── TtlPageC.vue │ │ │ ├── PersistentLayouts/ │ │ │ │ ├── RenderFunction/ │ │ │ │ │ ├── Nested/ │ │ │ │ │ │ ├── PageA.vue │ │ │ │ │ │ └── PageB.vue │ │ │ │ │ └── Simple/ │ │ │ │ │ ├── PageA.vue │ │ │ │ │ └── PageB.vue │ │ │ │ └── Shorthand/ │ │ │ │ ├── Nested/ │ │ │ │ │ ├── PageA.vue │ │ │ │ │ └── PageB.vue │ │ │ │ └── Simple/ │ │ │ │ ├── PageA.vue │ │ │ │ └── PageB.vue │ │ │ ├── Poll/ │ │ │ │ ├── Hook.vue │ │ │ │ ├── HookManual.vue │ │ │ │ ├── RouterManual.vue │ │ │ │ └── UnchangedData.vue │ │ │ ├── Prefetch/ │ │ │ │ ├── AfterError.vue │ │ │ │ ├── Form.vue │ │ │ │ ├── Page.vue │ │ │ │ ├── PreserveState.vue │ │ │ │ ├── SWR.vue │ │ │ │ ├── Tags.vue │ │ │ │ ├── TestPage.vue │ │ │ │ └── Wayfinder.vue │ │ │ ├── PreserveEqualProps.vue │ │ │ ├── ProgressComponent.vue │ │ │ ├── Reload/ │ │ │ │ ├── Concurrent.vue │ │ │ │ └── ConcurrentWithData.vue │ │ │ ├── Remember/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── ComponentA.vue │ │ │ │ │ └── ComponentB.vue │ │ │ │ ├── Default.vue │ │ │ │ ├── FormHelper/ │ │ │ │ │ ├── Default.vue │ │ │ │ │ ├── Password.vue │ │ │ │ │ └── Remember.vue │ │ │ │ ├── MultipleComponents.vue │ │ │ │ ├── Object.vue │ │ │ │ └── Router.vue │ │ │ ├── SSR/ │ │ │ │ ├── Page1.vue │ │ │ │ ├── Page2.vue │ │ │ │ └── PageWithScriptElement.vue │ │ │ ├── ScrollAfterRender.vue │ │ │ ├── ScrollRegionPreserveUrl.vue │ │ │ ├── ScrollSmooth.vue │ │ │ ├── ScrollableParent.vue │ │ │ ├── TypeScriptCreateInertiaApp.ts │ │ │ ├── TypeScriptFlash.vue │ │ │ ├── TypeScriptProps.vue │ │ │ ├── ViewTransition/ │ │ │ │ ├── FormErrors.vue │ │ │ │ ├── PageA.vue │ │ │ │ └── PageB.vue │ │ │ ├── Visits/ │ │ │ │ ├── AfterError.vue │ │ │ │ ├── AutomaticCancellation.vue │ │ │ │ ├── Data/ │ │ │ │ │ ├── AutoConverted.vue │ │ │ │ │ ├── FormData.vue │ │ │ │ │ └── Object.vue │ │ │ │ ├── ErrorBags.vue │ │ │ │ ├── Headers.vue │ │ │ │ ├── Location.vue │ │ │ │ ├── Method.vue │ │ │ │ ├── PartialReloads.vue │ │ │ │ ├── PreserveScroll.vue │ │ │ │ ├── PreserveScrollFalse.vue │ │ │ │ ├── PreserveState.vue │ │ │ │ ├── Proxy.vue │ │ │ │ ├── ReloadOnMount.vue │ │ │ │ ├── Replace.vue │ │ │ │ ├── UrlFragments.vue │ │ │ │ └── Wayfinder.vue │ │ │ ├── WhenVisible.vue │ │ │ ├── WhenVisibleArrayReload.vue │ │ │ ├── WhenVisibleBackButton.vue │ │ │ ├── WhenVisibleFetching.vue │ │ │ ├── WhenVisibleMergeParams.vue │ │ │ ├── WhenVisibleParamsUpdate.vue │ │ │ └── WhenVisibleReload.vue │ │ ├── app.ts │ │ ├── eslint.config.js │ │ ├── index.html │ │ ├── package.json │ │ ├── ssr.ts │ │ ├── tsconfig.json │ │ ├── types.d.ts │ │ └── vite.config.ts │ └── tsconfig.json ├── playgrounds/ │ ├── react/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── Console/ │ │ │ │ └── Kernel.php │ │ │ ├── Exceptions/ │ │ │ │ └── Handler.php │ │ │ ├── Http/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── Controller.php │ │ │ │ ├── Kernel.php │ │ │ │ ├── Middleware/ │ │ │ │ │ ├── Authenticate.php │ │ │ │ │ ├── EncryptCookies.php │ │ │ │ │ ├── HandleInertiaRequests.php │ │ │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ │ │ ├── RedirectIfAuthenticated.php │ │ │ │ │ ├── TrimStrings.php │ │ │ │ │ ├── TrustHosts.php │ │ │ │ │ ├── TrustProxies.php │ │ │ │ │ ├── ValidateSignature.php │ │ │ │ │ └── VerifyCsrfToken.php │ │ │ │ ├── Requests/ │ │ │ │ │ └── PrecognitionFormRequest.php │ │ │ │ └── Resources/ │ │ │ │ └── UserResource.php │ │ │ ├── Models/ │ │ │ │ ├── ChatMessage.php │ │ │ │ └── User.php │ │ │ └── Providers/ │ │ │ ├── AppServiceProvider.php │ │ │ ├── AuthServiceProvider.php │ │ │ ├── BroadcastServiceProvider.php │ │ │ ├── EventServiceProvider.php │ │ │ └── RouteServiceProvider.php │ │ ├── artisan │ │ ├── bootstrap/ │ │ │ ├── app.php │ │ │ └── cache/ │ │ │ └── .gitignore │ │ ├── composer.json │ │ ├── config/ │ │ │ ├── app.php │ │ │ ├── auth.php │ │ │ ├── broadcasting.php │ │ │ ├── cache.php │ │ │ ├── cors.php │ │ │ ├── database.php │ │ │ ├── filesystems.php │ │ │ ├── hashing.php │ │ │ ├── inertia.php │ │ │ ├── logging.php │ │ │ ├── mail.php │ │ │ ├── prism.php │ │ │ ├── queue.php │ │ │ ├── sanctum.php │ │ │ ├── services.php │ │ │ ├── session.php │ │ │ └── view.php │ │ ├── database/ │ │ │ ├── .gitignore │ │ │ ├── factories/ │ │ │ │ ├── ChatMessageFactory.php │ │ │ │ └── UserFactory.php │ │ │ ├── migrations/ │ │ │ │ ├── 2014_10_12_000000_create_users_table.php │ │ │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ │ │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ │ │ │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ │ │ │ └── 2025_08_29_115526_create_chat_messages_table.php │ │ │ └── seeders/ │ │ │ ├── DatabaseSeeder.php │ │ │ └── conversation.json │ │ ├── init.sh │ │ ├── lang/ │ │ │ └── en/ │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ │ ├── package.json │ │ ├── phpunit.xml │ │ ├── public/ │ │ │ ├── .htaccess │ │ │ ├── index.php │ │ │ └── robots.txt │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── app.css │ │ │ ├── js/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── DeferredFood.tsx │ │ │ │ │ ├── DeferredOrganizations.tsx │ │ │ │ │ ├── DeferredUsers.tsx │ │ │ │ │ ├── Image.tsx │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ ├── Message.tsx │ │ │ │ │ ├── PaperAirplaneIcon.tsx │ │ │ │ │ ├── Spinner.tsx │ │ │ │ │ ├── StreamingIndicator.tsx │ │ │ │ │ ├── TestGrid.tsx │ │ │ │ │ ├── TestGridItem.tsx │ │ │ │ │ └── Textarea.tsx │ │ │ │ ├── Pages/ │ │ │ │ │ ├── Article.tsx │ │ │ │ │ ├── Async.tsx │ │ │ │ │ ├── Chat.tsx │ │ │ │ │ ├── DataTable.tsx │ │ │ │ │ ├── Defer.tsx │ │ │ │ │ ├── Flash.tsx │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── FormComponent.tsx │ │ │ │ │ ├── FormComponentPrecognition.tsx │ │ │ │ │ ├── Home.tsx │ │ │ │ │ ├── Login.tsx │ │ │ │ │ ├── Once/ │ │ │ │ │ │ ├── First.tsx │ │ │ │ │ │ ├── Fourth.tsx │ │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ │ ├── Second.tsx │ │ │ │ │ │ └── Third.tsx │ │ │ │ │ ├── PhotoGrid.tsx │ │ │ │ │ ├── PhotoHorizontal.tsx │ │ │ │ │ ├── Poll.tsx │ │ │ │ │ ├── User.tsx │ │ │ │ │ └── Users.tsx │ │ │ │ ├── app.tsx │ │ │ │ ├── ssr.tsx │ │ │ │ ├── types/ │ │ │ │ │ └── globals.d.ts │ │ │ │ └── vite.d.ts │ │ │ └── views/ │ │ │ └── app.blade.php │ │ ├── routes/ │ │ │ ├── api.php │ │ │ ├── channels.php │ │ │ ├── console.php │ │ │ └── web.php │ │ ├── storage/ │ │ │ ├── app/ │ │ │ │ └── .gitignore │ │ │ ├── framework/ │ │ │ │ ├── .gitignore │ │ │ │ ├── cache/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── sessions/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── testing/ │ │ │ │ │ └── .gitignore │ │ │ │ └── views/ │ │ │ │ └── .gitignore │ │ │ └── logs/ │ │ │ └── .gitignore │ │ ├── tests/ │ │ │ ├── CreatesApplication.php │ │ │ ├── Feature/ │ │ │ │ └── ExampleTest.php │ │ │ ├── TestCase.php │ │ │ └── Unit/ │ │ │ └── ExampleTest.php │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── svelte4/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── Console/ │ │ │ │ └── Kernel.php │ │ │ ├── Exceptions/ │ │ │ │ └── Handler.php │ │ │ ├── Http/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── Controller.php │ │ │ │ ├── Kernel.php │ │ │ │ ├── Middleware/ │ │ │ │ │ ├── Authenticate.php │ │ │ │ │ ├── EncryptCookies.php │ │ │ │ │ ├── HandleInertiaRequests.php │ │ │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ │ │ ├── RedirectIfAuthenticated.php │ │ │ │ │ ├── TrimStrings.php │ │ │ │ │ ├── TrustHosts.php │ │ │ │ │ ├── TrustProxies.php │ │ │ │ │ ├── ValidateSignature.php │ │ │ │ │ └── VerifyCsrfToken.php │ │ │ │ └── Requests/ │ │ │ │ └── PrecognitionFormRequest.php │ │ │ ├── Models/ │ │ │ │ └── User.php │ │ │ └── Providers/ │ │ │ ├── AppServiceProvider.php │ │ │ ├── AuthServiceProvider.php │ │ │ ├── BroadcastServiceProvider.php │ │ │ ├── EventServiceProvider.php │ │ │ └── RouteServiceProvider.php │ │ ├── artisan │ │ ├── bootstrap/ │ │ │ ├── app.php │ │ │ └── cache/ │ │ │ └── .gitignore │ │ ├── composer.json │ │ ├── config/ │ │ │ ├── app.php │ │ │ ├── auth.php │ │ │ ├── broadcasting.php │ │ │ ├── cache.php │ │ │ ├── cors.php │ │ │ ├── database.php │ │ │ ├── filesystems.php │ │ │ ├── hashing.php │ │ │ ├── inertia.php │ │ │ ├── logging.php │ │ │ ├── mail.php │ │ │ ├── queue.php │ │ │ ├── sanctum.php │ │ │ ├── services.php │ │ │ ├── session.php │ │ │ └── view.php │ │ ├── database/ │ │ │ ├── .gitignore │ │ │ ├── factories/ │ │ │ │ └── UserFactory.php │ │ │ ├── migrations/ │ │ │ │ ├── 2014_10_12_000000_create_users_table.php │ │ │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ │ │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ │ │ │ └── 2019_12_14_000001_create_personal_access_tokens_table.php │ │ │ └── seeders/ │ │ │ └── DatabaseSeeder.php │ │ ├── init.sh │ │ ├── lang/ │ │ │ └── en/ │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ │ ├── package.json │ │ ├── phpunit.xml │ │ ├── public/ │ │ │ ├── .htaccess │ │ │ ├── index.php │ │ │ └── robots.txt │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── app.css │ │ │ ├── js/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── Image.svelte │ │ │ │ │ ├── Layout.svelte │ │ │ │ │ ├── Spinner.svelte │ │ │ │ │ ├── TestGrid.svelte │ │ │ │ │ └── TestGridItem.svelte │ │ │ │ ├── Pages/ │ │ │ │ │ ├── Article.svelte │ │ │ │ │ ├── Async.svelte │ │ │ │ │ ├── DataTable.svelte │ │ │ │ │ ├── Defer.svelte │ │ │ │ │ ├── Flash.svelte │ │ │ │ │ ├── Form.svelte │ │ │ │ │ ├── FormComponent.svelte │ │ │ │ │ ├── FormComponentPrecognition.svelte │ │ │ │ │ ├── Home.svelte │ │ │ │ │ ├── InfiniteScroll.svelte │ │ │ │ │ ├── Login.svelte │ │ │ │ │ ├── Once/ │ │ │ │ │ │ ├── First.svelte │ │ │ │ │ │ ├── Fourth.svelte │ │ │ │ │ │ ├── Layout.svelte │ │ │ │ │ │ ├── Second.svelte │ │ │ │ │ │ └── Third.svelte │ │ │ │ │ ├── PhotoGrid.svelte │ │ │ │ │ ├── PhotoHorizontal.svelte │ │ │ │ │ ├── Poll.svelte │ │ │ │ │ ├── User.svelte │ │ │ │ │ └── Users.svelte │ │ │ │ ├── app.ts │ │ │ │ ├── ssr.ts │ │ │ │ ├── types/ │ │ │ │ │ └── globals.d.ts │ │ │ │ └── vite-env.d.ts │ │ │ └── views/ │ │ │ └── app.blade.php │ │ ├── routes/ │ │ │ ├── api.php │ │ │ ├── channels.php │ │ │ ├── console.php │ │ │ └── web.php │ │ ├── storage/ │ │ │ ├── app/ │ │ │ │ └── .gitignore │ │ │ ├── framework/ │ │ │ │ ├── .gitignore │ │ │ │ ├── cache/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── sessions/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── testing/ │ │ │ │ │ └── .gitignore │ │ │ │ └── views/ │ │ │ │ └── .gitignore │ │ │ └── logs/ │ │ │ └── .gitignore │ │ ├── svelte.config.js │ │ ├── tests/ │ │ │ ├── CreatesApplication.php │ │ │ ├── Feature/ │ │ │ │ └── ExampleTest.php │ │ │ ├── TestCase.php │ │ │ └── Unit/ │ │ │ └── ExampleTest.php │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── svelte5/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app/ │ │ │ ├── Console/ │ │ │ │ └── Kernel.php │ │ │ ├── Exceptions/ │ │ │ │ └── Handler.php │ │ │ ├── Http/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── Controller.php │ │ │ │ ├── Kernel.php │ │ │ │ ├── Middleware/ │ │ │ │ │ ├── Authenticate.php │ │ │ │ │ ├── EncryptCookies.php │ │ │ │ │ ├── HandleInertiaRequests.php │ │ │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ │ │ ├── RedirectIfAuthenticated.php │ │ │ │ │ ├── TrimStrings.php │ │ │ │ │ ├── TrustHosts.php │ │ │ │ │ ├── TrustProxies.php │ │ │ │ │ ├── ValidateSignature.php │ │ │ │ │ └── VerifyCsrfToken.php │ │ │ │ └── Requests/ │ │ │ │ └── PrecognitionFormRequest.php │ │ │ ├── Models/ │ │ │ │ └── User.php │ │ │ └── Providers/ │ │ │ ├── AppServiceProvider.php │ │ │ ├── AuthServiceProvider.php │ │ │ ├── BroadcastServiceProvider.php │ │ │ ├── EventServiceProvider.php │ │ │ └── RouteServiceProvider.php │ │ ├── artisan │ │ ├── bootstrap/ │ │ │ ├── app.php │ │ │ └── cache/ │ │ │ └── .gitignore │ │ ├── composer.json │ │ ├── config/ │ │ │ ├── app.php │ │ │ ├── auth.php │ │ │ ├── broadcasting.php │ │ │ ├── cache.php │ │ │ ├── cors.php │ │ │ ├── database.php │ │ │ ├── filesystems.php │ │ │ ├── hashing.php │ │ │ ├── inertia.php │ │ │ ├── logging.php │ │ │ ├── mail.php │ │ │ ├── queue.php │ │ │ ├── sanctum.php │ │ │ ├── services.php │ │ │ ├── session.php │ │ │ └── view.php │ │ ├── database/ │ │ │ ├── .gitignore │ │ │ ├── factories/ │ │ │ │ └── UserFactory.php │ │ │ ├── migrations/ │ │ │ │ ├── 2014_10_12_000000_create_users_table.php │ │ │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ │ │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ │ │ │ └── 2019_12_14_000001_create_personal_access_tokens_table.php │ │ │ └── seeders/ │ │ │ └── DatabaseSeeder.php │ │ ├── init.sh │ │ ├── lang/ │ │ │ └── en/ │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ │ ├── package.json │ │ ├── phpunit.xml │ │ ├── public/ │ │ │ ├── .htaccess │ │ │ ├── index.php │ │ │ └── robots.txt │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── app.css │ │ │ ├── js/ │ │ │ │ ├── Components/ │ │ │ │ │ ├── Image.svelte │ │ │ │ │ ├── Layout.svelte │ │ │ │ │ ├── Spinner.svelte │ │ │ │ │ ├── TestGrid.svelte │ │ │ │ │ └── TestGridItem.svelte │ │ │ │ ├── Pages/ │ │ │ │ │ ├── Article.svelte │ │ │ │ │ ├── Async.svelte │ │ │ │ │ ├── DataTable.svelte │ │ │ │ │ ├── Flash.svelte │ │ │ │ │ ├── Form.svelte │ │ │ │ │ ├── FormComponent.svelte │ │ │ │ │ ├── FormComponentPrecognition.svelte │ │ │ │ │ ├── Home.svelte │ │ │ │ │ ├── Login.svelte │ │ │ │ │ ├── Once/ │ │ │ │ │ │ ├── First.svelte │ │ │ │ │ │ ├── Fourth.svelte │ │ │ │ │ │ ├── Layout.svelte │ │ │ │ │ │ ├── Second.svelte │ │ │ │ │ │ └── Third.svelte │ │ │ │ │ ├── PhotoGrid.svelte │ │ │ │ │ ├── PhotoHorizontal.svelte │ │ │ │ │ ├── Poll.svelte │ │ │ │ │ ├── User.svelte │ │ │ │ │ └── Users.svelte │ │ │ │ ├── app.ts │ │ │ │ ├── ssr.ts │ │ │ │ ├── types/ │ │ │ │ │ └── globals.d.ts │ │ │ │ └── vite-env.d.ts │ │ │ └── views/ │ │ │ └── app.blade.php │ │ ├── routes/ │ │ │ ├── api.php │ │ │ ├── channels.php │ │ │ ├── console.php │ │ │ └── web.php │ │ ├── storage/ │ │ │ ├── app/ │ │ │ │ └── .gitignore │ │ │ ├── framework/ │ │ │ │ ├── .gitignore │ │ │ │ ├── cache/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── sessions/ │ │ │ │ │ └── .gitignore │ │ │ │ ├── testing/ │ │ │ │ │ └── .gitignore │ │ │ │ └── views/ │ │ │ │ └── .gitignore │ │ │ └── logs/ │ │ │ └── .gitignore │ │ ├── tests/ │ │ │ ├── CreatesApplication.php │ │ │ ├── Feature/ │ │ │ │ └── ExampleTest.php │ │ │ ├── TestCase.php │ │ │ └── Unit/ │ │ │ └── ExampleTest.php │ │ ├── tsconfig.json │ │ └── vite.config.js │ └── vue3/ │ ├── .gitattributes │ ├── .gitignore │ ├── README.md │ ├── app/ │ │ ├── Console/ │ │ │ └── Kernel.php │ │ ├── Exceptions/ │ │ │ └── Handler.php │ │ ├── Http/ │ │ │ ├── Controllers/ │ │ │ │ └── Controller.php │ │ │ ├── Kernel.php │ │ │ ├── Middleware/ │ │ │ │ ├── Authenticate.php │ │ │ │ ├── EncryptCookies.php │ │ │ │ ├── HandleInertiaRequests.php │ │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ │ ├── RedirectIfAuthenticated.php │ │ │ │ ├── TrimStrings.php │ │ │ │ ├── TrustHosts.php │ │ │ │ ├── TrustProxies.php │ │ │ │ ├── ValidateSignature.php │ │ │ │ └── VerifyCsrfToken.php │ │ │ ├── Requests/ │ │ │ │ └── PrecognitionFormRequest.php │ │ │ └── Resources/ │ │ │ └── UserResource.php │ │ ├── Models/ │ │ │ ├── ChatMessage.php │ │ │ └── User.php │ │ └── Providers/ │ │ ├── AppServiceProvider.php │ │ ├── AuthServiceProvider.php │ │ ├── BroadcastServiceProvider.php │ │ ├── EventServiceProvider.php │ │ └── RouteServiceProvider.php │ ├── artisan │ ├── bootstrap/ │ │ ├── app.php │ │ └── cache/ │ │ └── .gitignore │ ├── composer.json │ ├── config/ │ │ ├── app.php │ │ ├── auth.php │ │ ├── broadcasting.php │ │ ├── cache.php │ │ ├── cors.php │ │ ├── database.php │ │ ├── filesystems.php │ │ ├── hashing.php │ │ ├── inertia.php │ │ ├── logging.php │ │ ├── mail.php │ │ ├── prism.php │ │ ├── queue.php │ │ ├── sanctum.php │ │ ├── services.php │ │ ├── session.php │ │ └── view.php │ ├── database/ │ │ ├── .gitignore │ │ ├── factories/ │ │ │ ├── ChatMessageFactory.php │ │ │ └── UserFactory.php │ │ ├── migrations/ │ │ │ ├── 2014_10_12_000000_create_users_table.php │ │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ │ │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ │ │ └── 2025_08_29_115526_create_chat_messages_table.php │ │ └── seeders/ │ │ ├── DatabaseSeeder.php │ │ └── conversation.json │ ├── init.sh │ ├── lang/ │ │ └── en/ │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ └── validation.php │ ├── package.json │ ├── phpunit.xml │ ├── public/ │ │ ├── .htaccess │ │ ├── index.php │ │ └── robots.txt │ ├── resources/ │ │ ├── css/ │ │ │ └── app.css │ │ ├── js/ │ │ │ ├── Components/ │ │ │ │ ├── Image.vue │ │ │ │ ├── Layout.vue │ │ │ │ ├── Message.vue │ │ │ │ ├── PaperAirplaneIcon.vue │ │ │ │ ├── PhotoIcon.vue │ │ │ │ ├── Spinner.vue │ │ │ │ ├── StreamingIndicator.vue │ │ │ │ ├── TestGrid.vue │ │ │ │ ├── TestGridItem.vue │ │ │ │ └── Textarea.vue │ │ │ ├── Pages/ │ │ │ │ ├── Article.vue │ │ │ │ ├── Async.vue │ │ │ │ ├── Chat.vue │ │ │ │ ├── DataTable.vue │ │ │ │ ├── Defer.vue │ │ │ │ ├── Flash.vue │ │ │ │ ├── Form.vue │ │ │ │ ├── FormComponent.vue │ │ │ │ ├── FormComponentPrecognition.vue │ │ │ │ ├── Home.vue │ │ │ │ ├── InfiniteScroll.vue │ │ │ │ ├── Login.vue │ │ │ │ ├── Once/ │ │ │ │ │ ├── First.vue │ │ │ │ │ ├── Fourth.vue │ │ │ │ │ ├── Layout.vue │ │ │ │ │ ├── Second.vue │ │ │ │ │ └── Third.vue │ │ │ │ ├── PhotoGrid.vue │ │ │ │ ├── PhotoHorizontal.vue │ │ │ │ ├── Poll.vue │ │ │ │ ├── User.vue │ │ │ │ └── Users.vue │ │ │ ├── app.ts │ │ │ ├── ssr.ts │ │ │ ├── types/ │ │ │ │ └── globals.d.ts │ │ │ └── vite-env.d.ts │ │ └── views/ │ │ └── app.blade.php │ ├── routes/ │ │ ├── api.php │ │ ├── channels.php │ │ ├── console.php │ │ └── web.php │ ├── storage/ │ │ ├── app/ │ │ │ └── .gitignore │ │ ├── framework/ │ │ │ ├── .gitignore │ │ │ ├── cache/ │ │ │ │ └── .gitignore │ │ │ ├── sessions/ │ │ │ │ └── .gitignore │ │ │ ├── testing/ │ │ │ │ └── .gitignore │ │ │ └── views/ │ │ │ └── .gitignore │ │ └── logs/ │ │ └── .gitignore │ ├── tests/ │ │ ├── CreatesApplication.php │ │ ├── Feature/ │ │ │ └── ExampleTest.php │ │ ├── TestCase.php │ │ └── Unit/ │ │ └── ExampleTest.php │ ├── tsconfig.json │ └── vite.config.ts ├── playwright.config.ts ├── playwright.js ├── pnpm-workspace.yaml ├── prettier.config.js ├── release.sh └── tests/ ├── app/ │ ├── eloquent.js │ ├── helpers.js │ ├── package.json │ ├── server-status.js │ ├── server.js │ └── ssr.js ├── client-side-visits-props.spec.ts ├── client-side-visits-sequential.spec.ts ├── client-side-visits.spec.ts ├── config.spec.ts ├── core/ │ ├── config.test.ts │ ├── formObject.test.ts │ ├── objectUtils.test.ts │ └── url.test.ts ├── deep-merge-props.spec.ts ├── deferred-props-cancellation.spec.ts ├── deferred-props.spec.ts ├── domUtils.spec.ts ├── error-modal.spec.ts ├── events.spec.ts ├── flash.spec.ts ├── form-component-context.spec.ts ├── form-component.spec.ts ├── form-helper.spec.ts ├── head.spec.ts ├── history-quota.spec.ts ├── history-throttle.spec.ts ├── history.spec.ts ├── inertia.spec.ts ├── infinite-scroll.spec.ts ├── initial-visit.spec.ts ├── links.spec.ts ├── manual-visits.spec.ts ├── match-props-on-key.spec.ts ├── merge-props.spec.ts ├── network-error.spec.ts ├── once-props.spec.ts ├── pages.spec.ts ├── plugin.spec.ts ├── poll.spec.ts ├── precognition.spec.ts ├── prefetch.spec.ts ├── progress-component.spec.ts ├── remember.spec.ts ├── scroll-smooth.spec.ts ├── ssr.spec.ts ├── support.ts ├── svelte.spec.ts ├── view-transitions.spec.ts └── when-visible.spec.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{php,xml,htaccess}] indent_size = 4 [*.blade.php] indent_size = 2 ================================================ FILE: .gitattributes ================================================ # exclude playgrounds/ since otherwise the project gets classified as mainly php based. # https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#summary playgrounds/** linguist-vendored * text=auto ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct The Laravel Code of Conduct can be found in the [Laravel documentation](https://laravel.com/docs/contributions#code-of-conduct). ================================================ FILE: .github/ISSUE_TEMPLATE/0-bug-report.yml ================================================ name: Bug Report description: 'Submit an issue.' body: - type: markdown attributes: value: | Please read [our full contribution guide](https://laravel.com/docs/contributions#bug-reports) before submitting bug reports. Before submitting, please confirm: - You have **upgraded to the latest version** of both the JS package (`@inertiajs/{adapter}`) and the server-side adapter (`inertiajs/inertia-laravel`) and confirmed the issue still exists - Only Inertia **2.x** and **3.x (beta)** are supported. Issues for 0.x or 1.x will be closed. - type: dropdown attributes: label: Inertia version description: Which major version of Inertia are you using? options: - 2.x (stable) - 3.x (beta) validations: required: true - type: checkboxes attributes: label: Inertia adapter(s) affected description: Select all frontend adapters that are impacted by this issue. options: - label: React - label: Vue 3 - label: Svelte - label: Not Applicable - type: input attributes: label: 'JS package version' description: Provide the exact version of `@inertiajs/{adapter}` you are using (e.g. 2.0.3 or 3.0.0-beta.2). placeholder: 2.0.3 validations: required: true - type: textarea attributes: label: Backend stack (optional) description: If this bug depends on backend integration, provide details such as Laravel version, PHP version, or other relevant environment info. placeholder: | Laravel 12.x PHP 8.4 validations: required: false - type: textarea attributes: label: Describe the problem description: Explain the behavior you're seeing that you think is a bug, and describe how you expect it to behave instead. validations: required: true - type: textarea attributes: label: Steps to reproduce description: Provide clear steps to reproduce the issue. Include a minimal code example that clearly shows the problem. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/1-bug-report-react.yml ================================================ name: Bug Report - React description: 'Submit a React related issue.' labels: [react] body: - type: markdown attributes: value: | Please read [our full contribution guide](https://laravel.com/docs/contributions#bug-reports) before submitting bug reports. Before submitting, please confirm: - You have **upgraded to the latest version** of both `@inertiajs/react` and `inertiajs/inertia-laravel` and confirmed the issue still exists - Only Inertia **2.x** and **3.x (beta)** are supported. Issues for 0.x or 1.x will be closed. - type: dropdown attributes: label: Inertia version description: Which major version of Inertia are you using? options: - 2.x (stable) - 3.x (beta) validations: required: true - type: input attributes: label: '@inertiajs/react Version' description: Provide the exact version of `@inertiajs/react` you are using (e.g. 2.0.3 or 3.0.0-beta.2). placeholder: 2.0.3 validations: required: true - type: textarea attributes: label: Backend stack (optional) description: If this bug depends on backend integration, provide details such as Laravel version, PHP version, or other relevant environment info. placeholder: | Laravel 12.x PHP 8.4 validations: required: false - type: textarea attributes: label: Describe the problem description: Explain the behavior you're seeing that you think is a bug, and describe how you expect it to behave instead. validations: required: true - type: textarea attributes: label: Steps to reproduce description: Provide clear steps to reproduce the issue. Include a minimal code example that clearly shows the problem. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/2-bug-report-vue.yml ================================================ name: Bug Report - Vue 3 description: 'Submit a Vue 3 related issue.' labels: ['vue 3'] body: - type: markdown attributes: value: | Please read [our full contribution guide](https://laravel.com/docs/contributions#bug-reports) before submitting bug reports. Before submitting, please confirm: - You have **upgraded to the latest version** of both `@inertiajs/vue3` and `inertiajs/inertia-laravel` and confirmed the issue still exists - Only Inertia **2.x** and **3.x (beta)** are supported. Issues for 0.x or 1.x will be closed. - type: dropdown attributes: label: Inertia version description: Which major version of Inertia are you using? options: - 2.x (stable) - 3.x (beta) validations: required: true - type: input attributes: label: '@inertiajs/vue3 Version' description: Provide the exact version of `@inertiajs/vue3` you are using (e.g. 2.0.3 or 3.0.0-beta.2). placeholder: 2.0.3 validations: required: true - type: textarea attributes: label: Backend stack (optional) description: If this bug depends on backend integration, provide details such as Laravel version, PHP version, or other relevant environment info. placeholder: | Laravel 12.x PHP 8.4 validations: required: false - type: textarea attributes: label: Describe the problem description: Explain the behavior you're seeing that you think is a bug, and describe how you expect it to behave instead. validations: required: true - type: textarea attributes: label: Steps to reproduce description: Provide clear steps to reproduce the issue. Include a minimal code example that clearly shows the problem. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/3-bug-report-svelte.yml ================================================ name: Bug Report - Svelte description: 'Submit a Svelte related issue.' labels: [svelte] assignees: - pedroborges body: - type: markdown attributes: value: | Please read [our full contribution guide](https://laravel.com/docs/contributions#bug-reports) before submitting bug reports. Before submitting, please confirm: - You have **upgraded to the latest version** of both `@inertiajs/svelte` and `inertiajs/inertia-laravel` and confirmed the issue still exists - Only Inertia **2.x** and **3.x (beta)** are supported. Issues for 0.x or 1.x will be closed. - type: dropdown attributes: label: Inertia version description: Which major version of Inertia are you using? options: - 2.x (stable) - 3.x (beta) validations: required: true - type: input attributes: label: '@inertiajs/svelte Version' description: Provide the exact version of `@inertiajs/svelte` you are using (e.g. 2.0.3 or 3.0.0-beta.2). placeholder: 2.0.3 validations: required: true - type: textarea attributes: label: Backend stack (optional) description: If this bug depends on backend integration, provide details such as Laravel version, PHP version, or other relevant environment info. placeholder: | Laravel 12.x PHP 8.4 validations: required: false - type: textarea attributes: label: Describe the problem description: Explain the behavior you're seeing that you think is a bug, and describe how you expect it to behave instead. validations: required: true - type: textarea attributes: label: Steps to reproduce description: Provide clear steps to reproduce the issue. Include a minimal code example that clearly shows the problem. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Feature Request url: https://github.com/inertiajs/inertia/discussions/new?category=ideas about: 'For ideas or feature requests, start a new discussion' - name: Support Questions & Other url: https://github.com/inertiajs/inertia/discussions/new?category=help about: 'This repository is only for reporting bugs. If you have a question or need help using the library, click:' - name: Documentation issue url: https://github.com/inertiajs/docs about: For documentation issues, open a pull request at the inertiajs/docs repository ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).** ## Supported Versions Only the latest major version receives security fixes. ## Reporting a Vulnerability If you discover a security vulnerability within Laravel, please send an email to Taylor Otwell at taylor@laravel.com. All security vulnerabilities will be promptly addressed. ### Public PGP Key ``` -----BEGIN PGP PUBLIC KEY BLOCK----- Version: OpenPGP v2.0.8 Comment: Report Security Vulnerabilities to taylor@laravel.com xsFNBFugFSQBEACxEKhIY9IoJzcouVTIYKJfWFGvwFgbRjQWBiH3QdHId5vCrbWo s2l+4Rv03gMG+yHLJ3rWElnNdRaNdQv59+lShrZF7Bvu7Zvc0mMNmFOM/mQ/K2Lt OK/8bh6iwNNbEuyOhNQlarEy/w8hF8Yf55hBeu/rajGtcyURJDloQ/vNzcx4RWGK G3CLr8ka7zPYIjIFUvHLt27mcYFF9F4/G7b4HKpn75ICKC4vPoQSaYNAHlHQBLFb Jg/WPl93SySHLugU5F58sICs+fBZadXYQG5dWmbaF5OWB1K2XgRs45BQaBzf/8oS qq0scN8wVhAdBeYlVFf0ImDOxGlZ2suLK1BKJboR6zCIkBAwufKss4NV1R9KSUMv YGn3mq13PGme0QoIkvQkua5VjTwWfQx7wFDxZ3VQSsjIlbVyRL/Ac/hq71eAmiIR t6ZMNMPFpuSwBfYimrXqqb4EOffrfsTzRenG1Cxm4jNZRzX/6P4an7F/euoqXeXZ h37TiC7df+eHKcBI4mL+qOW4ibBqg8WwWPJ+jvuhANyQpRVzf3NNEOwJYCNbQPM/ PbqYvMruAH+gg7uyu9u0jX3o/yLSxJMV7kF4x/SCDuBKIxSSUI4cgbjIlnxWLXZC wl7KW4xAKkerO3wgIPnxNfxQoiYiEKA1c3PShWRA0wHIMt3rVRJxwGM4CwARAQAB zRJ0YXlsb3JAbGFyYXZlbC5jb23CwXAEEwEKABoFAlugFSQCGy8DCwkHAxUKCAIe AQIXgAIZAQAKCRDKAI7r/Ml7Zo0SD/9zwu9K87rbqXbvZ3TVu7TnN+z7mPvVBzl+ SFEK360TYq8a4GosghZuGm4aNEyZ90CeUjPQwc5fHwa26tIwqgLRppsG21B/mZwu 0m8c5RaBFRFX/mCTEjlpvBkOwMJZ8f05nNdaktq6W98DbMN03neUwnpWlNSLeoNI u4KYZmJopNFLEax5WGaaDpmqD1J+WDr/aPHx39MUAg2ZVuC3Gj/IjYZbD1nCh0xD a09uDODje8a9uG33cKRBcKKPRLZjWEt5SWReLx0vsTuqJSWhCybHRBl9BQTc/JJR gJu5V4X3f1IYMTNRm9GggxcXrlOAiDCjE2J8ZTUt0cSxedQFnNyGfKxe/l94oTFP wwFHbdKhsSDZ1OyxPNIY5OHlMfMvvJaNbOw0xPPAEutPwr1aqX9sbgPeeiJwAdyw mPw2x/wNQvKJITRv6atw56TtLxSevQIZGPHCYTSlsIoi9jqh9/6vfq2ruMDYItCq +8uzei6TyH6w+fUpp/uFmcwZdrDwgNVqW+Ptu+pD2WmthqESF8UEQVoOv7OPgA5E ofOMaeH2ND74r2UgcXjPxZuUp1RkhHE2jJeiuLtbvOgrWwv3KOaatyEbVl+zHA1e 1RHdJRJRPK+S7YThxxduqfOBX7E03arbbhHdS1HKhPwMc2e0hNnQDoNxQcv0GQp4 2Y6UyCRaus7ATQRboBUkAQgA0h5j3EO2HNvp8YuT1t/VF00uUwbQaz2LIoZogqgC 14Eb77diuIPM9MnuG7bEOnNtPVMFXxI5UYBIlzhLMxf7pfbrsoR4lq7Ld+7KMzdm eREqJRgUNfjZhtRZ9Z+jiFPr8AGpYxwmJk4v387uQGh1GC9JCc3CCLJoI62I9t/1 K2b25KiOzW/FVZ/vYFj1WbISRd5GqS8SEFh4ifU79LUlJ/nEsFv4JxAXN9RqjU0e H4S/m1Nb24UCtYAv1JKymcf5O0G7kOzvI0w06uKxk0hNwspjDcOebD8Vv9IdYtGl 0bn7PpBlVO1Az3s8s6Xoyyw+9Us+VLNtVka3fcrdaV/n0wARAQABwsKEBBgBCgAP BQJboBUkBQkPCZwAAhsuASkJEMoAjuv8yXtmwF0gBBkBCgAGBQJboBUkAAoJEA1I 8aTLtYHmjpIH/A1ZKwTGetHFokJxsd2omvbqv+VtpAjnUbvZEi5g3yZXn+dHJV+K UR/DNlfGxLWEcY6datJ3ziNzzD5u8zcPp2CqeTlCxOky8F74FjEL9tN/EqUbvvmR td2LXsSFjHnLJRK5lYfZ3rnjKA5AjqC9MttILBovY2rI7lyVt67kbS3hMHi8AZl8 EgihnHRJxGZjEUxyTxcB13nhfjAvxQq58LOj5754Rpe9ePSKbT8DNMjHbGpLrESz cmyn0VzDMLfxg8AA9uQFMwdlKqve7yRZXzeqvy08AatUpJaL7DsS4LKOItwvBub6 tHbCE3mqrUw5lSNyUahO3vOcMAHnF7fd4W++eA//WIQKnPX5t3CwCedKn8Qkb3Ow oj8xUNl2T6kEtQJnO85lKBFXaMOUykopu6uB9EEXEr0ShdunOKX/UdDbkv46F2AB 7TtltDSLB6s/QeHExSb8Jo3qra86JkDUutWdJxV7DYFUttBga8I0GqdPu4yRRoc/ 0irVXsdDY9q7jz6l7fw8mSeJR96C0Puhk70t4M1Vg/tu/ONRarXQW7fJ8kl21PcD UKNWWa242gji/+GLRI8AIpGMsBiX7pHhqmMMth3u7+ne5BZGGJz0uX+CzWboOHyq kWgfY4a62t3hM0vwnUkl/D7VgSGy4LiKQrapd3LvU2uuEfFsMu3CDicZBRXPqoXj PBjkkPKhwUTNlwEQrGF3QsZhNe0M9ptM2fC34qtxZtMIMB2NLvE4S621rmQ05oQv sT0B9WgUL3GYRKdx700+ojHEuwZ79bcLgo1dezvkfPtu/++2CXtieFthDlWHy8x5 XJJjI1pDfGO+BgX0rS3QrQEYlF/uPQynKwxe6cGI62eZ0ug0hNrPvKEcfMLVqBQv w4VH6iGp9yNKMUOgAECLCs4YCxK+Eka9Prq/Gh4wuqjWiX8m66z8YvKf27sFL3fR OwGaz3LsnRSxbk/8oSiZuOVLfn44XRcxsHebteZat23lwD93oq54rtKnlJgmZHJY 4vMgk1jpS4laGnvhZj7OwE0EW6AVJAEIAKJSrUvXRyK3XQnLp3Kfj82uj0St8Dt2 h8BMeVbrAbg38wCN8XQZzVR9+bRZRR+aCzpKSqwhEQVtH7gdKgfdNdGNhG2DFAVk SihMhQz190FKttUZgwY00enzD7uaaA5VwNAZzRIr8skwiASB7UoO+lIhrAYgcQCA LpwCSMrUNB3gY1IVa2xi9FljEbS2uMABfOsTfl7z4L4T4DRv/ovDf+ihyZOXsXiH RVoUTIpN8ZILCZiiKubE1sMj4fSQwCs06UyDy17HbOG5/dO9awR/LHW53O3nZCxE JbCqr5iHa2MdHMC12+luxWJKD9DbVB01LiiPZCTkuKUDswCyi7otpVEAEQEAAcLC hAQYAQoADwUCW6AVJAUJDwmcAAIbLgEpCRDKAI7r/Ml7ZsBdIAQZAQoABgUCW6AV JAAKCRDxrCjKN7eORjt2B/9EnKVJ9lwB1JwXcQp6bZgJ21r6ghyXBssv24N9UF+v 5QDz/tuSkTsKK1UoBrBDEinF/xTP2z+xXIeyP4c3mthMHsYdMl7AaGpcCwVJiL62 fZvd+AiYNX3C+Bepwnwoziyhx4uPaqoezSEMD8G2WQftt6Gqttmm0Di5RVysCECF EyhkHwvCcbpXb5Qq+4XFzCUyaIZuGpe+oeO7U8B1CzOC16hEUu0Uhbk09Xt6dSbS ZERoxFjrGU+6bk424MkZkKvNS8FdTN2s3kQuHoNmhbMY+fRzKX5JNrcQ4dQQufiB zFcc2Ba0JVU0nYAMftTeT5ALakhwSqr3AcdD2avJZp3EYfYP/3smPGTeg1cDJV3E WIlCtSlhbwviUjvWEWJUE+n9MjhoUNU0TJtHIliUYUajKMG/At5wJZTXJaKVUx32 UCWr4ioKfSzlbp1ngBuFlvU7LgZRcKbBZWvKj/KRYpxpfvPyPElmegCjAk6oiZYV LOV+jFfnMkk9PnR91ZZfTNx/bK+BwjOnO+g7oE8V2g2bA90vHdeSUHR52SnaVN/b 9ytt07R0f+YtyKojuPmlNsbyAaUYUtJ1o+XNCwdVxzarYEuUabhAfDiVTu9n8wTr YVvnriSFOjNvOY9wdLAa56n7/qM8bzuGpoBS5SilXgJvITvQfWPvg7I9C3QhwK1S F6B1uquQGbBSze2wlnMbKXmhyGLlv9XpOqpkkejQo3o58B+Sqj4B8DuYixSjoknr pRbj8gqgqBKlcpf1wD5X9qCrl9vq19asVOHaKhiFZGxZIVbBpBOdvAKaMj4p/uln yklN3YFIfgmGPYbL0elvXVn7XfvwSV1mCQV5LtMbLHsFf0VsA16UsG8A/tLWtwgt 0antzftRHXb+DI4qr+qEYKFkv9F3oCOXyH4QBhPA42EzKqhMXByEkEK9bu6skioL mHhDQ7yHjTWcxstqQjkUQ0T/IF9ls+Sm5u7rVXEifpyI7MCb+76kSCDawesvInKt WBGOG/qJGDlNiqBYYt2xNqzHCJoC =zXOv -----END PGP PUBLIC KEY BLOCK----- ``` ================================================ FILE: .github/SUPPORT.md ================================================ # Support Questions The Laravel support guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions#support-questions). ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: [push, pull_request] jobs: build: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository timeout-minutes: 15 runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm build:all ================================================ FILE: .github/workflows/coding-standards.yml ================================================ name: Coding Standards on: push: branches: - master pull_request: jobs: format: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 - name: Install dependencies run: pnpm install - name: Format code run: pnpm run format - name: Commit linted files uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: 'Fix code style' ================================================ FILE: .github/workflows/es2020-compatibility.yml ================================================ name: Compatibility Checks on: [push, pull_request] jobs: es2020-compatibility: name: ES2020 (${{ matrix.adapter }}) if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-24.04 timeout-minutes: 15 strategy: fail-fast: false matrix: adapter: ['core', 'react', 'vue', 'svelte'] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build core package if: matrix.adapter != 'core' run: pnpm -r --filter ./packages/core build - name: Validate ES2020 compatibility run: pnpm -r --filter ./packages/${{ matrix.adapter }}* es2020-check ================================================ FILE: .github/workflows/playwright-chromium.yml ================================================ name: Playwright Tests on Chromium on: [push, pull_request] jobs: test-chromium: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository name: Chromium (${{ matrix.adapter }}) timeout-minutes: 15 runs-on: ubuntu-24.04 strategy: matrix: adapter: ['vue', 'react', 'svelte'] steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build - name: Install Playwright Browsers run: pnpm playwright install chromium - name: Run Playwright Tests run: pnpm test:${{ matrix.adapter }} - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: playwright-failure-screenshots-${{ matrix.adapter }}-chromium path: test-results test-chromium-ssr: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository name: Chromium SSR (${{ matrix.adapter }}) timeout-minutes: 15 runs-on: ubuntu-24.04 strategy: matrix: adapter: ['vue', 'react', 'svelte'] steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build - name: Install Playwright Browsers run: pnpm playwright install chromium - name: Run Playwright SSR Tests run: pnpm test:ssr:${{ matrix.adapter }} --project=chromium - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: playwright-failure-screenshots-${{ matrix.adapter }}-chromium-ssr path: test-results ================================================ FILE: .github/workflows/playwright-firefox.yml ================================================ name: Playwright Tests on Firefox on: [push, pull_request] jobs: test-firefox: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository name: Firefox (${{ matrix.adapter }}) - shard ${{ matrix.shard }} of 3) timeout-minutes: 15 runs-on: ubuntu-24.04 strategy: matrix: adapter: ['vue', 'react', 'svelte'] shard: ['1', '2', '3'] steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build - name: Install Playwright Browsers run: pnpm playwright install firefox - name: Run Playwright Tests run: pnpm test:${{ matrix.adapter }} --firefox --shard=${{ matrix.shard }}/3 - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: playwright-failure-screenshots-${{ matrix.adapter }}-firefox-shard-${{ matrix.shard }} path: test-results ================================================ FILE: .github/workflows/playwright-webkit.yml ================================================ name: Playwright Tests on WebKit on: [push, pull_request] jobs: test-webkit: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository name: WebKit (${{ matrix.adapter }} - shard ${{ matrix.shard }} of 4) timeout-minutes: 15 runs-on: macos-15 strategy: matrix: adapter: ['vue', 'react', 'svelte'] shard: ['1', '2', '3', '4'] steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build - name: Install Playwright Browsers run: pnpm playwright install webkit - name: Run Playwright Tests run: pnpm test:${{ matrix.adapter }} --webkit --shard=${{ matrix.shard }}/4 - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: playwright-failure-screenshots-${{ matrix.adapter }}-webkit-shard-${{ matrix.shard }} path: test-results ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish Packages on: release: types: [released, prereleased] permissions: id-token: write # Required for OIDC contents: read jobs: publish: runs-on: ubuntu-latest strategy: matrix: adapter: ['core', 'vue3', 'react', 'svelte'] steps: - name: Checkout uses: actions/checkout@v6 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 - uses: actions/setup-node@v6 with: node-version: 24 registry-url: 'https://registry.npmjs.org' cache: pnpm # Ensure npm 11.5.1 or later is installed - name: Update npm run: npm install -g npm@latest - name: Install dependencies run: pnpm install - name: 'Publish ${{ matrix.adapter }}' run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }} build - name: 'Publish ${{ matrix.adapter }} to npm' run: cd ./packages/${{ matrix.adapter }} && pnpm publish --no-git-checks ================================================ FILE: .github/workflows/test-app-quality.yml ================================================ name: Test App Quality on: [push, pull_request] jobs: test-app-quality: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository timeout-minutes: 15 runs-on: ubuntu-24.04 strategy: matrix: adapter: ['vue', 'react', 'svelte'] steps: - name: Checkout uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.14 cache: pnpm - name: Install dependencies run: pnpm install - name: Build Inertia run: pnpm -r --filter ./packages/core --filter ./packages/${{ matrix.adapter }}* build - name: Type-check test-app run: cd packages/${{ matrix.adapter == 'vue' && 'vue3' || matrix.adapter }}/test-app && pnpm run type-check - name: ESLint test-app run: cd packages/${{ matrix.adapter == 'vue' && 'vue3' || matrix.adapter }}/test-app && pnpm run lint ================================================ FILE: .github/workflows/update-changelog.yml ================================================ name: update changelog on: release: types: [released] permissions: {} jobs: update: permissions: contents: write uses: laravel/.github/.github/workflows/update-changelog.yml@main ================================================ FILE: .gitignore ================================================ .DS_Store .idea /packages/*/test-app/test-results/.last-run.json /packages/svelte/test-app/vite.config.js.timestamp-*.mjs /playwright-report /test-results node_modules ================================================ FILE: .prettierignore ================================================ # Dependencies node_modules/ **/vendor/ # Build outputs **/dist/ **/.svelte-kit **/bootstrap/ssr **/public/build # Files we don't want to format pnpm-lock.yaml *.lock *.md *.timestamp-*.mjs # Vue files with parsing issues (dual script blocks) packages/vue3/test-app/Pages/PersistentLayouts/RenderFunction/Nested/PageA.vue packages/vue3/test-app/Pages/PersistentLayouts/RenderFunction/Nested/PageB.vue packages/vue3/test-app/Pages/PersistentLayouts/RenderFunction/Simple/PageA.vue packages/vue3/test-app/Pages/PersistentLayouts/RenderFunction/Simple/PageB.vue packages/vue3/test-app/Pages/PersistentLayouts/Shorthand/Nested/PageA.vue packages/vue3/test-app/Pages/PersistentLayouts/Shorthand/Nested/PageB.vue # Directories we don't want to format **/test-results/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). For changes prior to v1.0.0, see the [legacy releases](https://legacy.inertiajs.com/releases). ## [Unreleased](https://github.com/inertiajs/inertia/compare/v2.3.18...master) - Nothing yet ## [v2.3.18](https://github.com/inertiajs/inertia/compare/v2.3.17...v2.3.18) - 2026-03-12 ### What's Changed * Bump [@sveltejs](https://github.com/sveltejs)/kit from 2.53.2 to 2.53.3 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2919 * Bump multer from 2.0.2 to 2.1.1 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2923 * [2.x] Remove request from stream on network failure by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2948 * Use SharedPageProps in GlobalEventsMap event types by [@hamedelasma](https://github.com/hamedelasma) in https://github.com/inertiajs/inertia/pull/2946 * [2.x] fix: include SharedPageProps in createInertiaApp and onSuccess types by [@isaackaara](https://github.com/isaackaara) in https://github.com/inertiajs/inertia/pull/2931 * [2.x] Remove `server-renderer` dependency from Vue adapter type by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2955 * [2.x] fix(types): module augmentation example by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2954 * [2.x] fix: always fire flash event regardless of partial reload equality by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2953 * fix: skip view transition when document visibility is hidden by [@mortenhauberg](https://github.com/mortenhauberg) in https://github.com/inertiajs/inertia/pull/2957 ### New Contributors * [@hamedelasma](https://github.com/hamedelasma) made their first contribution in https://github.com/inertiajs/inertia/pull/2946 * [@mortenhauberg](https://github.com/mortenhauberg) made their first contribution in https://github.com/inertiajs/inertia/pull/2957 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.17...v2.3.18 ## [v2.3.17](https://github.com/inertiajs/inertia/compare/v2.3.16...v2.3.17) - 2026-02-26 ### What's Changed * Include resources directory in published packages by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/inertiajs/inertia/pull/2914 * [2.x] Bump deps by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2916 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.16...v2.3.17 ## [v2.3.16](https://github.com/inertiajs/inertia/compare/v2.3.15...v2.3.16) - 2026-02-24 ### What's Changed * Bump [@sveltejs](https://github.com/sveltejs)/kit from 2.50.2 to 2.52.2 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2904 * Add Boost Guidelines & Skills by [@pushpak1300](https://github.com/pushpak1300) in https://github.com/inertiajs/inertia/pull/2896 ### New Contributors * [@pushpak1300](https://github.com/pushpak1300) made their first contribution in https://github.com/inertiajs/inertia/pull/2896 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.15...v2.3.16 ## [v2.3.15](https://github.com/inertiajs/inertia/compare/v2.3.14...v2.3.15) - 2026-02-13 ### What's Changed * Bump axios from 1.13.2 to 1.13.5 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2888 * [2.x] Fix flash data being cleared by `history.replaceState` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2889 * [2.x] Handle `bfcache` restoration for encrypted history by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2890 * [2.x] Bump dependencies by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2891 * [2.x] Fix InfiniteScroll loading all pages in reverse mode with flex/grid layouts by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2893 * [2.x] Improve flaky tests and test app quality by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2895 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.14...v2.3.15 ## [v2.3.14](https://github.com/inertiajs/inertia/compare/v2.3.13...v2.3.14) - 2026-02-11 ### What's Changed * [2.x] Shut down entire cluster on SSR shutdown by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2876 * [2.x] Fix `useForm` type inference when passing data as callback by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2878 * Add global configuration support for withAllErrors in form components by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2865 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.13...v2.3.14 ## [v2.3.13](https://github.com/inertiajs/inertia/compare/v2.3.12...v2.3.13) - 2026-01-30 ### What's Changed * Fix `useForm` type inference in generic wrapper components by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2851 * Support wildcard paths in `validate()` method by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2854 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.12...v2.3.13 ## [v2.3.12](https://github.com/inertiajs/inertia/compare/v2.3.11...v2.3.12) - 2026-01-27 ### What's Changed * Bump lodash from 4.17.21 to 4.17.23 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2835 * Bump lodash-es from 4.17.22 to 4.17.23 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2836 * Fix cancellation of concurrent partial reloads with query parameters by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2843 * Support for the `formTarget` attribute in the `
` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2844 * Clear stale form errors on resubmit by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2845 * Prevent `` from rendering children with undefined props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2846 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.11...v2.3.12 ## [v2.3.11](https://github.com/inertiajs/inertia/compare/v2.3.10...v2.3.11) - 2026-01-20 ### What's Changed * Bump and cleanup dependencies by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2822 * Add test for Precognition validation with transform key changes by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2827 * TS and console error on conflicting `useForm()` keys by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2823 * Allow `useForm` without arguments by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2830 * Pass `true` to `inert` attribute in React 19 by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2831 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.10...v2.3.11 ## [v2.3.10](https://github.com/inertiajs/inertia/compare/v2.3.9...v2.3.10) - 2026-01-15 ### What's Changed * Add `async` and `sync` options to `cancelAll` method by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2814 * Fix smooth scrolling in Firefox and add Firefox to CI by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2815 * Improve flaky `` test in WebKit CI by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2818 * Pass `onceProps` as second argument in client-side visit props callback by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2816 * Prevent converting a string response to an object by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2821 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.9...v2.3.10 ## [v2.3.9](https://github.com/inertiajs/inertia/compare/v2.3.8...v2.3.9) - 2026-01-14 ### What's Changed * Fix React Precognition Error Sync (issue #2806) by [@joetifa2003](https://github.com/joetifa2003) in https://github.com/inertiajs/inertia/pull/2808 * Add tests for deferred scroll props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2811 * Fix deferred props not loading after back button navigation by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2812 * Add test for concurrent reloads by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2813 * Cancel in-flight deferred prop requests on navigation by [@alexspeller](https://github.com/alexspeller) in https://github.com/inertiajs/inertia/pull/2656 * fix: preserve query parameters in test server responses by [@alexspeller](https://github.com/alexspeller) in https://github.com/inertiajs/inertia/pull/2665 * [2.x] Add Form Context Support by [@laserhybiz](https://github.com/laserhybiz) in https://github.com/inertiajs/inertia/pull/2671 ### New Contributors * [@joetifa2003](https://github.com/joetifa2003) made their first contribution in https://github.com/inertiajs/inertia/pull/2808 * [@alexspeller](https://github.com/alexspeller) made their first contribution in https://github.com/inertiajs/inertia/pull/2656 * [@laserhybiz](https://github.com/laserhybiz) made their first contribution in https://github.com/inertiajs/inertia/pull/2671 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.8...v2.3.9 ## [v2.3.8](https://github.com/inertiajs/inertia/compare/v2.3.7...v2.3.8) - 2026-01-09 ### What's Changed * fix: update has more state when resetting before updating page by [@AydinHassan](https://github.com/AydinHassan) in https://github.com/inertiajs/inertia/pull/2787 * Ensure Flash Data listener is registered before event is fired by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2799 * Improve indices detection in `mergeDataIntoQueryString()` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2798 * Fix `` re-registering observer when params change by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2804 ### New Contributors * [@AydinHassan](https://github.com/AydinHassan) made their first contribution in https://github.com/inertiajs/inertia/pull/2787 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.7...v2.3.8 ## [v2.3.7](https://github.com/inertiajs/inertia/compare/v2.3.6...v2.3.7) - 2026-01-07 ### What's Changed * Add `dontRemember()` method to form helper by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2792 * Only call `replaceState()` when page data has actually changed by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2793 * Fix `@typescript-eslint/unbound-method` warning when destructuring `useForm()` methods by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2794 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.6...v2.3.7 ## [v2.3.6](https://github.com/inertiajs/inertia/compare/v2.3.5...v2.3.6) - 2025-12-31 ### What's Changed * Bump qs from 6.14.0 to 6.14.1 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2790 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.5...v2.3.6 ## [v2.3.5](https://github.com/inertiajs/inertia/compare/v2.3.4...v2.3.5) - 2025-12-31 ### What's Changed * Refresh the Infinite Scroll triggers after partial reload by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2784 * Fix `hasAnyState()` to actually check for browser history by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2785 * Handle WebKit history throttle errors by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2786 * Merge `data` and `params` props in `` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2789 * Handle `QuotaExceededError` in WebKit by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2788 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.4...v2.3.5 ## [v2.3.4](https://github.com/inertiajs/inertia/compare/v2.3.3...v2.3.4) - 2025-12-19 ### What's Changed * Only restore Infinite Scroll state from history on back/forward navigation by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2777 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.3...v2.3.4 ## [v2.3.3](https://github.com/inertiajs/inertia/compare/v2.3.2...v2.3.3) - 2025-12-17 ### What's Changed * Add support for protocol-relative urls in url.ts by [@machour](https://github.com/machour) in https://github.com/inertiajs/inertia/pull/2769 * Fix brackets notation qs parsing by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2722 * Support for Flash Data by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2757 ### New Contributors * [@machour](https://github.com/machour) made their first contribution in https://github.com/inertiajs/inertia/pull/2769 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.2...v2.3.3 ## [v2.3.2](https://github.com/inertiajs/inertia/compare/v2.3.1...v2.3.2) - 2025-12-16 ### What's Changed * Expose InertiaPrecognitiveForm type by [@lcdss](https://github.com/lcdss) in https://github.com/inertiajs/inertia/pull/2756 * Test for loading deferred props on `router.reload()` without `only`/`except` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2761 * Expose `fetching` in default `` slot by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2766 * Include submitter element value in Form component submission by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2770 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.1...v2.3.2 ## [v2.3.1](https://github.com/inertiajs/inertia/compare/v2.3.0...v2.3.1) - 2025-12-12 ### What's Changed * Test for Form + Vue Options API by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2750 * Fix for validating items in dynamic arrays by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2753 * Escape forward slashes when using useScriptElementForInitialPage by [@kirk-loretz-fsn](https://github.com/kirk-loretz-fsn) in https://github.com/inertiajs/inertia/pull/2751 * Sync Playground configs by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2754 * Fix race condition when restoring scroll regions by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2755 ### New Contributors * [@kirk-loretz-fsn](https://github.com/kirk-loretz-fsn) made their first contribution in https://github.com/inertiajs/inertia/pull/2751 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.3.0...v2.3.1 ## [v2.3.0](https://github.com/inertiajs/inertia/compare/v2.2.21...v2.3.0) - 2025-12-11 ### What's Changed * Support for Precognition in `useForm()` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2684 * Support for Precognition in `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2700 * Improve Precognition examples in Playgrounds by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2746 * Improve flaky tests by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2747 * bugfix(whenVisible-vue): Fix loaded state when data already exists by [@ClaraLeigh](https://github.com/ClaraLeigh) in https://github.com/inertiajs/inertia/pull/2748 ### New Contributors * [@ClaraLeigh](https://github.com/ClaraLeigh) made their first contribution in https://github.com/inertiajs/inertia/pull/2748 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.21...v2.3.0 ## [v2.2.21](https://github.com/inertiajs/inertia/compare/v2.2.20...v2.2.21) - 2025-12-10 ### What's Changed * Add `viewTransition` to `FormComponentOptions` type by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2741 * Preserve untouched Once Props on Partial Reload + Once Props in Playground by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2743 * Only preserve loaded Deferred + Once props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2745 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.20...v2.2.21 ## [v2.2.20](https://github.com/inertiajs/inertia/compare/v2.2.19...v2.2.20) - 2025-12-09 ### What's Changed * Bump express from 5.1.0 to 5.2.0 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2727 * Add tests for SSR server by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2730 * Preserve errors when loading deferred props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2729 * Optimize page data size and parsing (37% size reduction!) by [@bram-pkg](https://github.com/bram-pkg) in https://github.com/inertiajs/inertia/pull/2687 * Support for `once()` props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2732 * Fix for sequential Client Side Visits by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2737 * Refactor duplicated initial page code by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2738 ### New Contributors * [@bram-pkg](https://github.com/bram-pkg) made their first contribution in https://github.com/inertiajs/inertia/pull/2687 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.19...v2.2.20 ## [v2.2.19](https://github.com/inertiajs/inertia/compare/v2.2.18...v2.2.19) - 2025-11-27 ### What's Changed * Bump dependencies by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2710 * TypeScript fix accessing error keys of optional nested object by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2718 * Use FormValue in Form component by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2709 * Fix anchor hash scrolling on initial page visit in React by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2719 * Ensure page is rendered before scrolling to top by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2721 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.18...v2.2.19 ## [v2.2.18](https://github.com/inertiajs/inertia/compare/v2.2.17...v2.2.18) - 2025-11-17 ### What's Changed * Ensure `objectsAreEqual()` checks all keys in both objects by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2705 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.17...v2.2.18 ## [v2.2.17](https://github.com/inertiajs/inertia/compare/v2.2.16...v2.2.17) - 2025-11-14 ### What's Changed * Reset `` loading state after a page reload by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2699 * Add test for reloading deferred props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2698 * Force `indices` array format when submitting data using `FormData` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2701 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.16...v2.2.17 ## [v2.2.16](https://github.com/inertiajs/inertia/compare/v2.2.15...v2.2.16) - 2025-11-13 ### What's Changed * Added test for `defaultValue` in Form component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2685 * Prevent navigation on right-click on `` with `prefetch="click"` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2676 * Export page component type for React adapter by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2691 * Switch `useContext` to `use` in `usePage()` hook by [@HichemTab-tech](https://github.com/HichemTab-tech) in https://github.com/inertiajs/inertia/pull/2680 * Improve serialization in `formDataToObject()` when mixing numeric and non-numeric object keys by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2692 * Fix `InfiniteScroll` scroll preservation by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2689 * Export Inertia `App` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2695 * Ignore `preserveScroll` and `preserveState` when finding cached response by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2694 * Upgrade Express server for test apps to v5 by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2693 * Add WebKit browser testing to CI with Safari compatibility fixes by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2696 * Bump symfony/http-foundation from 7.3.4 to 7.3.7 in /playgrounds/vue3 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2697 * Fix array keys misalignment in form data and query by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2690 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.15...v2.2.16 ## [v2.2.15](https://github.com/inertiajs/inertia/compare/v2.2.14...v2.2.15) - 2025-10-30 ### What's Changed * TS Fix for circularly references in form data by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2673 * Improve TS for config defaults by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2674 * [v2.x] feat: allow adding type to `router.restore` by [@peaklabs-dev](https://github.com/peaklabs-dev) in https://github.com/inertiajs/inertia/pull/2545 * Configurable prefetch hover delay by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2675 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.14...v2.2.15 ## [v2.2.14](https://github.com/inertiajs/inertia/compare/v2.2.13...v2.2.14) - 2025-10-28 ### What's Changed * TS cleanup for `` component + View Transition prop in Svelte by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2667 * Improve support for `any` as form data value by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2668 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.13...v2.2.14 ## [v2.2.13](https://github.com/inertiajs/inertia/compare/v2.2.12...v2.2.13) - 2025-10-28 ### What's Changed * Support for View Transitions by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2658 * Opt-in to using `data-inertia` attribute in `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2663 * Opt-in to using `` for error modals by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2664 * feat: give access to underlying data as object and as form data object by [@MeiKatz](https://github.com/MeiKatz) in https://github.com/inertiajs/inertia/pull/2605 ### New Contributors * [@MeiKatz](https://github.com/MeiKatz) made their first contribution in https://github.com/inertiajs/inertia/pull/2605 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.12...v2.2.13 ## [v2.2.12](https://github.com/inertiajs/inertia/compare/v2.2.11...v2.2.12) - 2025-10-27 ### What's Changed * Clone page props before writing it to the browser's history by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2662 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.11...v2.2.12 ## [v2.2.11](https://github.com/inertiajs/inertia/compare/v2.2.10...v2.2.11) - 2025-10-24 ### What's Changed * Configure global defaults and update during runtime by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2655 * Stabilize prop references when visiting the same page by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2657 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.10...v2.2.11 ## [v2.2.10](https://github.com/inertiajs/inertia/compare/v2.2.9...v2.2.10) - 2025-10-23 ### What's Changed * Restore uppercase `Component` object key in React's `App.ts` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2654 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.9...v2.2.10 ## [v2.2.9](https://github.com/inertiajs/inertia/compare/v2.2.8...v2.2.9) - 2025-10-21 ### What's Changed * Use local `@inertiajs/core` in Playgrounds + dependencies bump by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2633 * Introduce types for Head Manager by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2634 * Fix resolving `preserveScroll` and `preserveState` in Client Side Visits by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2635 * Support for type-hinting shared Page Props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2636 * Add `globals.d.ts` file to Playgrounds by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2637 * Remove wrong `shouldIntercept()` call in `keydown` event handler in `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2638 * Introduce `CancelToken` and `CancelTokenCallback` types to replace Axios imports by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2639 * Internal TypeScript improvements by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2640 * Tests and TS improvements for the `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2641 * Make `data` prop of `` required by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2642 * TS fixes in Vue adapter for `useRemember` and `remember` mixin by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2643 * Bump vite from 5.4.20 to 5.4.21 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2647 * TypeScript improvements to `createInertiaApp()` and unifying it across adapters by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2648 * ESLint check for test-apps by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2560 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.8...v2.2.9 ## [v2.2.8](https://github.com/inertiajs/inertia/compare/v2.2.7...v2.2.8) - 2025-10-09 ### What's Changed * Prevent false positives in `getScrollableParent()` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2626 * Restore scroll regions after navigation by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2627 * Prevent replacing history state when scroll regions are unchanged to fix popstate behavior in WebKit by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2629 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.7...v2.2.8 ## [v2.2.7](https://github.com/inertiajs/inertia/compare/v2.2.6...v2.2.7) - 2025-10-07 ### What's Changed * Preserve relative URL when `` updates query string by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2623 * Use `SlotsType` to type-hint Vue slots by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2620 * Fix race condition in `history.ts` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2624 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.6...v2.2.7 ## [v2.2.6](https://github.com/inertiajs/inertia/compare/v2.2.5...v2.2.6) - 2025-10-03 ### What's Changed * SSR fixes for `` component. by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2616 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.5...v2.2.6 ## [v2.2.5](https://github.com/inertiajs/inertia/compare/v2.2.4...v2.2.5) - 2025-10-02 ### What's Changed * Improve `` cleanup after navigating away by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2610 * Fix for `` component when using React SSR by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2612 * Fix conflicting Client Side Visits by queuing the URL synchronization by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2613 * Improvements to `` in Svelte adapter by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2614 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.4...v2.2.5 ## [v2.2.4](https://github.com/inertiajs/inertia/compare/v2.2.3...v2.2.4) - 2025-09-30 ### What's Changed * Compile TS while developing + improve CLI output by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2600 * Improve testing of scroll restoration by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2602 * Fix for reloading an unrelated prop affecting infinite scroll by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2603 * Add `preserve-url` prop to `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2541 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.3...v2.2.4 ## [v2.2.3](https://github.com/inertiajs/inertia/compare/v2.2.2...v2.2.3) - 2025-09-29 ### What's Changed * Preserve `ScrollProp` on Partial Reloads by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2597 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.2...v2.2.3 ## [v2.2.2](https://github.com/inertiajs/inertia/compare/v2.2.1...v2.2.2) - 2025-09-28 ### What's Changed * Reset `ScrollProp` when requested by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2595 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.1...v2.2.2 ## [v2.2.1](https://github.com/inertiajs/inertia/compare/v2.2.0...v2.2.1) - 2025-09-28 ### What's Changed * Don't restore remembered state after a refresh by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2591 * Remember Infinite Scroll state by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2592 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.2.0...v2.2.1 ## [v2.2.0](https://github.com/inertiajs/inertia/compare/v2.1.11...v2.2.0) - 2025-09-26 ### What's Changed * Support for merging nested prop paths by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2561 * Client-side visit helpers to update props by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2589 * Introduction of the `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2580 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.11...v2.2.0 ## [v2.1.11](https://github.com/inertiajs/inertia/compare/v2.1.10...v2.1.11) - 2025-09-24 ### What's Changed * Fix flaky tests in CI by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2582 * Bump Playwright by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2585 * Progress indicator API by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2581 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.10...v2.1.11 ## [v2.1.10](https://github.com/inertiajs/inertia/compare/v2.1.9...v2.1.10) - 2025-09-22 ### What's Changed * Fix PNPM publishing by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2578 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.9...v2.1.10 ## [v2.1.9](https://github.com/inertiajs/inertia/compare/v2.1.8...v2.1.9) - 2025-09-22 ### What's Changed * Fix PNPM build by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2577 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.8...v2.1.9 ## [v2.1.8](https://github.com/inertiajs/inertia/compare/v2.1.7...v2.1.8) - 2025-09-22 ### What's Changed * Publish packages in CI by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2575 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.7...v2.1.8 ## [v2.1.7](https://github.com/inertiajs/inertia/compare/v2.1.6...v2.1.7) - 2025-09-18 ### What's Changed * Bump axios from 1.11.0 to 1.12.0 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2568 * Bump dependencies by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2571 * TypeScript upgrade by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2573 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.6...v2.1.7 ## [v2.1.6](https://github.com/inertiajs/inertia/compare/v2.1.5...v2.1.6) - 2025-09-12 ### What's Changed * Invalidate prefetch cache when page is received from a network request by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2567 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.5...v2.1.6 ## [v2.1.5](https://github.com/inertiajs/inertia/compare/v2.1.4...v2.1.5) - 2025-09-05 ### What's Changed * Fix race condition when combining Deferred Props with an instant Partial Reload on mount by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2562 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.4...v2.1.5 ## [v2.1.4](https://github.com/inertiajs/inertia/compare/v2.1.3...v2.1.4) - 2025-09-03 ### What's Changed * Replace html-escape with built-in function on Svelte package by [@kresnasatya](https://github.com/kresnasatya) in https://github.com/inertiajs/inertia/pull/2535 * Update dirty state after DOM changes (React only) by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2558 * Prevent errors caused by null href value by [@fritz-c](https://github.com/fritz-c) in https://github.com/inertiajs/inertia/pull/2550 * Remove data from the dependency array of setDefaults by [@jasonlbeggs](https://github.com/jasonlbeggs) in https://github.com/inertiajs/inertia/pull/2554 ### New Contributors * [@fritz-c](https://github.com/fritz-c) made their first contribution in https://github.com/inertiajs/inertia/pull/2550 * [@jasonlbeggs](https://github.com/jasonlbeggs) made their first contribution in https://github.com/inertiajs/inertia/pull/2554 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.3...v2.1.4 ## [v2.1.3](https://github.com/inertiajs/inertia/compare/v2.1.2...v2.1.3) - 2025-08-27 ### What's Changed * Code formatting with Prettier by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2515 * Add EditorConfig and fix some whitespace issues by [@jrmajor](https://github.com/jrmajor) in https://github.com/inertiajs/inertia/pull/2516 * Fix for nullable object types in `useForm` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2528 * Fix for Form Component in Svelte when resetting use input/button by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2525 * Improve Link component `as` prop by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2524 * [v2.x] fix: type error by changing page props type to `any` by [@peaklabs-dev](https://github.com/peaklabs-dev) in https://github.com/inertiajs/inertia/pull/2520 * Revert to back to Lodash to retain ES2020 compatibility by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2527 * Verify ES2020 compatibility in CI by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2530 * [Vue] Fixing action attribute on Form Component when using Wayfinder by [@nicolagianelli](https://github.com/nicolagianelli) in https://github.com/inertiajs/inertia/pull/2532 * Make package.json structure in Svelte package Consistent as Vue and React by [@kresnasatya](https://github.com/kresnasatya) in https://github.com/inertiajs/inertia/pull/2529 * Remove Svelte 5-next version constraint by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2533 * improve typescript configuration by [@sudo-barun](https://github.com/sudo-barun) in https://github.com/inertiajs/inertia/pull/2470 * Format JSON files with Prettier by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2534 * Fix warning about `inert` attribute in React < 19 by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2536 * Fix keyboard activation when using `prefetch: 'click'` by [@pedroborges](https://github.com/pedroborges) in https://github.com/inertiajs/inertia/pull/2538 * Fix `useForm` to respect manual `setDefaults()` calls in `onSuccess` and unify timing across adapters by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2539 * Run Playwright in parallel in CI by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2540 * Fix Coding Standards workflow by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2547 * bumpup axios to fix CVE-2025-7783 by [@vallerydelexy](https://github.com/vallerydelexy) in https://github.com/inertiajs/inertia/pull/2546 * Bump `@sveltejs/kit` version by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2548 ### New Contributors * [@peaklabs-dev](https://github.com/peaklabs-dev) made their first contribution in https://github.com/inertiajs/inertia/pull/2520 * [@nicolagianelli](https://github.com/nicolagianelli) made their first contribution in https://github.com/inertiajs/inertia/pull/2532 * [@kresnasatya](https://github.com/kresnasatya) made their first contribution in https://github.com/inertiajs/inertia/pull/2529 * [@vallerydelexy](https://github.com/vallerydelexy) made their first contribution in https://github.com/inertiajs/inertia/pull/2546 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.2...v2.1.3 ## [v2.1.2](https://github.com/inertiajs/inertia/compare/v2.1.1...v2.1.2) - 2025-08-15 ### What's Changed * Fix for manipulating form after redirect in `onSubmitComplete` by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2510 * Support for passing Wayfinder objects to router methods by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2497 * Tag-based cache invalidation for prefetch requests by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2484 * Add `resetOnError`, `resetOnSuccess`, `setDefaultsOnSuccess` to Form component by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2514 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.1...v2.1.2 ## [v2.1.1](https://github.com/inertiajs/inertia/compare/v2.1.0...v2.1.1) - 2025-08-14 ### What's Changed * Improve `Link` component types and support for prefetch events by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2464 * allow passing partial errors object to `setError()` by [@sudo-barun](https://github.com/sudo-barun) in https://github.com/inertiajs/inertia/pull/2461 * Add missing generic type support to PendingVisit and VisitHelperOptions by [@HichemTab-tech](https://github.com/HichemTab-tech) in https://github.com/inertiajs/inertia/pull/2454 * Revamp useForm's generic types across adaptors by [@Spice-King](https://github.com/Spice-King) in https://github.com/inertiajs/inertia/pull/2335 * TypeScript improvements for `Link` component and Client Side Visits by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2472 * Further TS improvements for `useForm` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2475 * Improve consistency in `useForm` across adapters by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2482 * Improve TypeScript support for Client Side Visit `props` callback by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2483 * TS improvements to Svelte's `` and `useForm()` implementations by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2489 * Typescript Improvements by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2468 * Test apps in TypeScript by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2469 * Fix empty action in `` component when the current URL has more than one segment by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2501 * Support uppercase method in `` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2502 * Add `Form` component `disableWhileProcessing` prop by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2504 * Reset form fields by name in Form components by [@skryukov](https://github.com/skryukov) in https://github.com/inertiajs/inertia/pull/2499 * Add `onSubmitComplete` prop to `Form` component by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2503 * Remove failed prefetch requests from in-flight array by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2500 * Add `defaults()` method to Form component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2507 * Release script by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2508 * Only `reset()` and `defaults()` in `onSubmitComplete` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2509 ### New Contributors * [@sudo-barun](https://github.com/sudo-barun) made their first contribution in https://github.com/inertiajs/inertia/pull/2461 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.1.0...v2.1.1 ## [v2.1.0](https://github.com/inertiajs/inertia/compare/v2.0.17...v2.1.0) - 2025-08-13 ### What's Changed * Support for passing custom component to `as` prop of `Link` component. by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2443 * Use `nodemon` to trigger new files and deleted files in test apps by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2442 * Use ReactNode type for children props by [@chack1172](https://github.com/chack1172) in https://github.com/inertiajs/inertia/pull/2385 * Allow function as children component in react Deferred and WhenVisible by [@chack1172](https://github.com/chack1172) in https://github.com/inertiajs/inertia/pull/2386 * Improve test that waits for scroll position restoration by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2473 * Introduction of the `Form` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2474 * Improve `children` prop of `` Component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2487 * Add Form component ref support by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2496 * Make Svelte's not crash in an SSR environment by [@dkulchenko](https://github.com/dkulchenko) in https://github.com/inertiajs/inertia/pull/2396 * Fix core: Queue processing when an item fails by [@pintend](https://github.com/pintend) in https://github.com/inertiajs/inertia/pull/2467 * Migrate playgrounds to Tailwind 4 by [@jrmajor](https://github.com/jrmajor) in https://github.com/inertiajs/inertia/pull/2369 ### New Contributors * [@dkulchenko](https://github.com/dkulchenko) made their first contribution in https://github.com/inertiajs/inertia/pull/2396 * [@pintend](https://github.com/pintend) made their first contribution in https://github.com/inertiajs/inertia/pull/2467 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.17...v2.1.0 ## [v2.0.17](https://github.com/inertiajs/inertia/compare/v2.0.16...v2.0.17) - 2025-07-18 ### What's Changed * Bump multer from 2.0.1 to 2.0.2 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2447 * Bump vite from 5.4.12 to 5.4.19 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2450 * Bump esbuild from 0.21.5 to 0.25.0 by [@dependabot](https://github.com/dependabot)[bot] in https://github.com/inertiajs/inertia/pull/2451 * Explicit string coercion in `Head` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2453 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.16...v2.0.17 ## [v2.0.16](https://github.com/inertiajs/inertia/compare/v2.0.15...v2.0.16) - 2025-07-18 ### What's Changed * Make errorBag parameter optional by [@joelstein](https://github.com/joelstein) in https://github.com/inertiajs/inertia/pull/2445 ### New Contributors * [@joelstein](https://github.com/joelstein) made their first contribution in https://github.com/inertiajs/inertia/pull/2445 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.15...v2.0.16 ## [v2.0.15](https://github.com/inertiajs/inertia/compare/v2.0.14...v2.0.15) - 2025-07-17 ### What's Changed * Improve GitHub issue templates by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2418 * Escape the attribute values that are passed into the `Head` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2403 * Introduce single method to reset form state and clear errors by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2414 * Use `CacheForOption` type in React `Link` component by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2426 * Improve query string merging in `mergeDataIntoQueryString()` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2417 * Improve scrolling when using anchor hash by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2428 * Cancel sync request on popstate event by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2429 * Support for path traversal by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2413 * Add event callbacks to `ClientSideVisitOptions` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2405 * Pass parameters to `onFinish` and `onSuccess` callbacks on Client Side Visits by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2433 * Prevent JS builds and test apps from being minified by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2424 * Migrate to pnpm by [@jrmajor](https://github.com/jrmajor) in https://github.com/inertiajs/inertia/pull/2276 * Fix single-use prefetching by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2440 * Change defaults values order in onSuccess callback of useForm by [@yilanboy](https://github.com/yilanboy) in https://github.com/inertiajs/inertia/pull/2437 * Improve reactivity of Link components by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2441 ### New Contributors * [@yilanboy](https://github.com/yilanboy) made their first contribution in https://github.com/inertiajs/inertia/pull/2437 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.14...v2.0.15 ## [v2.0.14](https://github.com/inertiajs/inertia/compare/v2.0.13...v2.0.14) - 2025-06-26 ### What's Changed * fix: fixed type error in `useForm SetDataAction` type by [@fxnm](https://github.com/fxnm) in https://github.com/inertiajs/inertia/pull/2395 * Call `provider.update` outside useEffect block to support SSR by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2397 * Improve state restore logic in `useRemember` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2401 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.13...v2.0.14 ## [v2.0.13](https://github.com/inertiajs/inertia/compare/v2.0.12...v2.0.13) - 2025-06-20 ### What's Changed * Allow deepMerge on custom properties by [@mpociot](https://github.com/mpociot) in https://github.com/inertiajs/inertia/pull/2344 * fix: React StrictMode breaking Inertia Head by [@jordanhavard](https://github.com/jordanhavard) in https://github.com/inertiajs/inertia/pull/2328 * Bump multer from 1.4.4 to 2.0.1 in /tests/app by [@dependabot](https://github.com/dependabot) in https://github.com/inertiajs/inertia/pull/2373 * Initialize router before components in React by [@chack1172](https://github.com/chack1172) in https://github.com/inertiajs/inertia/pull/2379 * Prevent duplicate render of the initial page in React by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2377 * Update default state when `setDefault()` is called right after `setData()` is called by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2364 * [2.x] Restore `router.resolveComponent()` method by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2039 * Move `currentIsInitialPage` variable outside of `App` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2381 * Don't overwrite Vite class in Svelte playgrounds by [@jrmajor](https://github.com/jrmajor) in https://github.com/inertiajs/inertia/pull/2368 * Dependency update + Prevent Playwright 1.53.0 due to Svelte bug by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2382 * Update to Vite 6 by [@SuperDJ](https://github.com/SuperDJ) in https://github.com/inertiajs/inertia/pull/2315 * Fix React scroll restoration on popState by [@sebastiandedeyne](https://github.com/sebastiandedeyne) in https://github.com/inertiajs/inertia/pull/2357 * feat(useForm): export granular setData types and introduce SetDataAction by [@hasib-devs](https://github.com/hasib-devs) in https://github.com/inertiajs/inertia/pull/2356 * Refactor `mergeStrategies` to `matchOn` by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2384 * Remove `setSwapComponent` method and cleanup after PR #2379 by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2383 ### New Contributors * [@jordanhavard](https://github.com/jordanhavard) made their first contribution in https://github.com/inertiajs/inertia/pull/2328 * [@chack1172](https://github.com/chack1172) made their first contribution in https://github.com/inertiajs/inertia/pull/2379 * [@jrmajor](https://github.com/jrmajor) made their first contribution in https://github.com/inertiajs/inertia/pull/2368 * [@SuperDJ](https://github.com/SuperDJ) made their first contribution in https://github.com/inertiajs/inertia/pull/2315 * [@hasib-devs](https://github.com/hasib-devs) made their first contribution in https://github.com/inertiajs/inertia/pull/2356 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.12...v2.0.13 ## [v2.0.12](https://github.com/inertiajs/inertia/compare/v2.0.11...v2.0.12) - 2025-06-10 ### What's Changed * Send `Purpose: prefetch` header on prefetching by [@pascalbaljet](https://github.com/pascalbaljet) in https://github.com/inertiajs/inertia/pull/2367 ### New Contributors * [@pascalbaljet](https://github.com/pascalbaljet) made their first contribution in https://github.com/inertiajs/inertia/pull/2367 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.11...v2.0.12 ## [v2.0.11](https://github.com/inertiajs/inertia/compare/v2.0.10...v2.0.11) - 2025-05-16 ### What's Changed * Fix progress bar not showing on subsequent page clicks by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2349 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.10...v2.0.11 ## [v2.0.10](https://github.com/inertiajs/inertia/compare/v2.0.9...v2.0.10) - 2025-05-15 ### What's Changed * Don't show progress bar on prefetch hover by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2347 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.9...v2.0.10 ## [v2.0.9](https://github.com/inertiajs/inertia/compare/v2.0.8...v2.0.9) - 2025-05-09 ### What's Changed * Bump [@sveltejs](https://github.com/sveltejs)/kit from 2.11.1 to 2.20.6 by [@dependabot](https://github.com/dependabot) in https://github.com/inertiajs/inertia/pull/2312 * Bump vite from 5.4.17 to 5.4.18 by [@dependabot](https://github.com/dependabot) in https://github.com/inertiajs/inertia/pull/2307 * Fix for deferred props + prefetch links by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2321 * Progress: Make hide and reveal CSP-compatible by [@flexponsive](https://github.com/flexponsive) in https://github.com/inertiajs/inertia/pull/2316 * Corrected URL search parameter merge logic to match behavior prior to v2.0.8 by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2341 * Corrects url search parameter merge logic to match behavior prior to v2.0.8 by [@CTOJoe](https://github.com/CTOJoe) in https://github.com/inertiajs/inertia/pull/2320 * Bump vite from 5.4.18 to 5.4.19 by [@dependabot](https://github.com/dependabot) in https://github.com/inertiajs/inertia/pull/2334 * On back button, fetch from server if version hash is not current by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2342 * Allow custom URL protocols by [@mpociot](https://github.com/mpociot) in https://github.com/inertiajs/inertia/pull/2329 ### New Contributors * [@flexponsive](https://github.com/flexponsive) made their first contribution in https://github.com/inertiajs/inertia/pull/2316 * [@CTOJoe](https://github.com/CTOJoe) made their first contribution in https://github.com/inertiajs/inertia/pull/2320 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.8...v2.0.9 ## [v2.0.8](https://github.com/inertiajs/inertia/compare/v2.0.7...v2.0.8) - 2025-04-10 ### What's Changed * Add deepMerge Support for Merging Nested Arrays and Objects in Props by [@HichemTab-tech](https://github.com/HichemTab-tech) in https://github.com/inertiajs/inertia/pull/2069 * fix: build error because of invalid type definitions by [@fxnm](https://github.com/fxnm) in https://github.com/inertiajs/inertia/pull/2301 * fix(vue/useForm/defaults): untrack before assign by [@Dsaquel](https://github.com/Dsaquel) in https://github.com/inertiajs/inertia/pull/2112 * Improve type checking of request data by [@7nohe](https://github.com/7nohe) in https://github.com/inertiajs/inertia/pull/2304 * Remove empty payload from GET requests by [@edgars-vasiljevs](https://github.com/edgars-vasiljevs) in https://github.com/inertiajs/inertia/pull/2305 ### New Contributors * [@HichemTab-tech](https://github.com/HichemTab-tech) made their first contribution in https://github.com/inertiajs/inertia/pull/2069 * [@fxnm](https://github.com/fxnm) made their first contribution in https://github.com/inertiajs/inertia/pull/2301 * [@Dsaquel](https://github.com/Dsaquel) made their first contribution in https://github.com/inertiajs/inertia/pull/2112 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.7...v2.0.8 ## [v2.0.7](https://github.com/inertiajs/inertia/compare/v2.0.6...v2.0.7) - 2025-04-08 ### What's Changed * Added missing pages to React and Svelte5 playrounds by [@Verox001](https://github.com/Verox001) in https://github.com/inertiajs/inertia/pull/2217 * chore: replace lodash to decrease bundle size by [@lcdss](https://github.com/lcdss) in https://github.com/inertiajs/inertia/pull/2210 * do not pass url when storing scroll state to history by [@miDeb](https://github.com/miDeb) in https://github.com/inertiajs/inertia/pull/2280 * fix: react `Deferred` component error on partial visits by [@KaioFelps](https://github.com/KaioFelps) in https://github.com/inertiajs/inertia/pull/2223 * Bump vite from 5.4.16 to 5.4.17 by [@dependabot](https://github.com/dependabot) in https://github.com/inertiajs/inertia/pull/2295 * [2.x] SSR clustering by [@RobertBoes](https://github.com/RobertBoes) in https://github.com/inertiajs/inertia/pull/2206 * Allow Object type for href prop by [@nckrtl](https://github.com/nckrtl) in https://github.com/inertiajs/inertia/pull/2292 * Update GitHub Actions to Ubuntu 24.04 by [@joetannenbaum](https://github.com/joetannenbaum) in https://github.com/inertiajs/inertia/pull/2299 * [2.x]: Support for nested paths in forms by [@joaopalopes24](https://github.com/joaopalopes24) in https://github.com/inertiajs/inertia/pull/2181 ### New Contributors * [@Verox001](https://github.com/Verox001) made their first contribution in https://github.com/inertiajs/inertia/pull/2217 * [@lcdss](https://github.com/lcdss) made their first contribution in https://github.com/inertiajs/inertia/pull/2210 * [@miDeb](https://github.com/miDeb) made their first contribution in https://github.com/inertiajs/inertia/pull/2280 * [@KaioFelps](https://github.com/KaioFelps) made their first contribution in https://github.com/inertiajs/inertia/pull/2223 * [@nckrtl](https://github.com/nckrtl) made their first contribution in https://github.com/inertiajs/inertia/pull/2292 * [@joaopalopes24](https://github.com/joaopalopes24) made their first contribution in https://github.com/inertiajs/inertia/pull/2181 **Full Changelog**: https://github.com/inertiajs/inertia/compare/v2.0.6...v2.0.7 ## [v2.0.6](https://github.com/inertiajs/inertia/compare/v2.0.5...v2.0.6) - Deferred: More descriptive Deferred data prop error ([#2284](https://github.com/inertiajs/inertia/pull/2284)) - Bump vite from 5.4.12 to 5.4.16 ([#2288](https://github.com/inertiajs/inertia/pull/2288)) - Fix location return history decryption throwing error ([#2282](https://github.com/inertiajs/inertia/pull/2282)) - Make isDirty reactive to defaults ([#2236](https://github.com/inertiajs/inertia/pull/2236)) - Fix playground WhenVisible always ([#2203](https://github.com/inertiajs/inertia/pull/2203)) - Wayfinder support ([#2290](https://github.com/inertiajs/inertia/pull/2290)) ## [v2.0.5](https://github.com/inertiajs/inertia/compare/v2.0.4...v2.0.5) - Fix history state errors by nicholaspufal ([#2265](https://github.com/inertiajs/inertia/pull/2265)) - Bump axios from 1.7.9 to 1.8.2 ([#2269](https://github.com/inertiajs/inertia/pull/2269)) - Bump esbuild from 0.16.17 to 0.25.0 #2231 ([#2231](https://github.com/inertiajs/inertia/pull/2231)) - Bump vite from 5.4.11 to 5.4.12 ([#2201](https://github.com/inertiajs/inertia/pull/2201)) ## [v2.0.4](https://github.com/inertiajs/inertia/compare/v2.0.3...v2.0.4) - Fix anchor links on initial visits ([#2258](https://github.com/inertiajs/inertia/pull/2258)) ## [v2.0.3](https://github.com/inertiajs/inertia/compare/v2.0.2...v2.0.3) - Fix: Reload on mount ([#2200](https://github.com/inertiajs/inertia/pull/2200)) ## [v2.0.2](https://github.com/inertiajs/inertia/compare/v2.0.1...v2.0.2) - Fix SSR with scroll restoration ([#2190](https://github.com/inertiajs/inertia/pull/2190)) - Fix for scroll + back bug ([#2191](https://github.com/inertiajs/inertia/pull/2191)) - Backport 1.x fixes from [v1.3.0](https://github.com/inertiajs/inertia/releases/tag/v1.3.0) release ([#2193](https://github.com/inertiajs/inertia/pull/2193)) ## [v2.0.1](https://github.com/inertiajs/inertia/compare/v2.0.0...v2.0.1) - Fix playground dependencies ([#2070](https://github.com/inertiajs/inertia/pull/2070)) - Removed Vitest tests + dependencies ([#2175](https://github.com/inertiajs/inertia/pull/2175)) - Augment `vue` instead of `@vue/runtime-core` ([#2099](https://github.com/inertiajs/inertia/pull/2099)) - Fix prefetch missing `cacheFor` default value ([#2136](https://github.com/inertiajs/inertia/pull/2136)) - Fix `useForm` re-renders by memoizing functions in React [#2146](https://github.com/inertiajs/inertia/pull/2146) - WhenVisible useEffect function is not recreated when params change. ([#2153](https://github.com/inertiajs/inertia/pull/2153)) - Ensure callback execution ([#2163](https://github.com/inertiajs/inertia/pull/2163)) - More resilient logic for stripping the origin from page URLs ([#2164](https://github.com/inertiajs/inertia/pull/2164)) - Add helper scripts for running tests ([#2173](https://github.com/inertiajs/inertia/pull/2173)) - Export `InertiaFormProps` in React ([#2161](https://github.com/inertiajs/inertia/pull/2161)) - Use default empty object in `useForm` Vue and Svelte ([#2052](https://github.com/inertiajs/inertia/pull/2052)) - Remove `data` option from `useForm` options type ([#2060](https://github.com/inertiajs/inertia/pull/2060)) - Take over scroll restoration from browser ([#2051](https://github.com/inertiajs/inertia/pull/2051)) ## [v2.0.0](https://github.com/inertiajs/inertia/compare/v1.2.0...v2.0.0) ### Added - Add polling - Add link prefetching - Add deferred props - Add lazy loading of data when scrolling - Add history encryption API - Add React 19 support ([#2131](https://github.com/inertiajs/inertia/pull/2131)) - Add client side visits ([#2130](https://github.com/inertiajs/inertia/pull/2130)) ### Changed - Removal of NProgress dependency ([#2045](https://github.com/inertiajs/inertia/pull/2045)) - Change TypeScript module resolution in the Svelte adapter ([#2035](https://github.com/inertiajs/inertia/pull/2035)) - Refactor `createInertiaApp` in Svelte adapter ([#2036](https://github.com/inertiajs/inertia/pull/2036)) ### Fixed - Fix: make Link href prop reactive ([#2089](https://github.com/inertiajs/inertia/pull/2089)) ## [v1.3.0](https://github.com/inertiajs/inertia/compare/v1.2.0...v1.3.0) ### Added - Add React 19 support ([#2121](https://github.com/inertiajs/inertia/pull/2121)) - Add Svelte 5 support ([#1970](https://github.com/inertiajs/inertia/pull/1970)) - Add TypeScript support to Svelte adapter ([#1866](https://github.com/inertiajs/inertia/pull/1866), [69292e](https://github.com/inertiajs/inertia/commit/69292ef3592ccca5e0f05f7ce131a53f6c1ba22b), [#2003](https://github.com/inertiajs/inertia/pull/2003), [#2005](https://github.com/inertiajs/inertia/pull/2005)) ### Changed - Skip intercepting non-left button clicks on links ([#1908](https://github.com/inertiajs/inertia/pull/1908), [#1910](https://github.com/inertiajs/inertia/pull/1910)) - Changed `preserveScroll` to be `true` on initial page visit ([#1360](https://github.com/inertiajs/inertia/pull/1360)) - Return early when using `router.on()` during SSR ([#1715](https://github.com/inertiajs/inertia/pull/1715)) - Use updater function in `setData` in `useForm` hook in React adapter ([#1859](https://github.com/inertiajs/inertia/pull/1859)) ### Fixed - Fix history navigation issue on Chrome iOS ([#1984](https://github.com/inertiajs/inertia/pull/1984), [#1992](https://github.com/inertiajs/inertia/pull/1992)) - Fix `setNavigationType` for Safari 10 ([#1957](https://github.com/inertiajs/inertia/pull/1957)) - Export `InertiaFormProps` in all adapters ([#1596](https://github.com/inertiajs/inertia/pull/1596), [#1734](https://github.com/inertiajs/inertia/pull/1734)) - Fix `isDirty` after `form.defaults()` call in Vue 3 ([#1985](https://github.com/inertiajs/inertia/pull/1985)) - Fix scroll reset on page navigation ([#1980](https://github.com/inertiajs/inertia/pull/1980)) - Fix scroll position restoration for `[scroll-region]` elements ([#1782](https://github.com/inertiajs/inertia/pull/1782), [#1980](https://github.com/inertiajs/inertia/pull/1980)) - Fix `useForm` re-renders by memoizing functions in React adapter ([#1607](https://github.com/inertiajs/inertia/pull/1607)) - Fix doubling hash when using `` ([#1728](https://github.com/inertiajs/inertia/pull/1728)) - Fix type augmentation in Vue 3 adapter ([#1958](https://github.com/inertiajs/inertia/pull/1958)) - Fix form helper `transform` return type in React adapter ([#1896](https://github.com/inertiajs/inertia/pull/1896)) - Fix props reactivity in Svelte adapter ([#1969](https://github.com/inertiajs/inertia/pull/1969)) - Fix `` component to respect `preserveState` option in Svelte adapter ([#1943](https://github.com/inertiajs/inertia/pull/1943)) - Fix 'received an unexpected slot "default"' warning in Svelte adapter ([#1941](https://github.com/inertiajs/inertia/pull/1941)) - Fix command + click behavior on links in React adapter ([#2132](https://github.com/inertiajs/inertia/pull/2132)) - Fix import in Svelte adapter ([#2002](https://github.com/inertiajs/inertia/pull/2002)) ## [v1.2.0](https://github.com/inertiajs/inertia/compare/v1.1.0...v1.2.0) - Fix `preserveScroll` and `preserveState` types ([#1882](https://github.com/inertiajs/inertia/pull/1882)) - Revert "merge props from partial reloads" ([#1895](https://github.com/inertiajs/inertia/pull/1895)) ## [v1.1.0](https://github.com/inertiajs/inertia/compare/v1.0.16...v1.1.0) - Add new `except` visit option to exclude props from partial reloads ([#1876](https://github.com/inertiajs/inertia/pull/1876)) - Deep merge props from partial reloads ([#1877](https://github.com/inertiajs/inertia/pull/1877)) ## [v1.0.16](https://github.com/inertiajs/inertia/compare/v1.0.15...v1.0.16) - Fix Svelte 4 slot rendering issues ([#1763](https://github.com/inertiajs/inertia/pull/1763)) - Fix accessibility warning in Svelte `Link` component ([#1858](https://github.com/inertiajs/inertia/pull/1858)) - Use `Omit` instead of `Exclude` in router types ([#1857](https://github.com/inertiajs/inertia/pull/1857)) ## [v1.0.15](https://github.com/inertiajs/inertia/compare/v1.0.14...v1.0.15) - Bump axios from `v1.4.0` to `v1.6.0` ([#1723](https://github.com/inertiajs/inertia/pull/1723)) ## [v1.0.14](https://github.com/inertiajs/inertia/compare/v1.0.13...v1.0.14) - Revert "Clear errors on form reset (#1568)" ([#1716](https://github.com/inertiajs/inertia/pull/1716)) ## [v1.0.13](https://github.com/inertiajs/inertia/compare/v1.0.12...v1.0.13) - Clear errors on form reset ([#1568](https://github.com/inertiajs/inertia/pull/1568)) - Fix `Link` type in React ([#1659](https://github.com/inertiajs/inertia/pull/1659)) ## [v1.0.12](https://github.com/inertiajs/inertia/compare/v1.0.11...v1.0.12) - Fix type of `onClick` for `Link` component in React and Vue ([#1699](https://github.com/inertiajs/inertia/pull/1699), [#1701](https://github.com/inertiajs/inertia/pull/1701)) ## [v1.0.11](https://github.com/inertiajs/inertia/compare/v1.0.10...v1.0.11) - Fix form helper types for `setDefaults()` method (React) and `defaults()` method (Vue) ([#1504](https://github.com/inertiajs/inertia/pull/1504)) - Fix interface issue with `useForm()` in React and Vue adapters ([#1649](https://github.com/inertiajs/inertia/pull/1649)) ## [v1.0.10](https://github.com/inertiajs/inertia/compare/v1.0.9...v1.0.10) - Fix Svelte's `useForm` helper ([#1610](https://github.com/inertiajs/inertia/pull/1610)) ## [v1.0.9](https://github.com/inertiajs/inertia/compare/v1.0.8...v1.0.9) - Fix `` vNode handling in Vue 3 adapter ([#1590](https://github.com/inertiajs/inertia/pull/1590)) - Add Svelte 4 support ([60699c7](https://github.com/inertiajs/inertia/commit/60699c7c5978eebd393e0333b567d8e465f4b58f)) ## [v1.0.8](https://github.com/inertiajs/inertia/compare/v1.0.7...v1.0.8) ### Fixed - Fix `` vNode handling in Vue 3 adapter ([#1570](https://github.com/inertiajs/inertia/pull/1570)) - Fix watching remembered data in Vue 3 adapter ([#1571](https://github.com/inertiajs/inertia/pull/1571)) ## [v1.0.7](https://github.com/inertiajs/inertia/compare/v1.0.6...v1.0.7) ### Fixed - Fix `` fragment detection in Vue 3 adapter ([#1509](https://github.com/inertiajs/inertia/pull/1509)) ## [v1.0.6](https://github.com/inertiajs/inertia/compare/v1.0.5...v1.0.6) ### Fixed - Fix `usePage()` null object error in Vue 3 adapter ([#1530](https://github.com/inertiajs/inertia/pull/1530)) ## [v1.0.5](https://github.com/inertiajs/inertia/compare/v1.0.4...v1.0.5) ### Fixed - Fix `usePage()` reactivity in Vue 2 adapter ([#1527](https://github.com/inertiajs/inertia/pull/1527)) ### Changed - Simplify the Vue 2 form helper ([#1529](https://github.com/inertiajs/inertia/pull/1529)) ## [v1.0.4](https://github.com/inertiajs/inertia/compare/v1.0.3...v1.0.4) ### Added - Added `displayName` to `Link` component in React adapter ([#1512](https://github.com/inertiajs/inertia/pull/1512)) ### Fixed - Fix `usePage()` reactivity in Vue 3 adapter ([#1469](https://github.com/inertiajs/inertia/pull/1469)) ## [v1.0.3](https://github.com/inertiajs/inertia/compare/v1.0.2...v1.0.3) ### Added - Added initialization callback to form helper in Vue adapters ([#1516](https://github.com/inertiajs/inertia/pull/1516)) ## [v1.0.2](https://github.com/inertiajs/inertia/compare/v1.0.1...v1.0.2) ### Fixed - Added explicit children to `InertiaHeadProps` ([#1448](https://github.com/inertiajs/inertia/pull/1448)) - Exported `InertiaLinkProps` type ([#1450](https://github.com/inertiajs/inertia/pull/1450)) - Improved React `usePage` generic type ([#1451](https://github.com/inertiajs/inertia/pull/1451)) ## [v1.0.1](https://github.com/inertiajs/inertia/compare/v1.0.0...v1.0.1) ### Fixed - Fixed Vue type overrides for `$page` and `$inertia` ([#1393](https://github.com/inertiajs/inertia/pull/1393)) - Restored React `usePage` generic type ([#1396](https://github.com/inertiajs/inertia/pull/1396)) - Prevented need to use `Method` enum with the Link component ([#1392](https://github.com/inertiajs/inertia/pull/1392)) - Restored Vue 3 `usePage` generic type ([#1394](https://github.com/inertiajs/inertia/pull/1394)) - Fixed export of server types ([#1397](https://github.com/inertiajs/inertia/pull/1397)) - Updated form types to support nested data ([#1401](https://github.com/inertiajs/inertia/pull/1401)) - Allowed stronger type support with Vue `useForm` ([#1413](https://github.com/inertiajs/inertia/pull/1413)) - Fixed Vue 2 `setup` prop types ([#1418](https://github.com/inertiajs/inertia/pull/1418)) - Fixed issue when passing multiple children to React `Head` component ([#1433](https://github.com/inertiajs/inertia/pull/1433)) ## [v1.0.0](https://github.com/inertiajs/inertia/compare/7ce91ec...v1.0.0) - 2023-01-14 ### Added - Added SSR support to Svelte library ([#1349](https://github.com/inertiajs/inertia/pull/1349)) - Added first-class TypeScript support to React adapter - Added first-class TypeScript support to Vue 2 adapter - Added first-class TypeScript support to Vue 3 adapter - Added new `useForm()` hook to Vue 2 adapter ([ff59196](https://github.com/inertiajs/inertia/commit/ff59196)) ### Changed - Renamed `@inertiajs/inertia` library to `@inertiajs/core` ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Renamed `@inertiajs/inertia-react` library to `@inertiajs/react` ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Renamed `@inertiajs/inertia-svelte` library to `@inertiajs/svelte` ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Renamed `@inertiajs/inertia-vue` library to `@inertiajs/vue2` ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Renamed `@inertiajs/inertia-vue3` library to `@inertiajs/vue3` ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Merged progress library to core and deprecated `@inertiajs/progress` library ([#1282](https://github.com/inertiajs/inertia/pull/1282), [0b5f773](https://github.com/inertiajs/inertia/commit/0b5f773)) - Merged server library to core and deprecated `@inertiajs/server` library ([#1282](https://github.com/inertiajs/inertia/pull/1282)) - Renamed `Inertia` named export to `router` ([#1282](https://github.com/inertiajs/inertia/pull/1282), [e556703](https://github.com/inertiajs/inertia/commit/e556703)) - Removed deprecated named exports ([#1282](https://github.com/inertiajs/inertia/pull/1282), [e556703](https://github.com/inertiajs/inertia/commit/e556703)) - Removed deprecated `app` argument from `createInertiaApp()` in Vue adapters ([#1282](https://github.com/inertiajs/inertia/pull/1282), [65f8a5f](https://github.com/inertiajs/inertia/commit/65f8a5f)) - Updated axios to 1.x ([#1377](https://github.com/inertiajs/inertia/pull/1377)) - Simplified `usePage()` hook in Vue 3 adapter ([#1373](https://github.com/inertiajs/inertia/pull/1373)) - Improved Svelte `use:inertia` and `` component ([#1344](https://github.com/inertiajs/inertia/pull/1344)) - Removed global `visitOptions()` hook ([#1282](https://github.com/inertiajs/inertia/pull/1282), [30908c2](https://github.com/inertiajs/inertia/commit/30908c2)) - Switched bundler from Microbundle to ESbuild ([f711b46](https://github.com/inertiajs/inertia/commit/f711b46), [8093713](https://github.com/inertiajs/inertia/commit/8093713), [342312d](https://github.com/inertiajs/inertia/commit/342312d), [c9e12b3](https://github.com/inertiajs/inertia/commit/c9e12b3)) ### Fixed - Fixed `` tag not always being included when a `title` callback is defined in `createInertiaApp()` ([#1055](https://github.com/inertiajs/inertia/pull/1055)) - Fixed types to include `undefined` as a valid `FormDataConvertable` option ([#1165](https://github.com/inertiajs/inertia/pull/1165)) - Fixed issue where remembered state wasn't clear on a full page reload ([769f643](https://github.com/inertiajs/inertia/commit/769f643)) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thank you for your interest in contributing to Inertia.js! Your contributions help make this project better for everyone. Inertia.js is maintained as a monorepo using [pnpm workspaces](https://pnpm.io/workspaces). Below you'll find an overview of the repository and how to get your development environment running. > **Note:** You'll need **pnpm version 10 or higher**. If you're unsure which version you have, run `pnpm -v`. ## Repository Overview ``` inertia/ ├── packages/ Core libraries and framework adapters │ ├── core/ Framework-agnostic core library │ ├── react/ React adapter │ │ └── test-app/ React test application │ ├── svelte/ Svelte adapter │ │ └── test-app/ Svelte test application │ └── vue3/ Vue 3 adapter │ └── test-app/ Vue 3 test application ├── playgrounds/ Full Laravel applications for manual testing │ ├── react/ Laravel + React │ ├── svelte4/ Laravel + Svelte 4 │ ├── svelte5/ Laravel + Svelte 5 │ └── vue3/ Laravel + Vue 3 └── tests/ End-to-end tests and test server ├── app/ Shared Node.js backend └── *.spec.ts Playwright test suite ``` ### Key Components - **Core Library:** The framework-agnostic engine powering all adapters (`packages/core`). - **Adapters:** Framework-specific integrations for React, Svelte, and Vue. - **Test Applications:** Minimal frontend apps used for automated testing (`packages/*/test-app/`). - **Playwright Tests:** Framework-agnostic end-to-end tests that verify behavior across adapters (`tests/*.spec.ts`). - **Playgrounds:** Full Laravel applications for manual testing (`playgrounds/`). These are optional and may eventually be removed. ## Getting Started Clone the repository and install the dependencies: ```sh git clone https://github.com/inertiajs/inertia.git inertia cd inertia pnpm install ``` Then, start the development environment: ```sh pnpm dev ``` This builds the core library and all adapters, and starts a file watcher that will automatically rebuild each package when changes are made. If you prefer, you can also start individual watchers from each package directory. For example: ```sh cd packages/core && pnpm dev cd packages/react && pnpm dev ``` > **Note:** The core package (`packages/core`) must always be running, as all adapters depend on it. ## Running Tests Inertia.js uses Playwright to run a shared end-to-end test suite against each adapter. This is how we verify that Inertia behaves the same across React, Svelte, and Vue. Run the test suite for a specific adapter: ```sh pnpm test:react pnpm test:svelte pnpm test:vue ``` These commands automatically set a `PACKAGE` environment variable that tells the Node.js test server which adapter to serve. For example, when running `pnpm test:react`, the test server loads the React test application. If you want to run Playwright directly, you can pass the environment variable yourself: ```sh PACKAGE=react playwright test ``` You may filter tests by name: ```sh pnpm test:react -g "partial reload" ``` Run tests in headed mode (to see the browser): ```sh pnpm test:vue --headed ``` Or in debug mode: ```sh pnpm test:vue --debug ``` ### How the Test Setup Works All adapters use the same Node.js backend and Playwright test suite. The only difference is which adapter's test app is served. ``` tests/app/server.js Shared Node.js backend ├── serves: react test app (when PACKAGE=react) ├── serves: svelte test app (when PACKAGE=svelte) └── serves: vue test app (when PACKAGE=vue3) tests/*.spec.ts Shared Playwright test suite ``` When running a test command, the correct adapter is selected automatically: | Adapter | `PACKAGE` value | Test server port | App URL | | ------- | --------------- | ---------------- | -------------------------------------------------- | | React | `react` | 13716 | [http://localhost:13716/](http://localhost:13716/) | | Svelte | `svelte` | 13717 | [http://localhost:13717/](http://localhost:13717/) | | Vue 3 | `vue3` | 13715 | [http://localhost:13715/](http://localhost:13715/) | ### Automatic Test Server Boot You do not need to start the test server manually. When you run a test, Playwright automatically builds the frontend for the selected adapter and boots the Node.js test server before running the tests. This is configured in the Playwright config (`playwright.config.ts`) using the [`webServer`](https://playwright.dev/docs/test-configuration#webserver) option. If a server is already running (for example, during local development), Playwright will reuse it. ## Running Test Applications The test applications are the primary development environments for Inertia.js. These minimal apps cover all supported features and are used for both manual development and automated end-to-end testing. Run all test apps at once: ```sh pnpm dev:test-app ``` Or start an individual one: ```sh pnpm dev:test-app:react pnpm dev:test-app:svelte pnpm dev:test-app:vue ``` Each test app runs two servers: - A Node.js backend that automatically restarts when changed - A Vite development server for the frontend If you are developing a new feature or fixing a bug, you can use these test apps to develop and test your changes. ## Adding Tests If you are fixing a bug, adding a feature, or improving existing functionality, please verify that your changes work across all adapters, not just one. ### 1. Add Frontend Pages Create the same frontend page in each test application: ``` packages/react/test-app/Pages/YourFeature.jsx packages/svelte/test-app/Pages/YourFeature.svelte packages/vue3/test-app/Pages/YourFeature.vue ``` Each page should provide the same behavior and functionality. ### 2. Add Backend Routes (If Needed) If your change requires a backend route, add it to the shared Node.js test server: ```javascript // tests/app/server.js app.get('/your-feature', (req, res) => inertia.render(req, res, { component: 'YourFeature', props: { foo: 'bar' }, }), ) ``` ### 3. Write a Playwright Test Add a new Playwright test to verify your change. Playwright allows us to test features across all adapters without duplicating test logic. ```typescript // tests/your-feature.spec.ts import { test, expect } from '@playwright/test' test('your feature works', async ({ page }) => { await page.goto('/your-feature') // Your assertions here }) ``` ### 4. Run the Tests in All Adapters Be sure to run your test for each adapter: ```sh pnpm test:react -g "your feature" pnpm test:svelte -g "your feature" pnpm test:vue -g "your feature" ``` Your work is not considered complete until it works consistently across all frameworks. ## Using the Playgrounds (Optional) The repository also includes several full Laravel applications that integrate Inertia.js. These are optional and mostly useful for manually exploring how Inertia works inside a real Laravel app. The playgrounds are provided as-is and are not part of the automated test setup. They may be removed in the future. ### Getting Started To start a playground, simply run: ```sh pnpm playground:react ``` The playground script will automatically handle initial setup if needed: - Installing PHP dependencies via Composer - Installing Node.js dependencies via pnpm - Creating the `.env` file from `.env.example` - Generating the application key - Setting up the SQLite database - Running migrations with seed data Visit the application at [http://127.0.0.1:8000](http://127.0.0.1:8000). Each playground has its own pnpm script: ```sh pnpm playground:react pnpm playground:svelte4 pnpm playground:svelte5 pnpm playground:vue ``` ## Publishing (Maintainers Only) Releasing is handled by the included release script. You'll need both the `git` CLI and the GitHub CLI ([`gh`](https://cli.github.com)) installed. To create a new release: ```sh ./release.sh ``` The script will: - Ensure you're on the master branch with a clean working tree - Prompt you to select the type of version bump (patch, minor, or major) - Update all package versions automatically - Update the lockfile - Create a git commit and tag - Push changes and tags to GitHub - Create a GitHub release with auto-generated notes - Trigger the CI publishing workflow Publishing is handled securely using GitHub + npm [trusted publishing](https://docs.npmjs.com/trusted-publishers). ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) Jonathan Reinink <jonathan@reinink.ca> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Inertia.js](https://raw.githubusercontent.com/inertiajs/inertia/master/.github/LOGO.png)](https://inertiajs.com/) Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers. Find full documentation at [inertiajs.com](https://inertiajs.com/). ## Contributing Thank you for considering contributing to Inertia! You can read the contribution guide [here](CONTRIBUTING.md). ## Code of Conduct In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). ## Security Vulnerabilities Please review [our security policy](https://github.com/inertiajs/inertia/security/policy) on how to report security vulnerabilities. ## License Inertia is open-sourced software licensed under the [MIT license](LICENSE.md). ================================================ FILE: package.json ================================================ { "name": "inertia", "private": true, "type": "module", "scripts": { "build:all": "pnpm -r --filter './packages/*' build", "dev": "pnpx concurrently -c \"#b794f4,#61dafb,#d43008,#32a06f\" \"pnpm dev:core\" \"pnpm dev:react\" \"pnpm dev:svelte\" \"pnpm dev:vue\" --names=core,react,svelte,vue", "dev:core": "cd packages/core && pnpm run dev", "dev:vue": "cd packages/vue3 && pnpm run dev", "dev:react": "cd packages/react && pnpm run dev", "dev:svelte": "cd packages/svelte && pnpm run dev", "dev:test-app": "pnpx concurrently -c \"#61dafb,#d43008,#32a06f\" \"pnpm dev:test-app:react\" \"pnpm dev:test-app:svelte\" \"pnpm dev:test-app:vue\" --names=react,svelte,vue", "dev:test-app:react": "pnpx concurrently -c \"#c4b5fd,#ffa800\" \"cd tests/app && PACKAGE=react pnpm serve:watch\" \"cd packages/react/test-app && pnpm run dev\" --names=server,vite", "dev:test-app:svelte": "pnpx concurrently -c \"#c4b5fd,#ffa800\" \"cd tests/app && PACKAGE=svelte pnpm serve:watch\" \"cd packages/svelte/test-app && pnpm run dev\" --names=server,vite", "dev:test-app:vue": "pnpx concurrently -c \"#c4b5fd,#ffa800\" \"cd tests/app && PACKAGE=vue3 pnpm serve:watch\" \"cd packages/vue3/test-app && pnpm run dev\" --names=server,vite", "es2020-check": "pnpm -r --filter './packages/*' es2020-check", "lint:test-app": "pnpm -r --filter './packages/*/test-app' lint", "lint:test-app:react": "cd packages/react/test-app && pnpm run lint", "lint:test-app:svelte": "cd packages/svelte/test-app && pnpm run lint", "lint:test-app:vue": "cd packages/vue3/test-app && pnpm run lint", "type-check:test-app": "pnpm -r --filter './packages/*/test-app' type-check", "type-check:test-app:react": "cd packages/react/test-app && pnpm run type-check", "type-check:test-app:svelte": "cd packages/svelte/test-app && pnpm run type-check", "type-check:test-app:vue": "cd packages/vue3/test-app && pnpm run type-check", "test:react": "PACKAGE=react node playwright.js", "test:svelte": "PACKAGE=svelte node playwright.js", "test:vue": "PACKAGE=vue3 node playwright.js", "test:ssr:react": "PACKAGE=react SSR=true npx playwright test", "test:ssr:svelte": "PACKAGE=svelte SSR=true npx playwright test", "test:ssr:vue": "PACKAGE=vue3 SSR=true npx playwright test", "playground:react": "cd playgrounds/react && ./init.sh && composer run dev", "playground:svelte4": "cd playgrounds/svelte4 && ./init.sh && composer run dev", "playground:svelte5": "cd playgrounds/svelte5 && ./init.sh && composer run dev", "playground:vue": "cd playgrounds/vue3 && ./init.sh && composer run dev", "format": "prettier --write ." }, "dependencies": { "@playwright/test": "^1.58.2", "prettier": "^3.8.1", "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-svelte": "^3.5.0", "prettier-plugin-tailwindcss": "^0.7.2" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.59.0" }, "pnpm": { "overrides": { "cookie": ">=0.7.0", "esbuild": ">=0.25.0" } } } ================================================ FILE: packages/core/.gitignore ================================================ dist types node_modules package-lock.json yarn.lock ================================================ FILE: packages/core/LICENSE ================================================ MIT License Copyright (c) Jonathan Reinink <jonathan@reinink.ca> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/core/build.js ================================================ #!/usr/bin/env node import esbuild from 'esbuild' import { nodeExternalsPlugin } from 'esbuild-node-externals' const watch = process.argv.slice(1).includes('--watch') const withDeps = process.argv.slice(1).includes('--with-deps') const config = { bundle: true, minify: false, sourcemap: withDeps ? false : true, target: 'es2020', plugins: [ ...(withDeps ? [] : [nodeExternalsPlugin()]), { name: 'inertia', setup(build) { let count = 0 build.onEnd((result) => { if (count++ !== 0) { console.log(`Rebuilding ${build.initialOptions.entryPoints} (${build.initialOptions.format})…`) } }) }, }, ], } const builds = [ { entryPoints: ['src/index.ts'], format: 'esm', outfile: 'dist/index.esm.js', platform: 'browser' }, { entryPoints: ['src/index.ts'], format: 'cjs', outfile: 'dist/index.js', platform: 'browser' }, { entryPoints: ['src/server.ts'], format: 'esm', outfile: 'dist/server.esm.js', platform: 'node' }, { entryPoints: ['src/server.ts'], format: 'cjs', outfile: 'dist/server.js', platform: 'node' }, ] builds.forEach(async (build) => { const context = await esbuild.context({ ...config, ...build }) if (watch) { console.log(`Watching ${build.entryPoints} (${build.format})…`) await context.watch() } else { await context.rebuild() context.dispose() console.log(`Built ${build.entryPoints} (${build.format}) ${withDeps ? '(with-deps)' : ''}…`) } }) ================================================ FILE: packages/core/package.json ================================================ { "name": "@inertiajs/core", "version": "2.3.18", "license": "MIT", "description": "A framework for creating server-driven single page apps.", "contributors": [ "Jonathan Reinink <jonathan@reinink.ca>", "Claudio Dekker <claudio@ubient.net>", "Sebastian De Deyne <sebastiandedeyne@gmail.com>" ], "homepage": "https://inertiajs.com/", "repository": { "type": "git", "url": "https://github.com/inertiajs/inertia.git", "directory": "packages/inertia" }, "bugs": { "url": "https://github.com/inertiajs/inertia/issues" }, "files": [ "dist", "types" ], "type": "module", "main": "dist/index.js", "types": "types/index.d.ts", "exports": { ".": { "types": "./types/index.d.ts", "import": "./dist/index.esm.js", "require": "./dist/index.js" }, "./server": { "types": "./types/server.d.ts", "import": "./dist/server.esm.js", "require": "./dist/server.js" } }, "typesVersions": { "*": { "server": [ "types/server.d.ts" ] } }, "scripts": { "build": "pnpm clean && ./build.js && tsc", "build:with-deps": "./build.js --with-deps", "clean": "rm -rf types && rm -rf dist", "dev": "pnpx concurrently -c \"#ffcf00,#3178c6\" \"pnpm dev:build\" \"pnpm dev:types\" --names build,types", "dev:build": "./build.js --watch", "dev:types": "tsc --watch --preserveWatchOutput", "es2020-check": "pnpm build:with-deps && es-check es2020 \"dist/index.esm.js\" --checkFeatures --module --noCache --verbose" }, "dependencies": { "@types/lodash-es": "^4.17.12", "axios": "^1.13.5", "laravel-precognition": "^1.0.2", "lodash-es": "^4.17.23", "qs": "^6.15.0" }, "devDependencies": { "@types/node": "^18.19.130", "@types/qs": "^6.14.0", "es-check": "^9.6.1", "esbuild": "^0.27.3", "esbuild-node-externals": "^1.20.1", "typescript": "^5.9.3" } } ================================================ FILE: packages/core/readme.md ================================================ # Inertia.js Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers. Visit [inertiajs.com](https://inertiajs.com/) to learn more. ================================================ FILE: packages/core/src/config.ts ================================================ import { get, has, set } from 'lodash-es' import { InertiaAppConfig } from './types' // Generate all possible nested paths type ConfigKeys<T> = T extends Function ? never : string extends keyof T ? string : | Extract<keyof T, string> | { [Key in Extract<keyof T, string>]: T[Key] extends object ? `${Key}.${ConfigKeys<T[Key]> & string}` : never }[Extract<keyof T, string>] // Extract the value type at a given path type ConfigValue<T, K extends ConfigKeys<T>> = K extends `${infer P}.${infer Rest}` ? P extends keyof T ? Rest extends ConfigKeys<T[P]> ? ConfigValue<T[P], Rest> : never : never : K extends keyof T ? T[K] : never // Helper type for setting multiple config values with an object type ConfigSetObject<T> = { [K in ConfigKeys<T>]?: ConfigValue<T, K> } type FirstLevelOptional<T> = { [K in keyof T]?: T[K] extends object ? { [P in keyof T[K]]?: T[K][P] } : T[K] } export class Config<TConfig extends {} = {}> { protected config: FirstLevelOptional<TConfig> = {} protected defaults: TConfig public constructor(defaults: TConfig) { this.defaults = defaults } public extend<TExtension extends {}>(defaults?: TExtension): Config<TConfig & TExtension> { if (defaults) { this.defaults = { ...this.defaults, ...defaults } as TConfig & TExtension } return this as unknown as Config<TConfig & TExtension> } public replace(newConfig: FirstLevelOptional<TConfig>): void { this.config = newConfig } public get<K extends ConfigKeys<TConfig>>(key: K): ConfigValue<TConfig, K> { return (has(this.config, key) ? get(this.config, key) : get(this.defaults, key)) as ConfigValue<TConfig, K> } public set<K extends ConfigKeys<TConfig>>( keyOrValues: K | Partial<ConfigSetObject<TConfig>>, value?: ConfigValue<TConfig, K>, ): void { if (typeof keyOrValues === 'string') { set(this.config, keyOrValues, value) } else { Object.entries(keyOrValues).forEach(([key, val]) => { set(this.config, key, val) }) } } } export const config = new Config<InertiaAppConfig>({ form: { recentlySuccessfulDuration: 2_000, forceIndicesArrayFormatInFormData: true, withAllErrors: false, }, future: { preserveEqualProps: false, useDataInertiaHeadAttribute: false, useDialogForErrorModal: false, useScriptElementForInitialPage: false, }, prefetch: { cacheFor: 30_000, hoverDelay: 75, }, }) ================================================ FILE: packages/core/src/debounce.ts ================================================ export default function debounce<F extends (...params: any[]) => ReturnType<F>>(fn: F, delay: number): F { let timeoutID: NodeJS.Timeout return function (...args: unknown[]) { clearTimeout(timeoutID) timeoutID = setTimeout(() => fn.apply(this, args), delay) } as F } ================================================ FILE: packages/core/src/debug.ts ================================================ export const stackTrace = (autolog = true) => { try { throw new Error() } catch (e) { const stack = (e as Error).stack if (!autolog) { return stack } console.log(stack) } } ================================================ FILE: packages/core/src/dialog.ts ================================================ import modal from './modal' export default { show(html: Record<string, unknown> | string): void { const { iframe, page } = modal.createIframeAndPage(html) iframe.style.boxSizing = 'border-box' iframe.style.display = 'block' const dialog = document.createElement('dialog') dialog.id = 'inertia-error-dialog' // Style the dialog to mimic 50px padding Object.assign(dialog.style, { width: 'calc(100vw - 100px)', height: 'calc(100vh - 100px)', padding: '0', margin: 'auto', border: 'none', backgroundColor: 'transparent', }) // There's no way to directly style the backdrop of a dialog, so we need to use a style element... const dialogStyleElement = document.createElement('style') dialogStyleElement.textContent = ` dialog#inertia-error-dialog::backdrop { background-color: rgba(0, 0, 0, 0.6); } dialog#inertia-error-dialog:focus { outline: none; } ` document.head.appendChild(dialogStyleElement) dialog.addEventListener('click', (event: MouseEvent) => { if (event.target === dialog) { dialog.close() } }) dialog.addEventListener('close', () => { dialogStyleElement.remove() dialog.remove() }) dialog.appendChild(iframe) document.body.prepend(dialog) dialog.showModal() // Focus the dialog so the 'Escape' key works immediately dialog.focus() if (!iframe.contentWindow) { throw new Error('iframe not yet ready.') } iframe.contentWindow.document.open() iframe.contentWindow.document.write(page.outerHTML) iframe.contentWindow.document.close() }, } ================================================ FILE: packages/core/src/domUtils.ts ================================================ const elementInViewport = (el: HTMLElement) => { if (el.offsetParent === null) { // Element is not participating in layout (e.g., display: none) return false } const rect = el.getBoundingClientRect() // We check both vertically and horizontally for containers that scroll in either direction const verticallyVisible = rect.top < window.innerHeight && rect.bottom >= 0 const horizontallyVisible = rect.left < window.innerWidth && rect.right >= 0 return verticallyVisible && horizontallyVisible } export const getScrollableParent = (element: HTMLElement | null): HTMLElement | null => { const allowsVerticalScroll = (el: HTMLElement): boolean => { const computedStyle = window.getComputedStyle(el) if (['scroll', 'overlay'].includes(computedStyle.overflowY)) { return true } if (computedStyle.overflowY !== 'auto') { return false } if (['visible', 'clip'].includes(computedStyle.overflowX)) { return true } return hasDimensionConstraint(computedStyle.maxHeight, el.style.height) || isConstrainedByLayout(el, 'height') } const allowsHorizontalScroll = (el: HTMLElement): boolean => { const computedStyle = window.getComputedStyle(el) if (['scroll', 'overlay'].includes(computedStyle.overflowX)) { return true } if (computedStyle.overflowX !== 'auto') { return false } if (['visible', 'clip'].includes(computedStyle.overflowY)) { return true } return hasDimensionConstraint(computedStyle.maxWidth, el.style.width) || isConstrainedByLayout(el, 'width') } const hasDimensionConstraint = (computedMaxDimension: string, inlineStyleDimension: string): boolean => { if (computedMaxDimension && computedMaxDimension !== 'none' && computedMaxDimension !== '0px') { return true } if (inlineStyleDimension && inlineStyleDimension !== 'auto' && inlineStyleDimension !== '0') { return true } return false } // When overflow is set to 'auto' on one axis, the browser implicitly sets the other axis // to 'auto' as well (CSS spec), which causes the 'visible'/'clip' checks above to fail. // In flex/grid layouts, the element's size may be constrained by the parent layout rather // than explicit dimension properties, so we check for that here. const isConstrainedByLayout = (el: HTMLElement, dimension: 'height' | 'width'): boolean => { const parent = el.parentElement if (!parent) { return false } const parentStyle = window.getComputedStyle(parent) if (['flex', 'inline-flex'].includes(parentStyle.display)) { const isColumnLayout = ['column', 'column-reverse'].includes(parentStyle.flexDirection) return dimension === 'height' ? isColumnLayout : !isColumnLayout } return ['grid', 'inline-grid'].includes(parentStyle.display) } let parent = element?.parentElement while (parent) { const allowsScroll = allowsVerticalScroll(parent) || allowsHorizontalScroll(parent) if (window.getComputedStyle(parent).display !== 'contents' && allowsScroll) { return parent } parent = parent.parentElement } return null } export const getElementsInViewportFromCollection = ( elements: HTMLElement[], referenceElement?: HTMLElement, ): HTMLElement[] => { if (!referenceElement) { return elements.filter((element) => elementInViewport(element)) } const referenceIndex = elements.indexOf(referenceElement) const upwardElements: HTMLElement[] = [] const downwardElements: HTMLElement[] = [] // Traverse upwards until an element is not visible for (let i = referenceIndex; i >= 0; i--) { const element = elements[i] if (elementInViewport(element)) { upwardElements.push(element) } else { break } } // Traverse downwards until an element is not visible for (let i = referenceIndex + 1; i < elements.length; i++) { const element = elements[i] if (elementInViewport(element)) { downwardElements.push(element) } else { break } } // Reverse upward elements to maintain DOM order, then append downward elements return [...upwardElements.reverse(), ...downwardElements] } export const requestAnimationFrame = (cb: () => void, times: number = 1): void => { window.requestAnimationFrame(() => { if (times > 1) { requestAnimationFrame(cb, times - 1) } else { cb() } }) } export const getInitialPageFromDOM = <T>(id: string, useScriptElement: boolean = false): T | null => { if (typeof window === 'undefined') { return null } if (!useScriptElement) { const el = document.getElementById(id) if (el?.dataset.page) { return JSON.parse(el.dataset.page) } } const scriptEl = document.querySelector(`script[data-page="${id}"][type="application/json"]`) if (scriptEl?.textContent) { return JSON.parse(scriptEl.textContent) } return null } ================================================ FILE: packages/core/src/encryption.ts ================================================ import { SessionStorage } from './sessionStorage' export const encryptHistory = async (data: any): Promise<ArrayBuffer> => { if (typeof window === 'undefined') { throw new Error('Unable to encrypt history') } const iv = getIv() const storedKey = await getKeyFromSessionStorage() const key = await getOrCreateKey(storedKey) if (!key) { throw new Error('Unable to encrypt history') } const encrypted = await encryptData(iv, key, data) return encrypted } export const historySessionStorageKeys = { key: 'historyKey', iv: 'historyIv', } export const decryptHistory = async (data: any): Promise<any> => { const iv = getIv() const storedKey = await getKeyFromSessionStorage() if (!storedKey) { throw new Error('Unable to decrypt history') } return await decryptData(iv, storedKey, data) } const encryptData = async (iv: BufferSource, key: CryptoKey, data: any) => { if (typeof window === 'undefined') { throw new Error('Unable to encrypt history') } if (typeof window.crypto.subtle === 'undefined') { console.warn('Encryption is not supported in this environment. SSL is required.') return Promise.resolve(data) } const textEncoder = new TextEncoder() const str = JSON.stringify(data) const encoded = new Uint8Array(str.length * 3) const result = textEncoder.encodeInto(str, encoded) return window.crypto.subtle.encrypt( { name: 'AES-GCM', iv, }, key, encoded.subarray(0, result.written), ) } const decryptData = async (iv: BufferSource, key: CryptoKey, data: any) => { if (typeof window.crypto.subtle === 'undefined') { console.warn('Decryption is not supported in this environment. SSL is required.') return Promise.resolve(data) } const decrypted = await window.crypto.subtle.decrypt( { name: 'AES-GCM', iv, }, key, data, ) return JSON.parse(new TextDecoder().decode(decrypted)) } const getIv = (): BufferSource => { const ivString = SessionStorage.get(historySessionStorageKeys.iv) if (ivString) { return new Uint8Array(ivString) } const iv = window.crypto.getRandomValues(new Uint8Array(12)) SessionStorage.set(historySessionStorageKeys.iv, Array.from(iv)) return iv } const createKey = async () => { if (typeof window.crypto.subtle === 'undefined') { console.warn('Encryption is not supported in this environment. SSL is required.') return Promise.resolve(null) } return window.crypto.subtle.generateKey( { name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt'], ) } const saveKey = async (key: CryptoKey) => { if (typeof window.crypto.subtle === 'undefined') { console.warn('Encryption is not supported in this environment. SSL is required.') return Promise.resolve() } const keyData = await window.crypto.subtle.exportKey('raw', key) SessionStorage.set(historySessionStorageKeys.key, Array.from(new Uint8Array(keyData))) } const getOrCreateKey = async (key: CryptoKey | null) => { if (key) { return key } const newKey = await createKey() if (!newKey) { return null } await saveKey(newKey) return newKey } const getKeyFromSessionStorage = async (): Promise<CryptoKey | null> => { const stringKey = SessionStorage.get(historySessionStorageKeys.key) if (!stringKey) { return null } const key = await window.crypto.subtle.importKey( 'raw', new Uint8Array(stringKey), { name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt'], ) return key } ================================================ FILE: packages/core/src/eventHandler.ts ================================================ import debounce from './debounce' import { fireNavigateEvent } from './events' import { history } from './history' import { router } from './index' import { page as currentPage } from './page' import { Scroll } from './scroll' import { GlobalEvent, GlobalEventNames, GlobalEventResult, InternalEvent } from './types' import { hrefToUrl } from './url' class EventHandler { protected internalListeners: { event: InternalEvent listener: (...args: any[]) => void }[] = [] public init() { if (typeof window !== 'undefined') { window.addEventListener('popstate', this.handlePopstateEvent.bind(this)) window.addEventListener('pageshow', this.handlePageshowEvent.bind(this)) window.addEventListener('scroll', debounce(Scroll.onWindowScroll.bind(Scroll), 100), true) } if (typeof document !== 'undefined') { document.addEventListener('scroll', debounce(Scroll.onScroll.bind(Scroll), 100), true) } } public onGlobalEvent<TEventName extends GlobalEventNames>( type: TEventName, callback: (event: GlobalEvent<TEventName>) => GlobalEventResult<TEventName>, ): VoidFunction { const listener = ((event: GlobalEvent<TEventName>) => { const response = callback(event) if (event.cancelable && !event.defaultPrevented && response === false) { event.preventDefault() } }) as EventListener return this.registerListener(`inertia:${type}`, listener) } public on(event: InternalEvent, callback: (...args: any[]) => void): VoidFunction { this.internalListeners.push({ event, listener: callback }) return () => { this.internalListeners = this.internalListeners.filter((listener) => listener.listener !== callback) } } public onMissingHistoryItem() { // At this point, the user has probably cleared the state // Mark the current page as cleared so that we don't try to write anything to it. currentPage.clear() // Fire an event so that that any listeners can handle this situation this.fireInternalEvent('missingHistoryItem') } public fireInternalEvent(event: InternalEvent, ...args: any[]): void { this.internalListeners .filter((listener) => listener.event === event) .forEach((listener) => listener.listener(...args)) } protected registerListener(type: string, listener: EventListener): VoidFunction { document.addEventListener(type, listener) return () => document.removeEventListener(type, listener) } // bfcache restores pages without firing `popstate`, so we use `pageshow` to // re-validate encrypted history entries after `clearHistory` removed the keys. // https://web.dev/articles/bfcache protected handlePageshowEvent(event: PageTransitionEvent): void { if (event.persisted) { history.decrypt().catch(() => this.onMissingHistoryItem()) } } protected handlePopstateEvent(event: PopStateEvent): void { const state = event.state || null if (state === null) { const url = hrefToUrl(currentPage.get().url) url.hash = window.location.hash history.replaceState({ ...currentPage.getWithoutFlashData(), url: url.href }) Scroll.reset() return } if (!history.isValidState(state)) { return this.onMissingHistoryItem() } history .decrypt(state.page) .then((data) => { if (currentPage.get().version !== data.version) { this.onMissingHistoryItem() return } // Cancel ongoing requests except prefetch requests router.cancelAll({ prefetch: false }) currentPage.setQuietly(data, { preserveState: false }).then(() => { Scroll.restore(history.getScrollRegions()) fireNavigateEvent(currentPage.get()) const pendingDeferred: Record<string, string[]> = {} const pageProps = currentPage.get().props for (const [group, props] of Object.entries(data.initialDeferredProps ?? data.deferredProps ?? {})) { const missing = props.filter((prop) => pageProps[prop] === undefined) if (missing.length > 0) { pendingDeferred[group] = missing } } if (Object.keys(pendingDeferred).length > 0) { this.fireInternalEvent('loadDeferredProps', pendingDeferred) } }) }) .catch(() => { this.onMissingHistoryItem() }) } } export const eventHandler = new EventHandler() ================================================ FILE: packages/core/src/events.ts ================================================ import { GlobalEventDetails, GlobalEventNames, GlobalEventTrigger } from './types' function fireEvent<TEventName extends GlobalEventNames>( name: TEventName, options: CustomEventInit<GlobalEventDetails<TEventName>>, ): boolean { return document.dispatchEvent(new CustomEvent(`inertia:${name}`, options)) } export const fireBeforeEvent: GlobalEventTrigger<'before'> = (visit) => { return fireEvent('before', { cancelable: true, detail: { visit } }) } export const fireErrorEvent: GlobalEventTrigger<'error'> = (errors) => { return fireEvent('error', { detail: { errors } }) } export const fireExceptionEvent: GlobalEventTrigger<'exception'> = (exception) => { return fireEvent('exception', { cancelable: true, detail: { exception } }) } export const fireFinishEvent: GlobalEventTrigger<'finish'> = (visit) => { return fireEvent('finish', { detail: { visit } }) } export const fireInvalidEvent: GlobalEventTrigger<'invalid'> = (response) => { return fireEvent('invalid', { cancelable: true, detail: { response } }) } export const fireBeforeUpdateEvent: GlobalEventTrigger<'beforeUpdate'> = (page) => { return fireEvent('beforeUpdate', { detail: { page } }) } export const fireNavigateEvent: GlobalEventTrigger<'navigate'> = (page) => { return fireEvent('navigate', { detail: { page } }) } export const fireProgressEvent: GlobalEventTrigger<'progress'> = (progress) => { return fireEvent('progress', { detail: { progress } }) } export const fireStartEvent: GlobalEventTrigger<'start'> = (visit) => { return fireEvent('start', { detail: { visit } }) } export const fireSuccessEvent: GlobalEventTrigger<'success'> = (page) => { return fireEvent('success', { detail: { page } }) } export const firePrefetchedEvent: GlobalEventTrigger<'prefetched'> = (response, visit) => { return fireEvent('prefetched', { detail: { fetchedAt: Date.now(), response: response.data, visit } }) } export const firePrefetchingEvent: GlobalEventTrigger<'prefetching'> = (visit) => { return fireEvent('prefetching', { detail: { visit } }) } export const fireFlashEvent: GlobalEventTrigger<'flash'> = (flash) => { return fireEvent('flash', { detail: { flash } }) } ================================================ FILE: packages/core/src/files.ts ================================================ import { FormDataConvertible, RequestPayload } from './types' export const isFile = (value: unknown): boolean => (typeof File !== 'undefined' && value instanceof File) || value instanceof Blob || (typeof FileList !== 'undefined' && value instanceof FileList && value.length > 0) export function hasFiles(data: RequestPayload | FormDataConvertible): boolean { return ( isFile(data) || (data instanceof FormData && Array.from(data.values()).some((value) => hasFiles(value))) || (typeof data === 'object' && data !== null && Object.values(data).some((value) => hasFiles(value))) ) } ================================================ FILE: packages/core/src/formData.ts ================================================ import type { FormDataConvertible, QueryStringArrayFormatOption } from './types' export const isFormData = (value: any): value is FormData => value instanceof FormData export function objectToFormData( source: Record<string, FormDataConvertible>, form: FormData = new FormData(), parentKey: string | null = null, queryStringArrayFormat: QueryStringArrayFormatOption = 'brackets', ): FormData { source = source || {} for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { append(form, composeKey(parentKey, key, 'indices'), source[key], queryStringArrayFormat) } } return form } function composeKey(parent: string | null, key: string, format: QueryStringArrayFormatOption): string { if (!parent) { return key } return format === 'brackets' ? `${parent}[]` : `${parent}[${key}]` } function append(form: FormData, key: string, value: FormDataConvertible, format: QueryStringArrayFormatOption): void { if (Array.isArray(value)) { return Array.from(value.keys()).forEach((index) => append(form, composeKey(key, index.toString(), format), value[index], format), ) } else if (value instanceof Date) { return form.append(key, value.toISOString()) } else if (value instanceof File) { return form.append(key, value, value.name) } else if (value instanceof Blob) { return form.append(key, value) } else if (typeof value === 'boolean') { return form.append(key, value ? '1' : '0') } else if (typeof value === 'string') { return form.append(key, value) } else if (typeof value === 'number') { return form.append(key, `${value}`) } else if (value === null || value === undefined) { return form.append(key, '') } objectToFormData(value, form, key, format) } ================================================ FILE: packages/core/src/formObject.ts ================================================ import { get, set } from 'lodash-es' import { isFile } from './files' import { FormDataConvertible } from './types' /** * Transform dotted notation to bracket notation. * * Examples: * user.name => user[name] * user.profile.city => user[profile][city] * user.skills[] => user[skills][] * users.company[address].street => users[company][address][street] * config\.app\.name => config.app.name (escaped, literal) */ function undotKey(key: string): string { if (!key.includes('.')) { return key } const transformSegment = (segment: string): string => { if (segment.startsWith('[') && segment.endsWith(']')) { return segment // Already in bracket notation - leave untouched } // Convert dotted segment to bracket notation: "user.name" → "user[name]" return segment.split('.').reduce((result, part, index) => (index === 0 ? part : `${result}[${part}]`)) } return key .replace(/\\\./g, '__ESCAPED_DOT__') // Temporarily replace escaped dots (\.) to protect them from transformation .split(/(\[[^\]]*\])/) // Split on bracket notation while preserving the brackets in the result array .filter(Boolean) // Remove empty strings from the split operation .map(transformSegment) // Transform each segment: dotted parts become bracketed, existing brackets stay as-is .join('') // Reassemble all segments back into a single string .replace(/__ESCAPED_DOT__/g, '.') // Restore the escaped dots as literal dots in the final result } /** * Parse a key into an array of path segments. * * Examples: * - "user[name]" => ["user", "name"] * - "tags[]" => ["tags", ""] * - "items[0][name]" => ["items", 0, "name"] */ function parseKey(key: string): (string | number | '')[] { const path: (string | number | '')[] = [] const pattern = /([^\[\]]+)|\[(\d*)\]/g let match: RegExpExecArray | null while ((match = pattern.exec(key)) !== null) { if (match[1] !== undefined) { path.push(match[1]) } else if (match[2] !== undefined) { path.push(match[2] === '' ? '' : Number(match[2])) } } return path } /** * Set value in nested object, always creating objects (never arrays). * This ensures we can analyze the final structure before deciding what should be arrays. */ function setNestedObject(obj: Record<string, any>, path: string[], value: any): void { let current = obj for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in current)) { current[path[i]] = {} } current = current[path[i]] } current[path[path.length - 1]] = value } /** * Check if an object has sequential numeric keys (0, 1, 2, ...). */ function objectHasSequentialNumericKeys(value: any): boolean { const keys = Object.keys(value) const numericKeys = keys .filter((k) => /^\d+$/.test(k)) .map(Number) .sort((a, b) => a - b) return ( keys.length === numericKeys.length && numericKeys.length > 0 && numericKeys[0] === 0 && numericKeys.every((n, i) => n === i) ) } /** * Convert objects with sequential numeric keys (0, 1, 2, ...) to arrays. */ function convertSequentialObjectsToArrays(value: any): any { if (Array.isArray(value)) { return value.map(convertSequentialObjectsToArrays) } if (typeof value !== 'object' || value === null || isFile(value)) { return value } if (objectHasSequentialNumericKeys(value)) { const result = [] for (let i = 0; i < Object.keys(value).length; i++) { result[i] = convertSequentialObjectsToArrays(value[i]) } return result } // Keep as object, recursively process values const result: Record<string, any> = {} for (const key in value) { result[key] = convertSequentialObjectsToArrays(value[key]) } return result } /** * Convert a FormData instance into an object structure. */ export function formDataToObject(source: FormData): Record<string, FormDataConvertible> { const form: Record<string, any> = {} // formData.entries() returns an iterator where the first element is the key and the second element // is the value. Examples of the keys are "user[name]", "tags[]", "items[0][name]", "user.name", etc. // We should construct a new (nested) object based on these keys. for (const [key, value] of source.entries()) { if (value instanceof File && value.size === 0 && value.name === '') { // Check if the given value is an empty file. We want to filter // those out as they prevent us from comparing objects with // each other, which we do to set the isDirty prop. continue } const path = parseKey(undotKey(key)) // If the key ends with an empty string (''), treat it as an array push (e.g., "tags[]") if (path[path.length - 1] === '') { const arrayPath = path.slice(0, -1) const existing = get(form, arrayPath) if (Array.isArray(existing)) { existing.push(value) } else if (existing && typeof existing === 'object' && !isFile(existing)) { // If existing is an object with numeric keys, convert to array (treating indices as relative) const numericKeys = Object.keys(existing) .filter((k) => /^\d+$/.test(k)) .map(Number) .sort((a, b) => a - b) set(form, arrayPath, numericKeys.length > 0 ? [...numericKeys.map((k) => existing[k]), value] : [value]) } else { set(form, arrayPath, [value]) } continue } // Always build nested objects first, then convert sequential numeric keys to arrays. // This prevents the creation of sparse arrays when mixing numeric and string keys. setNestedObject(form, path.map(String), value) } // Convert objects with sequential numeric keys (0, 1, 2, ...) to arrays return convertSequentialObjectsToArrays(form) } ================================================ FILE: packages/core/src/head.ts ================================================ import { config, HeadManager, HeadManagerOnUpdateCallback, HeadManagerTitleCallback } from '.' import debounce from './debounce' const Renderer = { preferredAttribute(): 'data-inertia' | 'inertia' { return config.get('future.useDataInertiaHeadAttribute') ? 'data-inertia' : 'inertia' }, buildDOMElement(tag: string): ChildNode { const template = document.createElement('template') template.innerHTML = tag const node = template.content.firstChild as Element if (!tag.startsWith('<script ')) { return node } const script = document.createElement('script') script.innerHTML = node.innerHTML node.getAttributeNames().forEach((name) => { script.setAttribute(name, node.getAttribute(name) || '') }) return script }, isInertiaManagedElement(element: Element): boolean { return element.nodeType === Node.ELEMENT_NODE && element.getAttribute(this.preferredAttribute()) !== null }, findMatchingElementIndex(element: Element, elements: Array<Element>): number { const attribute = this.preferredAttribute() const key = element.getAttribute(attribute) if (key !== null) { return elements.findIndex((element) => element.getAttribute(attribute) === key) } return -1 }, update: debounce(function (elements: Array<string>) { const sourceElements = elements.map((element) => this.buildDOMElement(element)) const targetElements = Array.from(document.head.childNodes).filter((element) => this.isInertiaManagedElement(element as Element), ) targetElements.forEach((targetElement) => { const index = this.findMatchingElementIndex(targetElement as Element, sourceElements) if (index === -1) { targetElement?.parentNode?.removeChild(targetElement) return } const sourceElement = sourceElements.splice(index, 1)[0] if (sourceElement && !targetElement.isEqualNode(sourceElement)) { targetElement?.parentNode?.replaceChild(sourceElement, targetElement) } }) sourceElements.forEach((element) => document.head.appendChild(element)) }, 1), } export default function createHeadManager( isServer: boolean, titleCallback: HeadManagerTitleCallback, onUpdate: HeadManagerOnUpdateCallback, ): HeadManager { const states: Record<string, Array<string>> = {} let lastProviderId = 0 function connect(): string { const id = (lastProviderId += 1) states[id] = [] return id.toString() } function disconnect(id: string): void { if (id === null || Object.keys(states).indexOf(id) === -1) { return } delete states[id] commit() } function reconnect(id: string): void { if (Object.keys(states).indexOf(id) === -1) { states[id] = [] } } function update(id: string, elements: Array<string> = []): void { if (id !== null && Object.keys(states).indexOf(id) > -1) { states[id] = elements } commit() } function collect(): Array<string> { const title = titleCallback('') const attribute = Renderer.preferredAttribute() const defaults: Record<string, string> = { ...(title ? { title: `<title ${attribute}="">${title}` } : {}), } const elements = Object.values(states) .reduce((carry, elements) => carry.concat(elements), []) .reduce((carry, element) => { if (element.indexOf('<') === -1) { return carry } if (element.indexOf(']+>)(.*?)(<\/title>)/) carry.title = title ? `${title[1]}${titleCallback(title[2])}${title[3]}` : element return carry } const match = element.match(attribute === 'inertia' ? / inertia="[^"]+"/ : / data-inertia="[^"]+"/) if (match) { carry[match[0]] = element } else { carry[Object.keys(carry).length] = element } return carry }, defaults) return Object.values(elements) } function commit(): void { isServer ? onUpdate(collect()) : Renderer.update(collect()) } // By committing during initialization, we can guarantee that the default // tags are set, as well as that they exist during SSR itself. commit() return { forceUpdate: commit, createProvider: function () { const id = connect() return { preferredAttribute: Renderer.preferredAttribute, reconnect: () => reconnect(id), update: (elements) => update(id, elements), disconnect: () => disconnect(id), } }, } } ================================================ FILE: packages/core/src/history.ts ================================================ import { cloneDeep, isEqual } from 'lodash-es' import { decryptHistory, encryptHistory, historySessionStorageKeys } from './encryption' import { eventHandler } from './eventHandler' import { page as currentPage } from './page' import Queue from './queue' import { SessionStorage } from './sessionStorage' import { Page, ScrollRegion } from './types' const isServer = typeof window === 'undefined' const queue = new Queue<Promise<void>>() const isChromeIOS = !isServer && /CriOS/.test(window.navigator.userAgent) class History { public rememberedState = 'rememberedState' as const public scrollRegions = 'scrollRegions' as const public preserveUrl = false protected current: Partial<Page> = {} // We need initialState for `restore` protected initialState: Partial<Page> | null = null public remember(data: unknown, key: string): void { this.replaceState({ ...currentPage.getWithoutFlashData(), rememberedState: { ...(currentPage.get()?.rememberedState ?? {}), [key]: data, }, }) } public restore(key: string): unknown { if (!isServer) { return this.current[this.rememberedState]?.[key] !== undefined ? this.current[this.rememberedState]?.[key] : this.initialState?.[this.rememberedState]?.[key] } } public pushState(page: Page, cb: (() => void) | null = null): void { if (isServer) { return } if (this.preserveUrl) { cb && cb() return } this.current = page queue.add(() => { return this.getPageData(page).then((data) => { // Defer history.pushState to the next event loop tick to prevent timing conflicts. // Ensure any previous history.replaceState completes before pushState is executed. const doPush = () => this.doPushState({ page: data }, page.url).then(() => cb?.()) if (isChromeIOS) { return new Promise((resolve) => { setTimeout(() => doPush().then(resolve)) }) } return doPush() }) }) } protected clonePageProps(page: Page): Page { try { structuredClone(page.props) return page } catch { // Props contain non-serializable data (e.g., Proxies, functions). // Clone them to ensure they can be safely stored in browser history. return { ...page, props: cloneDeep(page.props), } } } protected getPageData(page: Page): Promise<Page | ArrayBuffer> { const pageWithClonedProps = this.clonePageProps(page) return new Promise((resolve) => { return page.encryptHistory ? encryptHistory(pageWithClonedProps).then(resolve) : resolve(pageWithClonedProps) }) } public processQueue(): Promise<void> { return queue.process() } public decrypt(page: Page | null = null): Promise<Page> { if (isServer) { return Promise.resolve(page ?? currentPage.get()) } const pageData = page ?? window.history.state?.page return this.decryptPageData(pageData).then((data) => { if (!data) { throw new Error('Unable to decrypt history') } if (this.initialState === null) { this.initialState = data ?? undefined } else { this.current = data ?? {} } return data }) } protected decryptPageData(pageData: ArrayBuffer | Page | null): Promise<Page | null> { return pageData instanceof ArrayBuffer ? decryptHistory(pageData) : Promise.resolve(pageData) } public saveScrollPositions(scrollRegions: ScrollRegion[]): void { queue.add(() => { return Promise.resolve().then(() => { if (!window.history.state?.page) { return } if (isEqual(this.getScrollRegions(), scrollRegions)) { return } return this.doReplaceState({ page: window.history.state.page, scrollRegions, }) }) }) } public saveDocumentScrollPosition(scrollRegion: ScrollRegion): void { queue.add(() => { return Promise.resolve().then(() => { if (!window.history.state?.page) { return } if (isEqual(this.getDocumentScrollPosition(), scrollRegion)) { return } return this.doReplaceState({ page: window.history.state.page, documentScrollPosition: scrollRegion, }) }) }) } public getScrollRegions(): ScrollRegion[] { return window.history.state?.scrollRegions || [] } public getDocumentScrollPosition(): ScrollRegion { return window.history.state?.documentScrollPosition || { top: 0, left: 0 } } public replaceState(page: Page, cb: (() => void) | null = null): void { if (isEqual(this.current, page)) { cb && cb() return } // Exclude flash from the merge to prevent callers (like router.remember()) // from accidentally clearing flash data on the current page. const { flash, ...pageWithoutFlash } = page currentPage.merge(pageWithoutFlash) if (isServer) { return } if (this.preserveUrl) { cb && cb() return } this.current = page queue.add(() => { return this.getPageData(page).then((data) => { // Defer history.replaceState to the next event loop tick to prevent timing conflicts. // Ensure any previous history.pushState completes before replaceState is executed. const doReplace = () => this.doReplaceState({ page: data }, page.url).then(() => cb?.()) if (isChromeIOS) { return new Promise((resolve) => { setTimeout(() => doReplace().then(resolve)) }) } return doReplace() }) }) } protected isHistoryThrottleError(error: unknown): error is Error & { name: 'SecurityError' } { return ( error instanceof Error && error.name === 'SecurityError' && (error.message.includes('history.pushState') || error.message.includes('history.replaceState')) ) } protected isQuotaExceededError(error: unknown): error is Error & { name: 'QuotaExceededError' } { return error instanceof Error && error.name === 'QuotaExceededError' } protected withThrottleProtection<T = void>(cb: () => T): Promise<T | undefined> { return Promise.resolve().then(() => { try { return cb() } catch (error) { if (!this.isHistoryThrottleError(error)) { throw error } console.error(error.message) } }) } protected doReplaceState( data: { page: Page | ArrayBuffer scrollRegions?: ScrollRegion[] documentScrollPosition?: ScrollRegion }, url?: string, ): Promise<void> { return this.withThrottleProtection(() => { window.history.replaceState( { ...data, scrollRegions: data.scrollRegions ?? window.history.state?.scrollRegions, documentScrollPosition: data.documentScrollPosition ?? window.history.state?.documentScrollPosition, }, '', url, ) }) } protected doPushState( data: { page: Page | ArrayBuffer scrollRegions?: ScrollRegion[] documentScrollPosition?: ScrollRegion }, url: string, ): Promise<void> { return this.withThrottleProtection(() => { try { window.history.pushState(data, '', url) } catch (error) { if (!this.isQuotaExceededError(error)) { throw error } eventHandler.fireInternalEvent('historyQuotaExceeded', url) } }) } public getState<T>(key: keyof Page, defaultValue?: T): any { return this.current?.[key] ?? defaultValue } public deleteState(key: keyof Page) { if (this.current[key] !== undefined) { delete this.current[key] this.replaceState(this.current as Page) } } public clearInitialState(key: keyof Page) { if (this.initialState && this.initialState[key] !== undefined) { delete this.initialState[key] } } public browserHasHistoryEntry(): boolean { return !isServer && !!window.history.state?.page } public clear() { SessionStorage.remove(historySessionStorageKeys.key) SessionStorage.remove(historySessionStorageKeys.iv) } public setCurrent(page: Page): void { this.current = page } public isValidState(state: any): boolean { return !!state.page } public getAllState(): Page { return this.current as Page } } if (typeof window !== 'undefined' && window.history.scrollRestoration) { window.history.scrollRestoration = 'manual' } export const history = new History() ================================================ FILE: packages/core/src/index.ts ================================================ import { Config } from './config' import { Router } from './router' export { UseFormUtils } from './useFormUtils' export { config } from './config' export { getInitialPageFromDOM, getScrollableParent } from './domUtils' export { objectToFormData } from './formData' export { formDataToObject } from './formObject' export { default as createHeadManager } from './head' export { default as useInfiniteScroll } from './infiniteScroll' export { shouldIntercept, shouldNavigate } from './navigationEvents' export { hide as hideProgress, progress, reveal as revealProgress, default as setupProgress } from './progress' export { FormComponentResetSymbol, resetFormFields } from './resetFormFields' export * from './types' export { hrefToUrl, isUrlMethodPair, mergeDataIntoQueryString, urlHasProtocol, urlToString, urlWithoutHash, } from './url' export { type Config, type Router } export const router = new Router() ================================================ FILE: packages/core/src/infiniteScroll/data.ts ================================================ import { router } from '../index' import { page as currentPage } from '../page' import { Page, PendingVisit, ReloadOptions, ScrollProp, UseInfiniteScrollDataManager } from '../types' const MERGE_INTENT_HEADER = 'X-Inertia-Infinite-Scroll-Merge-Intent' type Side = 'previous' | 'next' type ScrollPropPageNames = keyof Pick<ScrollProp, 'previousPage' | 'nextPage'> type InfiniteScrollState = { previousPage: number | string | null nextPage: number | string | null lastLoadedPage: number | string | null requestCount: number } export const useInfiniteScrollData = (options: { getPropName: () => string onBeforeUpdate: () => void onBeforePreviousRequest: () => void onBeforeNextRequest: () => void onCompletePreviousRequest: (loadedPage: string | number | null) => void onCompleteNextRequest: (loadedPage: string | number | null) => void onReset?: () => void }): UseInfiniteScrollDataManager => { const getScrollPropFromCurrentPage = (): ScrollProp => { const scrollProp = currentPage.get().scrollProps?.[options.getPropName()] if (scrollProp) { return scrollProp } throw new Error(`The page object does not contain a scroll prop named "${options.getPropName()}".`) } const state = { component: null, loading: false, previousPage: null, nextPage: null, lastLoadedPage: null, requestCount: 0, } as { component: string | null loading: boolean } & InfiniteScrollState const resetState = () => { const scrollProp = getScrollPropFromCurrentPage() state.component = currentPage.get().component state.loading = false state.previousPage = scrollProp.previousPage state.nextPage = scrollProp.nextPage state.lastLoadedPage = scrollProp.currentPage state.requestCount = 0 } const getRememberKey = () => `inertia:infinite-scroll-data:${options.getPropName()}` if (typeof window !== 'undefined') { resetState() const rememberedState = router.restore(getRememberKey()) as InfiniteScrollState | undefined if ( rememberedState && typeof rememberedState === 'object' && rememberedState.lastLoadedPage === getScrollPropFromCurrentPage().currentPage ) { // Restore remembered state only when it's consistent with the current scroll prop, // which ensures back/forward navigation works while direct URL visits reset properly. state.previousPage = rememberedState.previousPage state.nextPage = rememberedState.nextPage state.lastLoadedPage = rememberedState.lastLoadedPage state.requestCount = rememberedState.requestCount || 0 } } const removeEventListener = router.on('success', (event) => { if (state.component === event.detail.page.component && getScrollPropFromCurrentPage().reset) { resetState() options.onReset?.() } }) const getScrollPropKeyForSide = (side: Side): ScrollPropPageNames => { return side === 'next' ? 'nextPage' : 'previousPage' } const findPageToLoad = (side: Side) => { const pagePropName = getScrollPropKeyForSide(side) return state[pagePropName] } const syncStateOnSuccess = (side: Side) => { const scrollProp = getScrollPropFromCurrentPage() const paginationProp = getScrollPropKeyForSide(side) state.lastLoadedPage = scrollProp.currentPage state[paginationProp] = scrollProp[paginationProp] state.requestCount += 1 // We save the state in the browser history so it can be restored // if the user navigates away and then back to the page... router.remember( { previousPage: state.previousPage, nextPage: state.nextPage, lastLoadedPage: state.lastLoadedPage, requestCount: state.requestCount, } as InfiniteScrollState, getRememberKey(), ) } const getPageName = () => getScrollPropFromCurrentPage().pageName const getRequestCount = () => state.requestCount const fetchPage = (side: Side, reloadOptions: ReloadOptions = {}): void => { const page = findPageToLoad(side) if (state.loading || page === null) { return } state.loading = true router.reload({ ...reloadOptions, data: { [getPageName()]: page }, only: [options.getPropName()], preserveUrl: true, // we handle URL updates manually via useInfiniteScrollQueryString() headers: { [MERGE_INTENT_HEADER]: side === 'previous' ? 'prepend' : 'append', ...reloadOptions.headers, }, onBefore: (visit: PendingVisit) => { side === 'next' ? options.onBeforeNextRequest() : options.onBeforePreviousRequest() reloadOptions.onBefore?.(visit) }, onBeforeUpdate: (page: Page) => { options.onBeforeUpdate() reloadOptions.onBeforeUpdate?.(page) }, onSuccess: (page: Page) => { syncStateOnSuccess(side) reloadOptions.onSuccess?.(page) }, onFinish: (visit: any) => { state.loading = false side === 'next' ? options.onCompleteNextRequest(state.lastLoadedPage) : options.onCompletePreviousRequest(state.lastLoadedPage) reloadOptions.onFinish?.(visit) }, }) } const getLastLoadedPage = () => state.lastLoadedPage const hasPrevious = () => !!state.previousPage const hasNext = () => !!state.nextPage const fetchPrevious = (reloadOptions?: ReloadOptions): void => fetchPage('previous', reloadOptions) const fetchNext = (reloadOptions?: ReloadOptions): void => fetchPage('next', reloadOptions) return { getLastLoadedPage, getPageName, getRequestCount, hasPrevious, hasNext, fetchNext, fetchPrevious, removeEventListener, } } ================================================ FILE: packages/core/src/infiniteScroll/elements.ts ================================================ import { router } from '..' import debounce from '../debounce' import { useIntersectionObservers } from '../intersectionObservers' import { UseInfiniteScrollElementManager } from '../types' const INFINITE_SCROLL_PAGE_KEY = 'infiniteScrollPage' const INFINITE_SCROLL_IGNORE_KEY = 'infiniteScrollIgnore' type ElementRange = { from: number to: number } export const getPageFromElement = (element: HTMLElement): string | undefined => element.dataset[INFINITE_SCROLL_PAGE_KEY] export const useInfiniteScrollElementManager = (options: { shouldFetchNext: () => boolean shouldFetchPrevious: () => boolean getTriggerMargin: () => number getStartElement: () => HTMLElement getEndElement: () => HTMLElement getItemsElement: () => HTMLElement getScrollableParent: () => HTMLElement | null onPreviousTriggered: () => void onNextTriggered: () => void onItemIntersected: (element: HTMLElement) => void getPropName: () => string }): UseInfiniteScrollElementManager => { const intersectionObservers = useIntersectionObservers() let itemsObserver: IntersectionObserver let startElementObserver: IntersectionObserver let endElementObserver: IntersectionObserver let itemsMutationObserver: MutationObserver let triggersEnabled = false const setupObservers = () => { // Watch for manually added DOM elements (not from server responses) // This mutation observer tracks when new elements are added to the slot, // so we can distinguish between user-added content and server-loaded pages itemsMutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) { return } addedElements.add(node as HTMLElement) }) }) rememberElementsDebounced() }) itemsMutationObserver.observe(options.getItemsElement(), { childList: true }) // Track individual items entering/leaving viewport for URL synchronization // When items become visible, we update the URL to reflect the current page itemsObserver = intersectionObservers.new((entry: IntersectionObserverEntry) => options.onItemIntersected(entry.target as HTMLElement), ) // Set up trigger zones at start/end that load more content when intersected. The rootMargin // creates a buffer zone so loading starts before user reaches the edge. We should always // have a root margin of at least 1px as our default elements have no height const observerOptions: IntersectionObserverInit = { root: options.getScrollableParent(), rootMargin: `${Math.max(1, options.getTriggerMargin())}px`, } startElementObserver = intersectionObservers.new(options.onPreviousTriggered, observerOptions) endElementObserver = intersectionObservers.new(options.onNextTriggered, observerOptions) } const enableTriggers = () => { if (triggersEnabled) { // Make sure we don't register multiple watchers disableTriggers() } const startElement = options.getStartElement() const endElement = options.getEndElement() if (startElement && options.shouldFetchPrevious()) { startElementObserver.observe(startElement) } if (endElement && options.shouldFetchNext()) { endElementObserver.observe(endElement) } triggersEnabled = true } const disableTriggers = () => { if (!triggersEnabled) { return } startElementObserver.disconnect() endElementObserver.disconnect() triggersEnabled = false } const refreshTriggers = () => { if (triggersEnabled) { enableTriggers() } } const flushAll = () => { disableTriggers() intersectionObservers.flushAll() itemsMutationObserver?.disconnect() } const addedElements = new Set<HTMLElement>() const elementIsUntagged = (element: HTMLElement): boolean => !(INFINITE_SCROLL_PAGE_KEY in element.dataset) && !(INFINITE_SCROLL_IGNORE_KEY in element.dataset) const processManuallyAddedElements = () => { // Tag manually added elements so they don't interfere with URL management // These elements get marked as "ignore" since they weren't loaded from the server Array.from(addedElements).forEach((element) => { if (elementIsUntagged(element)) { element.dataset[INFINITE_SCROLL_IGNORE_KEY] = 'true' } itemsObserver.observe(element) }) addedElements.clear() } const findUntaggedElements = (containerElement: HTMLElement): HTMLElement[] => { return Array.from( containerElement.querySelectorAll( `:scope > *:not([data-infinite-scroll-page]):not([data-infinite-scroll-ignore])`, ), ) } let hasRestoredElements = false const processServerLoadedElements = (loadedPage: string | number | null) => { // On first run, try to restore the elements tags from browser history if (!hasRestoredElements) { hasRestoredElements = true if (restoreElements()) { return } } // Tag new server-loaded elements with their page number for URL management // This allows us to update the URL based on which page's content is most visible findUntaggedElements(options.getItemsElement()).forEach((element) => { if (elementIsUntagged(element)) { element.dataset[INFINITE_SCROLL_PAGE_KEY] = loadedPage?.toString() || '1' } itemsObserver.observe(element) }) rememberElements() } const getElementsRememberKey = () => `inertia:infinite-scroll-elements:${options.getPropName()}` // Remember in browser history which elements belong to which page, so we can restore // them on back/forward navigation and keep URL synchronization working correctly const rememberElements = () => { const pageElementRange: Record<string, ElementRange> = {} const childNodes = options.getItemsElement().childNodes for (let index = 0; index < childNodes.length; index++) { const node = childNodes[index] if (node.nodeType !== Node.ELEMENT_NODE) { continue } const page = getPageFromElement(node as HTMLElement) if (typeof page === 'undefined') { continue } if (!(page in pageElementRange)) { pageElementRange[page] = { from: index, to: index } } else { pageElementRange[page].to = index } } router.remember(pageElementRange, getElementsRememberKey()) } const rememberElementsDebounced = debounce(rememberElements, 250) const restoreElements = (): boolean => { const pageElementRange = router.restore(getElementsRememberKey()) as Record<string, ElementRange> | undefined if (!pageElementRange || typeof pageElementRange !== 'object') { return false } const childNodes = options.getItemsElement().childNodes // Use for loop instead of forEach for better performance for (let index = 0; index < childNodes.length; index++) { const node = childNodes[index] if (node.nodeType !== Node.ELEMENT_NODE) { continue } const element = node as HTMLElement // Find which page this element belongs to based on ranges let elementPage: string | undefined for (const [page, range] of Object.entries(pageElementRange)) { if (index >= range.from && index <= range.to) { elementPage = page break } } if (elementPage) { element.dataset[INFINITE_SCROLL_PAGE_KEY] = elementPage } else if (!elementIsUntagged(element)) { continue } else { element.dataset[INFINITE_SCROLL_IGNORE_KEY] = 'true' } itemsObserver.observe(element) } return true } return { setupObservers, enableTriggers, disableTriggers, refreshTriggers, flushAll, processManuallyAddedElements, processServerLoadedElements, } } ================================================ FILE: packages/core/src/infiniteScroll/queryString.ts ================================================ import { hrefToUrl, router, urlHasProtocol, urlToString } from '..' import debounce from '../debounce' import { getElementsInViewportFromCollection } from '../domUtils' import { page as currentPage } from './../page' import Queue from './../queue' import { getPageFromElement } from './elements' // Shared queue among all instances to ensure URL updates are processed sequentially const queue = new Queue<Promise<void>>() let initialUrl: URL | null let payloadUrl: URL | null let initialUrlWasAbsolute: boolean | null = null /** * As users scroll through infinite content, this system updates the URL to reflect * which page they're currently viewing. It uses a "most visible page" calculation * so that the URL reflects whichever page has the most visible items. */ export const useInfiniteScrollQueryString = (options: { getPageName: () => string getItemsElement: () => HTMLElement shouldPreserveUrl: () => boolean }) => { let enabled = true const queuePageUpdate = (page: string) => { queue .add(() => { return new Promise((resolve) => { if (!enabled) { initialUrl = payloadUrl = null return resolve() } if (!initialUrl || !payloadUrl) { const currentPageUrl = currentPage.get().url initialUrl = hrefToUrl(currentPageUrl) payloadUrl = hrefToUrl(currentPageUrl) initialUrlWasAbsolute = urlHasProtocol(currentPageUrl) } const pageName = options.getPageName() const searchParams = payloadUrl.searchParams // Clean URLs: don't show ?page=1 in the URL, just remove the parameter entirely if (page === '1') { searchParams.delete(pageName) } else { searchParams.set(pageName, page) } setTimeout(() => resolve()) }) }) .finally(() => { if ( enabled && initialUrl && payloadUrl && initialUrl.href !== payloadUrl.href && initialUrlWasAbsolute !== null ) { // Update URL without triggering a page reload or affecting scroll position router.replace({ url: urlToString(payloadUrl, initialUrlWasAbsolute), preserveScroll: true, preserveState: true, }) } initialUrl = payloadUrl = initialUrlWasAbsolute = null }) } // Debounced to avoid excessive URL updates during fast scrolling const onItemIntersected = debounce((itemElement: HTMLElement) => { const itemsElement = options.getItemsElement() if (!enabled || options.shouldPreserveUrl() || !itemElement || !itemsElement) { return } // Count how many items from each page are currently visible in the viewport const pageMap = new Map<string, number>() const elements = [...itemsElement.children] as HTMLElement[] getElementsInViewportFromCollection(elements, itemElement).forEach((element) => { const page = getPageFromElement(element) ?? '1' if (pageMap.has(page)) { pageMap.set(page, pageMap.get(page)! + 1) } else { pageMap.set(page, 1) } }) // Find the page with the most visible items - this becomes the "current" page const sortedPages = Array.from(pageMap.entries()).sort((a, b) => b[1] - a[1]) const mostVisiblePage = sortedPages[0]?.[0] if (mostVisiblePage !== undefined) { queuePageUpdate(mostVisiblePage) } }, 250) return { onItemIntersected, cancel: () => (enabled = false), } } ================================================ FILE: packages/core/src/infiniteScroll/scrollPreservation.ts ================================================ import { getElementsInViewportFromCollection } from '../domUtils' /** * When loading content "before" the current viewport (e.g. loading page 1 when viewing page 2), * new content is prepended to the DOM, which naturally pushes existing content down and * disrupts the user's scroll position. This system maintains visual stability by: * * 1. Capturing a reference element and its position before the update * 2. After new content is added, calculating how far that reference element moved * 3. Adjusting scroll position to keep the reference element in the same visual location */ export const useInfiniteScrollPreservation = (options: { getScrollableParent: () => HTMLElement | null getItemsElement: () => HTMLElement }) => { const createCallbacks = () => { let currentScrollTop: number let referenceElement: Element | null = null let referenceElementTop: number = 0 const captureScrollPosition = () => { const scrollableContainer = options.getScrollableParent() const itemsElement = options.getItemsElement() // Capture current scroll position currentScrollTop = scrollableContainer?.scrollTop || window.scrollY // Find the first visible element to use as a reference point // This element will help us calculate how much the content shifted after the update const visibleElements = getElementsInViewportFromCollection([...itemsElement.children] as HTMLElement[]) if (visibleElements.length > 0) { referenceElement = visibleElements[0] const containerRect = scrollableContainer?.getBoundingClientRect() || { top: 0 } const containerTop = scrollableContainer ? containerRect.top : 0 const rect = referenceElement.getBoundingClientRect() // Store the reference element's position relative to its container referenceElementTop = rect.top - containerTop } } const restoreScrollPosition = () => { if (!referenceElement) { return } let attempts = 0 let restored = false const restore = () => { attempts++ if (restored || attempts > 10) { return false } // Calculate where our reference element ended up after new content was prepended const scrollableContainer = options.getScrollableParent() const containerRect = scrollableContainer?.getBoundingClientRect() || { top: 0 } const containerTop = scrollableContainer ? containerRect.top : 0 const newRect = referenceElement!.getBoundingClientRect() const newElementTop = newRect.top - containerTop // Calculate how much the reference element moved due to content being prepended const adjustment = newElementTop - referenceElementTop if (adjustment === 0) { // Try again on the next frame, as DOM may still be updating window.requestAnimationFrame(restore) return } // Adjust scroll position to compensate for the movement, keeping the reference element // in the same visual position as before the update if (scrollableContainer) { scrollableContainer.scrollTo({ top: currentScrollTop + adjustment }) } else { window.scrollTo(0, window.scrollY + adjustment) } restored = true } window.requestAnimationFrame(restore) } return { captureScrollPosition, restoreScrollPosition, } } return { createCallbacks, } } ================================================ FILE: packages/core/src/infiniteScroll.ts ================================================ import { requestAnimationFrame } from './domUtils' import { router } from './index' import { useInfiniteScrollData } from './infiniteScroll/data' import { useInfiniteScrollElementManager } from './infiniteScroll/elements' import { useInfiniteScrollQueryString } from './infiniteScroll/queryString' import { useInfiniteScrollPreservation } from './infiniteScroll/scrollPreservation' import { Page, ReloadOptions, UseInfiniteScrollOptions, UseInfiniteScrollProps } from './types' /** * Core infinite scroll composable that orchestrates data fetching, DOM management, * scroll preservation, and URL synchronization. * * This is the main entry point that coordinates four sub-systems: * - Data management: Handles pagination state and server requests * - Element management: DOM observation and intersection detection * - Query string sync: Updates URL as user scrolls through pages * - Scroll preservation: Maintains scroll position during content updates */ export default function useInfiniteScroll(options: UseInfiniteScrollOptions): UseInfiniteScrollProps { const queryStringManager = useInfiniteScrollQueryString({ ...options, getPageName: () => dataManager.getPageName() }) // Create scroll preservation callbacks that capture and restore scroll position // and restore it after new content is prepended to maintain visual stability const scrollPreservation = useInfiniteScrollPreservation(options) const elementManager = useInfiniteScrollElementManager({ ...options, // As items enter viewport, update URL to reflect the most visible page onItemIntersected: queryStringManager.onItemIntersected, onPreviousTriggered: () => dataManager.fetchPrevious(), onNextTriggered: () => dataManager.fetchNext(), }) const dataManager = useInfiniteScrollData({ ...options, // Before updating page data, tag any manually added DOM elements // so they don't get confused with server-loaded content onBeforeUpdate: elementManager.processManuallyAddedElements, // After successful request, tag new server content onCompletePreviousRequest: (loadedPage) => { options.onCompletePreviousRequest() requestAnimationFrame(() => elementManager.processServerLoadedElements(loadedPage), 2) }, onCompleteNextRequest: (loadedPage) => { options.onCompleteNextRequest() requestAnimationFrame(() => elementManager.processServerLoadedElements(loadedPage), 2) }, onReset: options.onDataReset, }) const addScrollPreservationCallbacks = (reloadOptions: ReloadOptions): ReloadOptions => { const { captureScrollPosition, restoreScrollPosition } = scrollPreservation.createCallbacks() const originalOnBeforeUpdate = reloadOptions.onBeforeUpdate || (() => {}) const originalOnSuccess = reloadOptions.onSuccess || (() => {}) reloadOptions.onBeforeUpdate = (page: Page) => { originalOnBeforeUpdate(page) captureScrollPosition() } reloadOptions.onSuccess = (page: Page) => { originalOnSuccess(page) restoreScrollPosition() } return reloadOptions } const originalFetchNext = dataManager.fetchNext dataManager.fetchNext = (reloadOptions: ReloadOptions = {}) => { if (options.inReverseMode()) { reloadOptions = addScrollPreservationCallbacks(reloadOptions) } originalFetchNext(reloadOptions) } const originalFetchPrevious = dataManager.fetchPrevious dataManager.fetchPrevious = (reloadOptions: ReloadOptions = {}) => { if (!options.inReverseMode()) { reloadOptions = addScrollPreservationCallbacks(reloadOptions) } originalFetchPrevious(reloadOptions) } const removeEventListener = router.on('success', () => requestAnimationFrame(elementManager.refreshTriggers, 2)) return { dataManager, elementManager, flush: () => { removeEventListener() dataManager.removeEventListener() elementManager.flushAll() queryStringManager.cancel() }, } } ================================================ FILE: packages/core/src/initialVisit.ts ================================================ import { eventHandler } from './eventHandler' import { fireFlashEvent, fireNavigateEvent } from './events' import { history } from './history' import { navigationType } from './navigationType' import { page as currentPage } from './page' import { Scroll } from './scroll' import { SessionStorage } from './sessionStorage' import { LocationVisit, Page } from './types' export class InitialVisit { public static handle(): void { this.clearRememberedStateOnReload() const scenarios = [this.handleBackForward, this.handleLocation, this.handleDefault] scenarios.find((handler) => handler.bind(this)()) } protected static clearRememberedStateOnReload(): void { if (navigationType.isReload()) { history.deleteState(history.rememberedState) history.clearInitialState(history.rememberedState) } } protected static handleBackForward(): boolean { if (!navigationType.isBackForward() || !history.browserHasHistoryEntry()) { return false } const scrollRegions = history.getScrollRegions() history .decrypt() .then((data) => { currentPage.set(data, { preserveScroll: true, preserveState: true }).then(() => { Scroll.restore(scrollRegions) fireNavigateEvent(currentPage.get()) }) }) .catch(() => { eventHandler.onMissingHistoryItem() }) return true } /** * @link https://inertiajs.com/redirects#external-redirects */ protected static handleLocation(): boolean { if (!SessionStorage.exists(SessionStorage.locationVisitKey)) { return false } const locationVisit: LocationVisit = SessionStorage.get(SessionStorage.locationVisitKey) || {} SessionStorage.remove(SessionStorage.locationVisitKey) if (typeof window !== 'undefined') { currentPage.setUrlHash(window.location.hash) } history .decrypt(currentPage.get()) .then(() => { const rememberedState = history.getState<Page['rememberedState']>(history.rememberedState, {}) const scrollRegions = history.getScrollRegions() currentPage.remember(rememberedState) currentPage .set(currentPage.get(), { preserveScroll: locationVisit.preserveScroll, preserveState: true, }) .then(() => { if (locationVisit.preserveScroll) { Scroll.restore(scrollRegions) } fireNavigateEvent(currentPage.get()) }) }) .catch(() => { eventHandler.onMissingHistoryItem() }) return true } protected static handleDefault(): void { if (typeof window !== 'undefined') { currentPage.setUrlHash(window.location.hash) } currentPage.set(currentPage.get(), { preserveScroll: true, preserveState: true }).then(() => { if (navigationType.isReload()) { Scroll.restore(history.getScrollRegions()) } else { Scroll.scrollToAnchor() } const page = currentPage.get() fireNavigateEvent(page) const flash = page.flash if (Object.keys(flash).length > 0) { queueMicrotask(() => fireFlashEvent(flash)) } }) } } ================================================ FILE: packages/core/src/intersectionObservers.ts ================================================ type IntersectionObserverCallback = (entry: IntersectionObserverEntry) => void interface IntersectionObserverManager { new: (callback: IntersectionObserverCallback, options?: IntersectionObserverInit) => IntersectionObserver flushAll: () => void } export const useIntersectionObservers = (): IntersectionObserverManager => { const intersectionObservers: IntersectionObserver[] = [] const newIntersectionObserver = ( callback: IntersectionObserverCallback, options: IntersectionObserverInit = {}, ): IntersectionObserver => { const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { callback(entry) } } }, options) intersectionObservers.push(observer) return observer } const flushAll = () => { intersectionObservers.forEach((observer) => observer.disconnect()) intersectionObservers.length = 0 } return { new: newIntersectionObserver, flushAll, } } ================================================ FILE: packages/core/src/modal.ts ================================================ export default { modal: null, listener: null, createIframeAndPage(html: Record<string, unknown> | string): { iframe: HTMLIFrameElement; page: HTMLElement } { if (typeof html === 'object') { html = `All Inertia requests must receive a valid Inertia response, however a plain JSON response was received.<hr>${JSON.stringify( html, )}` } const page = document.createElement('html') page.innerHTML = html page.querySelectorAll('a').forEach((a) => a.setAttribute('target', '_top')) const iframe = document.createElement('iframe') iframe.style.backgroundColor = 'white' iframe.style.borderRadius = '5px' iframe.style.width = '100%' iframe.style.height = '100%' return { iframe, page } }, show(html: Record<string, unknown> | string): void { const { iframe, page } = this.createIframeAndPage(html) this.modal = document.createElement('div') this.modal.style.position = 'fixed' this.modal.style.width = '100vw' this.modal.style.height = '100vh' this.modal.style.padding = '50px' this.modal.style.boxSizing = 'border-box' this.modal.style.backgroundColor = 'rgba(0, 0, 0, .6)' this.modal.style.zIndex = 200000 this.modal.addEventListener('click', () => this.hide()) this.modal.appendChild(iframe) document.body.prepend(this.modal) document.body.style.overflow = 'hidden' if (!iframe.contentWindow) { throw new Error('iframe not yet ready.') } iframe.contentWindow.document.open() iframe.contentWindow.document.write(page.outerHTML) iframe.contentWindow.document.close() this.listener = this.hideOnEscape.bind(this) document.addEventListener('keydown', this.listener) }, hide(): void { this.modal.outerHTML = '' this.modal = null document.body.style.overflow = 'visible' document.removeEventListener('keydown', this.listener) }, hideOnEscape(event: KeyboardEvent): void { if (event.keyCode === 27) { this.hide() } }, } ================================================ FILE: packages/core/src/navigationEvents.ts ================================================ type MouseNavigationEvent = Pick< MouseEvent, 'altKey' | 'ctrlKey' | 'shiftKey' | 'metaKey' | 'button' | 'currentTarget' | 'defaultPrevented' | 'target' > type KeyboardNavigationEvent = Pick<KeyboardEvent, 'currentTarget' | 'defaultPrevented' | 'key' | 'target'> function isContentEditableOrPrevented(event: KeyboardNavigationEvent | MouseNavigationEvent): boolean { return (event.target instanceof HTMLElement && event.target.isContentEditable) || event.defaultPrevented } /** * Determine if this mouse event should be intercepted for navigation purposes. * Links with modifier keys or non-left clicks should not be intercepted. * Content editable elements and prevented events are ignored. */ export function shouldIntercept(event: MouseNavigationEvent): boolean { const isLink = (event.currentTarget as HTMLElement).tagName.toLowerCase() === 'a' return !( isContentEditableOrPrevented(event) || (isLink && event.altKey) || (isLink && event.ctrlKey) || (isLink && event.metaKey) || (isLink && event.shiftKey) || (isLink && 'button' in event && event.button !== 0) ) } /** * Determine if this keyboard event should trigger a navigation request. * Enter triggers navigation for both links and buttons currently. * Space only triggers navigation for buttons specifically. */ export function shouldNavigate(event: KeyboardNavigationEvent): boolean { const isButton = (event.currentTarget as HTMLElement).tagName.toLowerCase() === 'button' return !isContentEditableOrPrevented(event) && (event.key === 'Enter' || (isButton && event.key === ' ')) } ================================================ FILE: packages/core/src/navigationType.ts ================================================ class NavigationType { protected type: NavigationTimingType public constructor() { this.type = this.resolveType() } protected resolveType(): NavigationTimingType { if (typeof window === 'undefined') { return 'navigate' } if ( window.performance && window.performance.getEntriesByType && window.performance.getEntriesByType('navigation').length > 0 ) { return (window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming).type } return 'navigate' } public get(): NavigationTimingType { return this.type } public isBackForward(): boolean { return this.type === 'back_forward' } public isReload(): boolean { return this.type === 'reload' } } export const navigationType = new NavigationType() ================================================ FILE: packages/core/src/objectUtils.ts ================================================ export const objectsAreEqual = <T extends Record<string, any>>( obj1: T, obj2: T, excludeKeys: { [K in keyof T]: K }[keyof T][], ): boolean => { if (obj1 === obj2) { return true } // Check keys in obj1 for (const key in obj1) { if (excludeKeys.includes(key)) { continue } if (obj1[key] === obj2[key]) { continue } if (!compareValues(obj1[key], obj2[key])) { return false } } // Check keys that exist in obj2 but not in obj1 for (const key in obj2) { if (excludeKeys.includes(key)) { continue } if (!(key in obj1)) { return false } } return true } const compareValues = (value1: any, value2: any): boolean => { switch (typeof value1) { case 'object': return objectsAreEqual(value1, value2, []) case 'function': return value1.toString() === value2.toString() default: return value1 === value2 } } ================================================ FILE: packages/core/src/page.ts ================================================ import { eventHandler } from './eventHandler' import { fireNavigateEvent } from './events' import { history } from './history' import { prefetchedRequests } from './prefetched' import { Scroll } from './scroll' import { Component, FlashData, Page, PageEvent, PageHandler, PageResolver, RouterInitParams, Visit } from './types' import { hrefToUrl, isSameUrlWithoutHash } from './url' class CurrentPage { protected page!: Page protected swapComponent!: PageHandler<any> protected resolveComponent!: PageResolver protected onFlashCallback?: (flash: Page['flash']) => void protected componentId = {} protected listeners: { event: PageEvent callback: VoidFunction }[] = [] protected isFirstPageLoad = true protected cleared = false protected pendingDeferredProps: Pick<Page, 'deferredProps' | 'url' | 'component'> | null = null protected historyQuotaExceeded = false public init<ComponentType = Component>({ initialPage, swapComponent, resolveComponent, onFlash, }: RouterInitParams<ComponentType>) { this.page = { ...initialPage, flash: initialPage.flash ?? {} } this.swapComponent = swapComponent this.resolveComponent = resolveComponent this.onFlashCallback = onFlash eventHandler.on('historyQuotaExceeded', () => { this.historyQuotaExceeded = true }) return this } public set( page: Page, { replace = false, preserveScroll = false, preserveState = false, viewTransition = false, }: { replace?: boolean preserveScroll?: boolean preserveState?: boolean viewTransition?: Visit['viewTransition'] } = {}, ): Promise<void> { if (Object.keys(page.deferredProps || {}).length) { this.pendingDeferredProps = { deferredProps: page.deferredProps, component: page.component, url: page.url, } // Preserve original deferred props for back button handling if (page.initialDeferredProps === undefined) { page.initialDeferredProps = page.deferredProps } } this.componentId = {} const componentId = this.componentId if (page.clearHistory) { history.clear() } return this.resolve(page.component).then((component) => { if (componentId !== this.componentId) { // Component has changed since we started resolving this component, bail return } page.rememberedState ??= {} const isServer = typeof window === 'undefined' const location = !isServer ? window.location : new URL(page.url) const scrollRegions = !isServer && preserveScroll ? Scroll.getScrollRegions() : [] replace = replace || isSameUrlWithoutHash(hrefToUrl(page.url), location) // Clear flash data from the page object, we don't want it when navigating back/forward... const pageForHistory = { ...page, flash: {} } return new Promise<void>((resolve) => replace ? history.replaceState(pageForHistory, resolve) : history.pushState(pageForHistory, resolve), ).then(() => { const isNewComponent = !this.isTheSame(page) if (!isNewComponent && Object.keys(page.props.errors || {}).length > 0) { // Don't use view transition if the page stays the same and there are (new) errors... viewTransition = false } this.page = page this.cleared = false if (this.hasOnceProps()) { prefetchedRequests.updateCachedOncePropsFromCurrentPage() } if (isNewComponent) { this.fireEventsFor('newComponent') } if (this.isFirstPageLoad) { this.fireEventsFor('firstLoad') } this.isFirstPageLoad = false if (this.historyQuotaExceeded) { // If we exceeded the history quota, don't attempt to swap the // component as we're performing a full page reload instead. this.historyQuotaExceeded = false return } return this.swap({ component, page, preserveState, viewTransition, }).then(() => { if (preserveScroll) { // Scroll regions must be explicitly restored since the DOM elements are destroyed // and recreated during the component 'swap'. Document scroll is naturally // preserved as the document element itself persists across navigations. window.requestAnimationFrame(() => Scroll.restoreScrollRegions(scrollRegions)) } else { Scroll.reset() } if ( this.pendingDeferredProps && this.pendingDeferredProps.component === page.component && this.pendingDeferredProps.url === page.url ) { eventHandler.fireInternalEvent('loadDeferredProps', this.pendingDeferredProps.deferredProps) } this.pendingDeferredProps = null if (!replace) { fireNavigateEvent(page) } }) }) }) } public setQuietly( page: Page, { preserveState = false, }: { preserveState?: boolean } = {}, ) { return this.resolve(page.component).then((component) => { this.page = page this.cleared = false history.setCurrent(page) return this.swap({ component, page, preserveState, viewTransition: false }) }) } public clear(): void { this.cleared = true } public isCleared(): boolean { return this.cleared } public get(): Page { return this.page } public getWithoutFlashData(): Page { return { ...this.page, flash: {} } } public hasOnceProps(): boolean { return Object.keys(this.page.onceProps ?? {}).length > 0 } public merge(data: Partial<Page>): void { this.page = { ...this.page, ...data } } public setFlash(flash: FlashData): void { this.page = { ...this.page, flash } this.onFlashCallback?.(flash) } public setUrlHash(hash: string): void { if (!this.page.url.includes(hash)) { this.page.url += hash } } public remember(data: Page['rememberedState']): void { this.page.rememberedState = data } public swap({ component, page, preserveState, viewTransition, }: { component: Component page: Page preserveState: boolean viewTransition: Visit['viewTransition'] }): Promise<unknown> { const doSwap = () => this.swapComponent({ component, page, preserveState }) if (!viewTransition || !document?.startViewTransition || document.visibilityState === 'hidden') { return doSwap() } const viewTransitionCallback = typeof viewTransition === 'boolean' ? () => null : viewTransition return new Promise((resolve) => { const transitionResult = document.startViewTransition(() => doSwap().then(resolve)) viewTransitionCallback(transitionResult) }) } public resolve(component: string): Promise<Component> { return Promise.resolve(this.resolveComponent(component)) } public isTheSame(page: Page): boolean { return this.page.component === page.component } public on(event: PageEvent, callback: VoidFunction): VoidFunction { this.listeners.push({ event, callback }) return () => { this.listeners = this.listeners.filter((listener) => listener.event !== event && listener.callback !== callback) } } public fireEventsFor(event: PageEvent): void { this.listeners.filter((listener) => listener.event === event).forEach((listener) => listener.callback()) } public mergeOncePropsIntoResponse(response: Page, { force = false }: { force?: boolean } = {}): void { Object.entries(response.onceProps ?? {}).forEach(([key, onceProp]) => { const existingOnceProp = this.page.onceProps?.[key] if (existingOnceProp === undefined) { return } if (force || response.props[onceProp.prop] === undefined) { response.props[onceProp.prop] = this.page.props[existingOnceProp.prop] response.onceProps![key].expiresAt = existingOnceProp.expiresAt } }) } } export const page = new CurrentPage() ================================================ FILE: packages/core/src/poll.ts ================================================ import { PollOptions } from './types' export class Poll { protected id: number | null = null protected throttle = false protected keepAlive = false protected cb: VoidFunction protected interval: number protected cbCount = 0 constructor(interval: number, cb: VoidFunction, options: PollOptions) { this.keepAlive = options.keepAlive ?? false this.cb = cb this.interval = interval if (options.autoStart ?? true) { this.start() } } public stop() { // console.log('stopping...', this.id) if (this.id) { // console.log('clearing interval...') clearInterval(this.id) } } public start() { if (typeof window === 'undefined') { return } this.stop() this.id = window.setInterval(() => { if (!this.throttle || this.cbCount % 10 === 0) { this.cb() } if (this.throttle) { this.cbCount++ } }, this.interval) } public isInBackground(hidden: boolean) { this.throttle = this.keepAlive ? false : hidden if (this.throttle) { this.cbCount = 0 } } } ================================================ FILE: packages/core/src/polls.ts ================================================ import { Poll } from './poll' import { PollOptions } from './types' class Polls { protected polls: Poll[] = [] constructor() { this.setupVisibilityListener() } public add( interval: number, cb: VoidFunction, options: PollOptions, ): { stop: VoidFunction start: VoidFunction } { const poll = new Poll(interval, cb, options) this.polls.push(poll) return { stop: () => poll.stop(), start: () => poll.start(), } } public clear() { this.polls.forEach((poll) => poll.stop()) this.polls = [] } protected setupVisibilityListener() { if (typeof document === 'undefined') { return } document.addEventListener( 'visibilitychange', () => { this.polls.forEach((poll) => poll.isInBackground(document.hidden)) }, false, ) } } export const polls = new Polls() ================================================ FILE: packages/core/src/prefetched.ts ================================================ import { cloneDeep } from 'lodash-es' import { objectsAreEqual } from './objectUtils' import { page as currentPage } from './page' import { Response } from './response' import { timeToMs } from './time' import { ActiveVisit, CacheForOption, InFlightPrefetch, InternalActiveVisit, Page, PrefetchedResponse, PrefetchOptions, PrefetchRemovalTimer, } from './types' class PrefetchedRequests { protected cached: PrefetchedResponse[] = [] protected inFlightRequests: InFlightPrefetch[] = [] protected removalTimers: PrefetchRemovalTimer[] = [] protected currentUseId: string | null = null public add( params: ActiveVisit, sendFunc: (params: InternalActiveVisit) => void, { cacheFor, cacheTags }: PrefetchOptions, ) { const inFlight = this.findInFlight(params) if (inFlight) { return Promise.resolve() } const existing = this.findCached(params) if (!params.fresh && existing && existing.staleTimestamp > Date.now()) { return Promise.resolve() } const [stale, prefetchExpiresIn] = this.extractStaleValues(cacheFor) const promise = new Promise<Response>((resolve, reject) => { sendFunc({ ...params, onCancel: () => { this.remove(params) params.onCancel() reject() }, onError: (error) => { this.remove(params) params.onError(error) reject() }, onPrefetching(visitParams) { params.onPrefetching(visitParams) }, onPrefetched(response, visit) { params.onPrefetched(response, visit) }, onPrefetchResponse(response) { resolve(response) }, onPrefetchError(error) { prefetchedRequests.removeFromInFlight(params) reject(error) }, }) }).then((response) => { this.remove(params) const pageResponse = response.getPageResponse() currentPage.mergeOncePropsIntoResponse(pageResponse) this.cached.push({ params: { ...params }, staleTimestamp: Date.now() + stale, expiresAt: Date.now() + prefetchExpiresIn, response: promise, singleUse: prefetchExpiresIn === 0, timestamp: Date.now(), inFlight: false, tags: Array.isArray(cacheTags) ? cacheTags : [cacheTags], }) const oncePropExpiresIn = this.getShortestOncePropTtl(pageResponse) this.scheduleForRemoval( params, oncePropExpiresIn ? Math.min(prefetchExpiresIn, oncePropExpiresIn) : prefetchExpiresIn, ) this.removeFromInFlight(params) response.handlePrefetch() return response }) this.inFlightRequests.push({ params: { ...params }, response: promise, staleTimestamp: null, inFlight: true, }) return promise } public removeAll(): void { this.cached = [] this.removalTimers.forEach((removalTimer) => { clearTimeout(removalTimer.timer) }) this.removalTimers = [] } public removeByTags(tags: string[]): void { this.cached = this.cached.filter((prefetched) => { return !prefetched.tags.some((tag) => tags.includes(tag)) }) } public remove(params: ActiveVisit): void { this.cached = this.cached.filter((prefetched) => { return !this.paramsAreEqual(prefetched.params, params) }) this.clearTimer(params) } protected removeFromInFlight(params: ActiveVisit): void { this.inFlightRequests = this.inFlightRequests.filter((prefetching) => { return !this.paramsAreEqual(prefetching.params, params) }) } protected extractStaleValues(cacheFor: PrefetchOptions['cacheFor']): [number, number] { const [stale, expires] = this.cacheForToStaleAndExpires(cacheFor) return [timeToMs(stale), timeToMs(expires)] } protected cacheForToStaleAndExpires(cacheFor: PrefetchOptions['cacheFor']): [CacheForOption, CacheForOption] { if (!Array.isArray(cacheFor)) { return [cacheFor, cacheFor] } switch (cacheFor.length) { case 0: return [0, 0] case 1: return [cacheFor[0], cacheFor[0]] default: return [cacheFor[0], cacheFor[1]] } } protected clearTimer(params: ActiveVisit) { const timer = this.removalTimers.find((removalTimer) => { return this.paramsAreEqual(removalTimer.params, params) }) if (timer) { clearTimeout(timer.timer) this.removalTimers = this.removalTimers.filter((removalTimer) => removalTimer !== timer) } } protected scheduleForRemoval(params: ActiveVisit, expiresIn: number) { if (typeof window === 'undefined') { return } this.clearTimer(params) if (expiresIn > 0) { const timer = window.setTimeout(() => this.remove(params), expiresIn) this.removalTimers.push({ params, timer, }) } } public get(params: ActiveVisit): InFlightPrefetch | PrefetchedResponse | null { return this.findCached(params) || this.findInFlight(params) } public use(prefetched: PrefetchedResponse | InFlightPrefetch, params: ActiveVisit) { const id = `${params.url.pathname}-${Date.now()}-${Math.random().toString(36).substring(7)}` this.currentUseId = id return prefetched.response.then((response) => { if (this.currentUseId !== id) { // They've since gone on to `use` a different request, // so we should ignore this one return } response.mergeParams({ ...params, onPrefetched: () => {} }) // If this was a one-time cache, remove it // (generally a prefetch="click" request with no specified cache value) this.removeSingleUseItems(params) return response.handle() }) } protected removeSingleUseItems(params: ActiveVisit) { this.cached = this.cached.filter((prefetched) => { if (!this.paramsAreEqual(prefetched.params, params)) { return true } return !prefetched.singleUse }) } public findCached(params: ActiveVisit): PrefetchedResponse | null { return ( this.cached.find((prefetched) => { return this.paramsAreEqual(prefetched.params, params) }) || null ) } public findInFlight(params: ActiveVisit): InFlightPrefetch | null { return ( this.inFlightRequests.find((prefetched) => { return this.paramsAreEqual(prefetched.params, params) }) || null ) } protected withoutPurposePrefetchHeader(params: ActiveVisit): ActiveVisit { const newParams = cloneDeep(params) if (newParams.headers['Purpose'] === 'prefetch') { delete newParams.headers['Purpose'] } return newParams } protected paramsAreEqual(params1: ActiveVisit, params2: ActiveVisit): boolean { return objectsAreEqual<ActiveVisit>( this.withoutPurposePrefetchHeader(params1), this.withoutPurposePrefetchHeader(params2), [ 'showProgress', 'replace', 'prefetch', 'preserveScroll', 'preserveState', 'onBefore', 'onBeforeUpdate', 'onStart', 'onProgress', 'onFinish', 'onCancel', 'onSuccess', 'onError', 'onFlash', 'onPrefetched', 'onCancelToken', 'onPrefetching', 'async', 'viewTransition', ], ) } public updateCachedOncePropsFromCurrentPage(): void { this.cached.forEach((prefetched) => { prefetched.response.then((response) => { const pageResponse = response.getPageResponse() currentPage.mergeOncePropsIntoResponse(pageResponse, { force: true }) for (const [group, deferredProps] of Object.entries(pageResponse.deferredProps ?? {})) { const remaining = deferredProps.filter((prop) => pageResponse.props[prop] === undefined) if (remaining.length > 0) { pageResponse.deferredProps![group] = remaining } else { delete pageResponse.deferredProps![group] } } const oncePropExpiresIn = this.getShortestOncePropTtl(pageResponse) if (oncePropExpiresIn === null) { return } const prefetchExpiresIn = prefetched.expiresAt - Date.now() const expiresIn = Math.min(prefetchExpiresIn, oncePropExpiresIn) if (expiresIn > 0) { this.scheduleForRemoval(prefetched.params, expiresIn) } else { this.remove(prefetched.params) } }) }) } protected getShortestOncePropTtl(page: Page): number | null { const expiryTimestamps = Object.values(page.onceProps ?? {}) .map((onceProp) => onceProp.expiresAt) .filter((expiresAt): expiresAt is number => !!expiresAt) if (expiryTimestamps.length === 0) { return null } return Math.min(...expiryTimestamps) - Date.now() } } export const prefetchedRequests = new PrefetchedRequests() ================================================ FILE: packages/core/src/progress-component.ts ================================================ /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress * @license MIT */ import { ProgressSettings } from './types' const baseComponentSelector = 'nprogress' let progress: HTMLDivElement const settings: ProgressSettings = { minimum: 0.08, easing: 'linear', positionUsing: 'translate3d', speed: 200, trickle: true, trickleSpeed: 200, showSpinner: true, barSelector: '[role="bar"]', spinnerSelector: '[role="spinner"]', parent: 'body', color: '#29d', includeCSS: true, template: [ '<div class="bar" role="bar">', '<div class="peg"></div>', '</div>', '<div class="spinner" role="spinner">', '<div class="spinner-icon"></div>', '</div>', ].join(''), } let status: number | null = null const configure = (options: Partial<ProgressSettings>) => { Object.assign(settings, options) if (settings.includeCSS) { injectCSS(settings.color) } progress = document.createElement('div') progress.id = baseComponentSelector progress.innerHTML = settings.template } /** * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. */ const set = (n: number) => { const started = isStarted() n = clamp(n, settings.minimum, 1) status = n === 1 ? null : n const progress = render(!started) const bar = progress.querySelector(settings.barSelector)! as HTMLElement const speed = settings.speed const ease = settings.easing progress.offsetWidth /* Repaint */ queue((next) => { const barStyles = ((): Partial<CSSStyleDeclaration> => { if (settings.positionUsing === 'translate3d') { return { transition: `all ${speed}ms ${ease}`, transform: `translate3d(${toBarPercentage(n)}%,0,0)`, } } if (settings.positionUsing === 'translate') { return { transition: `all ${speed}ms ${ease}`, transform: `translate(${toBarPercentage(n)}%,0)`, } } return { marginLeft: `${toBarPercentage(n)}%` } })() for (const key in barStyles) { bar.style[key] = barStyles[key]! } if (n !== 1) { return setTimeout(next, speed) } // Fade out progress.style.transition = 'none' progress.style.opacity = '1' progress.offsetWidth /* Repaint */ setTimeout(() => { progress.style.transition = `all ${speed}ms linear` progress.style.opacity = '0' setTimeout(() => { remove() progress.style.transition = '' progress.style.opacity = '' next() }, speed) }, speed) }) } const isStarted = () => typeof status === 'number' /** * Shows the progress bar. * This is the same as setting the status to 0%, except that it doesn't go backwards. */ const start = () => { if (!status) { set(0) } const work = function () { setTimeout(function () { if (!status) { return } increaseByRandom() work() }, settings.trickleSpeed) } if (settings.trickle) { work() } } /** * Hides the progress bar. * This is the *sort of* the same as setting the status to 100%, with the * difference being `done()` makes some placebo effect of some realistic motion. * * If `true` is passed, it will show the progress bar even if it's hidden. */ const done = (force?: boolean) => { if (!force && !status) { return } increaseByRandom(0.3 + 0.5 * Math.random()) set(1) } const increaseByRandom = (amount?: number) => { const n = status if (n === null) { return start() } if (n > 1) { return } amount = typeof amount === 'number' ? amount : (() => { const ranges: Record<number, [number, number]> = { 0.1: [0, 0.2], 0.04: [0.2, 0.5], 0.02: [0.5, 0.8], 0.005: [0.8, 0.99], } for (const r in ranges) { if (n >= ranges[r][0] && n < ranges[r][1]) { return parseFloat(r) } } return 0 })() return set(clamp(n + amount, 0, 0.994)) } /** * (Internal) renders the progress bar markup based on the `template` setting. */ const render = (fromStart: boolean) => { if (isRendered()) { return document.getElementById(baseComponentSelector)! } document.documentElement.classList.add(`${baseComponentSelector}-busy`) const bar = progress.querySelector(settings.barSelector)! as HTMLElement const perc = fromStart ? '-100' : toBarPercentage(status || 0) const parent = getParent() bar.style.transition = 'all 0 linear' bar.style.transform = `translate3d(${perc}%,0,0)` if (!settings.showSpinner) { progress.querySelector(settings.spinnerSelector)?.remove() } if (parent !== document.body) { parent.classList.add(`${baseComponentSelector}-custom-parent`) } parent.appendChild(progress) return progress } const getParent = (): HTMLElement => { return (isDOM(settings.parent) ? settings.parent : document.querySelector(settings.parent)) as HTMLElement } const remove = () => { document.documentElement.classList.remove(`${baseComponentSelector}-busy`) getParent().classList.remove(`${baseComponentSelector}-custom-parent`) progress?.remove() } const isRendered = () => { return document.getElementById(baseComponentSelector) !== null } const isDOM = (obj: any) => { if (typeof HTMLElement === 'object') { return obj instanceof HTMLElement } return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string' } function clamp(n: number, min: number, max: number): number { if (n < min) { return min } if (n > max) { return max } return n } // Converts a percentage (`0..1`) to a bar translateX percentage (`-100%..0%`). const toBarPercentage = (n: number) => (-1 + n) * 100 // Queues a function to be executed. const queue = (() => { const pending: ((...args: any[]) => any)[] = [] const next = () => { const fn = pending.shift() if (fn) { fn(next) } } return (fn: (...args: any[]) => any) => { pending.push(fn) if (pending.length === 1) { next() } } })() const injectCSS = (color: string): void => { const element = document.createElement('style') element.textContent = ` #${baseComponentSelector} { pointer-events: none; } #${baseComponentSelector} .bar { background: ${color}; position: fixed; z-index: 1031; top: 0; left: 0; width: 100%; height: 2px; } #${baseComponentSelector} .peg { display: block; position: absolute; right: 0px; width: 100px; height: 100%; box-shadow: 0 0 10px ${color}, 0 0 5px ${color}; opacity: 1.0; transform: rotate(3deg) translate(0px, -4px); } #${baseComponentSelector} .spinner { display: block; position: fixed; z-index: 1031; top: 15px; right: 15px; } #${baseComponentSelector} .spinner-icon { width: 18px; height: 18px; box-sizing: border-box; border: solid 2px transparent; border-top-color: ${color}; border-left-color: ${color}; border-radius: 50%; animation: ${baseComponentSelector}-spinner 400ms linear infinite; } .${baseComponentSelector}-custom-parent { overflow: hidden; position: relative; } .${baseComponentSelector}-custom-parent #${baseComponentSelector} .spinner, .${baseComponentSelector}-custom-parent #${baseComponentSelector} .bar { position: absolute; } @keyframes ${baseComponentSelector}-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ` document.head.appendChild(element) } const show = () => { if (progress) { progress.style.display = '' } } const hide = () => { if (progress) { progress.style.display = 'none' } } export default { configure, isStarted, done, set, remove, start, status, show, hide, } ================================================ FILE: packages/core/src/progress.ts ================================================ import ProgressComponent from './progress-component' import { GlobalEvent } from './types' class Progress { public hideCount = 0 public start(): void { ProgressComponent.start() } public reveal(force: boolean = false): void { this.hideCount = Math.max(0, this.hideCount - 1) if (force || this.hideCount === 0) { ProgressComponent.show() } } public hide(): void { this.hideCount++ ProgressComponent.hide() } public set(status: number): void { ProgressComponent.set(Math.max(0, Math.min(1, status))) } public finish(): void { ProgressComponent.done() } public reset(): void { ProgressComponent.set(0) } public remove(): void { ProgressComponent.done() ProgressComponent.remove() } public isStarted(): boolean { return ProgressComponent.isStarted() } public getStatus(): number | null { return ProgressComponent.status } } export const progress = new Progress() export const reveal = progress.reveal export const hide = progress.hide function addEventListeners(delay: number): void { document.addEventListener('inertia:start', (e) => handleStartEvent(e, delay)) document.addEventListener('inertia:progress', handleProgressEvent) } function handleStartEvent(event: GlobalEvent<'start'>, delay: number): void { if (!event.detail.visit.showProgress) { progress.hide() } const timeout = setTimeout(() => progress.start(), delay) document.addEventListener('inertia:finish', (e) => finish(e, timeout), { once: true }) } function handleProgressEvent(event: GlobalEvent<'progress'>): void { if (progress.isStarted() && event.detail.progress?.percentage) { progress.set(Math.max(progress.getStatus()!, (event.detail.progress.percentage / 100) * 0.9)) } } function finish(event: GlobalEvent<'finish'>, timeout: NodeJS.Timeout): void { clearTimeout(timeout!) if (!progress.isStarted()) { return } if (event.detail.visit.completed) { progress.finish() } else if (event.detail.visit.interrupted) { progress.reset() } else if (event.detail.visit.cancelled) { progress.remove() } } export default function setupProgress({ delay = 250, color = '#29d', includeCSS = true, showSpinner = false, } = {}): void { addEventListeners(delay) ProgressComponent.configure({ showSpinner, includeCSS, color }) } ================================================ FILE: packages/core/src/queue.ts ================================================ export default class Queue<T> { protected items: (() => T)[] = [] protected processingPromise: Promise<void> | null = null public add(item: () => T) { this.items.push(item) return this.process() } public process() { this.processingPromise ??= this.processNext().finally(() => { this.processingPromise = null }) return this.processingPromise } protected processNext(): Promise<void> { const next = this.items.shift() if (next) { return Promise.resolve(next()).then(() => this.processNext()) } return Promise.resolve() } } ================================================ FILE: packages/core/src/request.ts ================================================ import { type AxiosProgressEvent, type AxiosRequestConfig, default as axios } from 'axios' import { fireExceptionEvent, fireFinishEvent, firePrefetchingEvent, fireProgressEvent, fireStartEvent } from './events' import { page as currentPage } from './page' import { RequestParams } from './requestParams' import { Response } from './response' import type { ActiveVisit, Page } from './types' import { urlWithoutHash } from './url' export class Request { protected response!: Response protected cancelToken!: AbortController protected requestParams: RequestParams protected requestHasFinished = false constructor( params: ActiveVisit, protected page: Page, ) { this.requestParams = RequestParams.create(params) this.cancelToken = new AbortController() } public static create(params: ActiveVisit, page: Page): Request { return new Request(params, page) } public isPrefetch(): boolean { return this.requestParams.isPrefetch() } public async send() { this.requestParams.onCancelToken(() => this.cancel({ cancelled: true })) fireStartEvent(this.requestParams.all()) this.requestParams.onStart() if (this.requestParams.all().prefetch) { this.requestParams.onPrefetching() firePrefetchingEvent(this.requestParams.all()) } // We capture this up here because the response // will clear the prefetch flag so it can use it // as a regular response once the prefetch is done const originallyPrefetch = this.requestParams.all().prefetch return axios({ method: this.requestParams.all().method, url: urlWithoutHash(this.requestParams.all().url).href, data: this.requestParams.data(), params: this.requestParams.queryParams(), signal: this.cancelToken.signal, headers: this.getHeaders(), onUploadProgress: this.onProgress.bind(this), // Why text? This allows us to delay JSON.parse until we're ready to use the response, // helps with performance particularly on large responses + history encryption responseType: 'text', }) .then((response) => { this.response = Response.create(this.requestParams, response, this.page) return this.response.handle() }) .catch((error) => { if (error?.response) { this.response = Response.create(this.requestParams, error.response, this.page) return this.response.handle() } return Promise.reject(error) }) .catch((error) => { if (axios.isCancel(error)) { return } if (fireExceptionEvent(error)) { if (originallyPrefetch) { this.requestParams.onPrefetchError(error) } return Promise.reject(error) } }) .finally(() => { this.finish() if (originallyPrefetch && this.response) { this.requestParams.onPrefetchResponse(this.response) } }) } protected finish(): void { if (this.requestParams.wasCancelledAtAll()) { return } this.requestParams.markAsFinished() this.fireFinishEvents() } protected fireFinishEvents(): void { if (this.requestHasFinished) { // This could be called from multiple places, don't let it re-fire return } this.requestHasFinished = true fireFinishEvent(this.requestParams.all()) this.requestParams.onFinish() } public cancel({ cancelled = false, interrupted = false }: { cancelled?: boolean; interrupted?: boolean }): void { if (this.requestHasFinished) { // If the request has already finished, there's no need to cancel it return } this.cancelToken.abort() this.requestParams.markAsCancelled({ cancelled, interrupted }) this.fireFinishEvents() } protected onProgress(progress: AxiosProgressEvent): void { if (this.requestParams.data() instanceof FormData) { progress.percentage = progress.progress ? Math.round(progress.progress * 100) : 0 fireProgressEvent(progress) this.requestParams.all().onProgress(progress) } } protected getHeaders(): AxiosRequestConfig['headers'] { const headers: AxiosRequestConfig['headers'] = { ...this.requestParams.headers(), Accept: 'text/html, application/xhtml+xml', 'X-Requested-With': 'XMLHttpRequest', 'X-Inertia': true, } const page = currentPage.get() if (page.version) { headers['X-Inertia-Version'] = page.version } const onceProps = Object.entries(page.onceProps || {}) .filter(([, onceProp]) => { if (page.props[onceProp.prop] === undefined) { // The prop could deferred and not be loaded yet return false } return !onceProp.expiresAt || onceProp.expiresAt > Date.now() }) .map(([key]) => key) if (onceProps.length > 0) { headers['X-Inertia-Except-Once-Props'] = onceProps.join(',') } return headers } } ================================================ FILE: packages/core/src/requestParams.ts ================================================ import { AxiosRequestConfig } from 'axios' import { page as currentPage } from './page' import { Response } from './response' import { ActiveVisit, InternalActiveVisit, Page, PreserveStateOption, VisitCallbacks } from './types' export class RequestParams { protected callbacks: { name: keyof VisitCallbacks args: any[] }[] = [] protected params: InternalActiveVisit constructor(params: InternalActiveVisit) { if (!params.prefetch) { this.params = params } else { const wrappedCallbacks: Record<keyof VisitCallbacks, () => any> = { onBefore: this.wrapCallback(params, 'onBefore'), onBeforeUpdate: this.wrapCallback(params, 'onBeforeUpdate'), onStart: this.wrapCallback(params, 'onStart'), onProgress: this.wrapCallback(params, 'onProgress'), onFinish: this.wrapCallback(params, 'onFinish'), onCancel: this.wrapCallback(params, 'onCancel'), onSuccess: this.wrapCallback(params, 'onSuccess'), onError: this.wrapCallback(params, 'onError'), onFlash: this.wrapCallback(params, 'onFlash'), onCancelToken: this.wrapCallback(params, 'onCancelToken'), onPrefetched: this.wrapCallback(params, 'onPrefetched'), onPrefetching: this.wrapCallback(params, 'onPrefetching'), } this.params = { ...params, ...wrappedCallbacks, onPrefetchResponse: params.onPrefetchResponse || (() => {}), onPrefetchError: params.onPrefetchError || (() => {}), } } // } public static create(params: ActiveVisit): RequestParams { return new RequestParams(params) } public data() { return this.params.method === 'get' ? null : this.params.data } public queryParams() { return this.params.method === 'get' ? this.params.data : {} } public isPartial() { return this.params.only.length > 0 || this.params.except.length > 0 || this.params.reset.length > 0 } public isPrefetch(): boolean { return this.params.prefetch === true } public isDeferredPropsRequest() { return this.params.deferredProps === true } public onCancelToken(cb: VoidFunction) { this.params.onCancelToken({ cancel: cb, }) } public markAsFinished() { this.params.completed = true this.params.cancelled = false this.params.interrupted = false } public markAsCancelled({ cancelled = true, interrupted = false }) { this.params.onCancel() this.params.completed = false this.params.cancelled = cancelled this.params.interrupted = interrupted } public wasCancelledAtAll() { return this.params.cancelled || this.params.interrupted } public onFinish() { this.params.onFinish(this.params) } public onStart() { this.params.onStart(this.params) } public onPrefetching() { this.params.onPrefetching(this.params) } public onPrefetchResponse(response: Response) { if (this.params.onPrefetchResponse) { this.params.onPrefetchResponse(response) } } public onPrefetchError(error: Error) { if (this.params.onPrefetchError) { this.params.onPrefetchError(error) } } public all() { return this.params } public headers(): AxiosRequestConfig['headers'] { const headers: AxiosRequestConfig['headers'] = { ...this.params.headers, } if (this.isPartial()) { headers['X-Inertia-Partial-Component'] = currentPage.get().component } const only = this.params.only.concat(this.params.reset) if (only.length > 0) { headers['X-Inertia-Partial-Data'] = only.join(',') } if (this.params.except.length > 0) { headers['X-Inertia-Partial-Except'] = this.params.except.join(',') } if (this.params.reset.length > 0) { headers['X-Inertia-Reset'] = this.params.reset.join(',') } if (this.params.errorBag && this.params.errorBag.length > 0) { headers['X-Inertia-Error-Bag'] = this.params.errorBag } return headers } public setPreserveOptions(page: Page) { this.params.preserveScroll = RequestParams.resolvePreserveOption(this.params.preserveScroll, page) this.params.preserveState = RequestParams.resolvePreserveOption(this.params.preserveState, page) } public runCallbacks() { this.callbacks.forEach(({ name, args }) => { // @ts-ignore this.params[name](...args) }) } public merge(toMerge: Partial<ActiveVisit>) { this.params = { ...this.params, ...toMerge, } } protected wrapCallback(params: ActiveVisit, name: keyof VisitCallbacks) { // @ts-ignore return (...args) => { this.recordCallback(name, args) // @ts-ignore params[name](...args) } } protected recordCallback(name: keyof VisitCallbacks, args: any[]) { this.callbacks.push({ name, args }) } public static resolvePreserveOption(value: PreserveStateOption, page: Page): boolean { if (typeof value === 'function') { return value(page) } if (value === 'errors') { return Object.keys(page.props.errors || {}).length > 0 } return value } } ================================================ FILE: packages/core/src/requestStream.ts ================================================ import { Request } from './request' export class RequestStream { protected requests: Request[] = [] protected maxConcurrent: number protected interruptible: boolean constructor({ maxConcurrent, interruptible }: { maxConcurrent: number; interruptible: boolean }) { this.maxConcurrent = maxConcurrent this.interruptible = interruptible } public send(request: Request) { this.requests.push(request) request.send().finally(() => { this.requests = this.requests.filter((r) => r !== request) }) } public interruptInFlight(): void { this.cancel({ interrupted: true }, false) } public cancelInFlight({ prefetch = true } = {}): void { this.requests .filter((request) => prefetch || !request.isPrefetch()) .forEach((request) => request.cancel({ cancelled: true })) } protected cancel({ cancelled = false, interrupted = false } = {}, force: boolean = false): void { if (!force && !this.shouldCancel()) { return } const request = this.requests.shift()! request?.cancel({ cancelled, interrupted }) } protected shouldCancel(): boolean { return this.interruptible && this.requests.length >= this.maxConcurrent } } ================================================ FILE: packages/core/src/resetFormFields.ts ================================================ export const FormComponentResetSymbol = Symbol('FormComponentReset') type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement function isFormElement(element: Element): element is FormElement { return ( element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement ) } function resetInputElement(input: HTMLInputElement, defaultValues: FormDataEntryValue[]): boolean { const oldValue = input.value const oldChecked = input.checked switch (input.type.toLowerCase()) { case 'checkbox': // For checkboxes, check if the input's value is in the array of default values input.checked = defaultValues.includes(input.value) break case 'radio': // For radios, only use the first default value to avoid multiple radios being checked input.checked = defaultValues[0] === input.value break case 'file': input.value = '' break case 'button': case 'submit': case 'reset': case 'image': // These input types don't carry form state break default: // text, email, number, date, etc. - use first default value input.value = defaultValues[0] !== null && defaultValues[0] !== undefined ? String(defaultValues[0]) : '' } // Return true if the value actually changed return input.value !== oldValue || input.checked !== oldChecked } function resetSelectElement(select: HTMLSelectElement, defaultValues: FormDataEntryValue[]): boolean { const oldValue = select.value const oldSelectedOptions = Array.from(select.selectedOptions).map((opt) => opt.value) if (select.multiple) { // For multi-select, select all options that match any of the default values const defaultStrings = defaultValues.map((value) => String(value)) Array.from(select.options).forEach((option) => { option.selected = defaultStrings.includes(option.value) }) } else { // For single select, use the first default value (or empty string) select.value = defaultValues[0] !== undefined ? String(defaultValues[0]) : '' } // Check if selection actually changed const newSelectedOptions = Array.from(select.selectedOptions).map((opt) => opt.value) const hasChanged = select.multiple ? JSON.stringify(oldSelectedOptions.sort()) !== JSON.stringify(newSelectedOptions.sort()) : select.value !== oldValue return hasChanged } function resetFormElement(element: FormElement, defaultValues: FormDataEntryValue[]): boolean { if (element.disabled) { // For disabled elements, use their DOM defaultValue since they're not in FormData if (element instanceof HTMLInputElement) { const oldValue = element.value const oldChecked = element.checked switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': element.checked = element.defaultChecked return element.checked !== oldChecked case 'file': element.value = '' return oldValue !== '' case 'button': case 'submit': case 'reset': case 'image': // These input types don't carry form state return false default: element.value = element.defaultValue return element.value !== oldValue } } else if (element instanceof HTMLSelectElement) { // Reset select to default selected options const oldSelectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value) Array.from(element.options).forEach((option) => { option.selected = option.defaultSelected }) const newSelectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value) return JSON.stringify(oldSelectedOptions.sort()) !== JSON.stringify(newSelectedOptions.sort()) } else if (element instanceof HTMLTextAreaElement) { const oldValue = element.value element.value = element.defaultValue return element.value !== oldValue } return false } if (element instanceof HTMLInputElement) { // Pass all default values to handle checkboxes and radios correctly return resetInputElement(element, defaultValues) } else if (element instanceof HTMLSelectElement) { return resetSelectElement(element, defaultValues) } else if (element instanceof HTMLTextAreaElement) { const oldValue = element.value element.value = defaultValues[0] !== undefined ? String(defaultValues[0]) : '' return element.value !== oldValue } return false } function resetFieldElements( elements: Element | RadioNodeList | HTMLCollection, defaultValues: FormDataEntryValue[], ): boolean { let hasChanged = false if (elements instanceof RadioNodeList || elements instanceof HTMLCollection) { // Handle multiple elements with the same name (e.g., radio buttons, checkboxes, array fields) Array.from(elements).forEach((node, index) => { if (node instanceof Element && isFormElement(node)) { if (node instanceof HTMLInputElement && ['checkbox', 'radio'].includes(node.type.toLowerCase())) { // For checkboxes and radios, pass all default values for value-based matching if (resetFormElement(node, defaultValues)) { hasChanged = true } } else { // For other array elements (like text inputs), use index-based matching const indexedDefaultValues = defaultValues[index] !== undefined ? [defaultValues[index]] : [defaultValues[0] ?? null].filter(Boolean) if (resetFormElement(node, indexedDefaultValues)) { hasChanged = true } } } }) } else if (isFormElement(elements)) { // Handle single element - pass all default values (important for multi-selects) hasChanged = resetFormElement(elements, defaultValues) } return hasChanged } export function resetFormFields(formElement: HTMLFormElement, defaults: FormData, fieldNames?: string[]): void { if (!formElement) { return } const resetEntireForm = !fieldNames || fieldNames.length === 0 // If no specific fields provided, reset the entire form if (resetEntireForm) { // Get all field names from both defaults and form elements (including disabled ones) const formData = new FormData(formElement) const formElementNames = Array.from(formElement.elements) .map((el) => (isFormElement(el) ? el.name : '')) .filter(Boolean) fieldNames = [...new Set([...defaults.keys(), ...formData.keys(), ...formElementNames])] } let hasChanged = false fieldNames!.forEach((fieldName) => { const elements = formElement.elements.namedItem(fieldName) if (elements) { if (resetFieldElements(elements, defaults.getAll(fieldName))) { hasChanged = true } } }) // Dispatch reset event to notify listeners that the form was reset programmatically if (hasChanged && resetEntireForm) { // Use Symbol in detail so adapters can preventDefault() to avoid Firefox's native reset behavior formElement.dispatchEvent( new CustomEvent('reset', { bubbles: true, cancelable: true, detail: { [FormComponentResetSymbol]: true } }), ) } } ================================================ FILE: packages/core/src/response.ts ================================================ import { AxiosResponse } from 'axios' import { get, isEqual, set } from 'lodash-es' import { config, router } from '.' import dialog from './dialog' import { fireBeforeUpdateEvent, fireErrorEvent, fireFlashEvent, fireInvalidEvent, firePrefetchedEvent, fireSuccessEvent, } from './events' import { history } from './history' import modal from './modal' import { page as currentPage } from './page' import Queue from './queue' import { RequestParams } from './requestParams' import { SessionStorage } from './sessionStorage' import { ActiveVisit, ErrorBag, Errors, Page } from './types' import { hrefToUrl, isSameUrlWithoutHash, setHashIfSameUrl } from './url' const queue = new Queue<Promise<boolean | void>>() export class Response { protected wasPrefetched = false constructor( protected requestParams: RequestParams, protected response: AxiosResponse, protected originatingPage: Page, ) {} public static create(params: RequestParams, response: AxiosResponse, originatingPage: Page): Response { return new Response(params, response, originatingPage) } public async handlePrefetch() { if (isSameUrlWithoutHash(this.requestParams.all().url, window.location)) { this.handle() } } public async handle() { return queue.add(() => this.process()) } public async process() { if (this.requestParams.all().prefetch) { this.wasPrefetched = true this.requestParams.all().prefetch = false this.requestParams.all().onPrefetched(this.response, this.requestParams.all()) firePrefetchedEvent(this.response, this.requestParams.all()) return Promise.resolve() } this.requestParams.runCallbacks() if (!this.isInertiaResponse()) { return this.handleNonInertiaResponse() } await history.processQueue() history.preserveUrl = this.requestParams.all().preserveUrl await this.setPage() const errors = currentPage.get().props.errors || {} if (Object.keys(errors).length > 0) { const scopedErrors = this.getScopedErrors(errors) fireErrorEvent(scopedErrors) return this.requestParams.all().onError(scopedErrors) } router.flushByCacheTags(this.requestParams.all().invalidateCacheTags || []) if (!this.wasPrefetched) { // We end up here other than from the prefetch cache, so we assume this response is // never than the cached one and therefore flush the cache. router.flush(currentPage.get().url) } const { flash } = currentPage.get() if (Object.keys(flash).length > 0 && !this.requestParams.isDeferredPropsRequest()) { fireFlashEvent(flash) this.requestParams.all().onFlash(flash) } fireSuccessEvent(currentPage.get()) await this.requestParams.all().onSuccess(currentPage.get()) history.preserveUrl = false } public mergeParams(params: ActiveVisit) { this.requestParams.merge(params) } public getPageResponse(): Page { const data = this.getDataFromResponse(this.response.data) // Only spread if data is an object (not a string like HTML error pages) if (typeof data === 'object') { return (this.response.data = { ...data, flash: data.flash ?? {} }) } return (this.response.data = data) } protected async handleNonInertiaResponse() { if (this.isLocationVisit()) { const locationUrl = hrefToUrl(this.getHeader('x-inertia-location')) setHashIfSameUrl(this.requestParams.all().url, locationUrl) return this.locationVisit(locationUrl) } const response = { ...this.response, data: this.getDataFromResponse(this.response.data), } if (fireInvalidEvent(response)) { return config.get('future.useDialogForErrorModal') ? dialog.show(response.data) : modal.show(response.data) } } protected isInertiaResponse(): boolean { return this.hasHeader('x-inertia') } protected hasStatus(status: number): boolean { return this.response.status === status } protected getHeader(header: string): string { return this.response.headers[header] } protected hasHeader(header: string): boolean { return this.getHeader(header) !== undefined } protected isLocationVisit(): boolean { return this.hasStatus(409) && this.hasHeader('x-inertia-location') } /** * @link https://inertiajs.com/redirects#external-redirects */ protected locationVisit(url: URL): boolean | void { try { SessionStorage.set(SessionStorage.locationVisitKey, { preserveScroll: this.requestParams.all().preserveScroll === true, }) if (typeof window === 'undefined') { return } if (isSameUrlWithoutHash(window.location, url)) { window.location.reload() } else { window.location.href = url.href } } catch (error) { return false } } protected async setPage(): Promise<void> { const pageResponse = this.getPageResponse() if (!this.shouldSetPage(pageResponse)) { return Promise.resolve() } this.mergeProps(pageResponse) currentPage.mergeOncePropsIntoResponse(pageResponse) this.preserveEqualProps(pageResponse) await this.setRememberedState(pageResponse) this.requestParams.setPreserveOptions(pageResponse) pageResponse.url = history.preserveUrl ? currentPage.get().url : this.pageUrl(pageResponse) this.requestParams.all().onBeforeUpdate(pageResponse) fireBeforeUpdateEvent(pageResponse) return currentPage.set(pageResponse, { replace: this.requestParams.all().replace, preserveScroll: this.requestParams.all().preserveScroll as boolean, preserveState: this.requestParams.all().preserveState as boolean, viewTransition: this.requestParams.all().viewTransition, }) } protected getDataFromResponse(response: any): any { if (typeof response !== 'string') { return response } try { return JSON.parse(response) } catch (error) { return response } } protected shouldSetPage(pageResponse: Page): boolean { if (!this.requestParams.all().async) { // If the request is sync, we should always set the page return true } if (this.originatingPage.component !== pageResponse.component) { // We originated from a component but the response re-directed us, // we should respect the redirection and set the page return true } // At this point, if the originating request component is different than the current component, // the user has since navigated and we should discard the response if (this.originatingPage.component !== currentPage.get().component) { return false } const originatingUrl = hrefToUrl(this.originatingPage.url) const currentPageUrl = hrefToUrl(currentPage.get().url) // We have the same component, let's double-check the URL // If we're no longer on the same path name (e.g. /users/1 -> /users/2), we should not set the page return originatingUrl.origin === currentPageUrl.origin && originatingUrl.pathname === currentPageUrl.pathname } protected pageUrl(pageResponse: Page) { const responseUrl = hrefToUrl(pageResponse.url) setHashIfSameUrl(this.requestParams.all().url, responseUrl) return responseUrl.pathname + responseUrl.search + responseUrl.hash } protected preserveEqualProps(pageResponse: Page): void { if (pageResponse.component !== currentPage.get().component || config.get('future.preserveEqualProps') !== true) { return } const currentPageProps = currentPage.get().props Object.entries(pageResponse.props).forEach(([key, value]) => { if (isEqual(value, currentPageProps[key])) { pageResponse.props[key] = currentPageProps[key] } }) } protected mergeProps(pageResponse: Page): void { if (!this.requestParams.isPartial() || pageResponse.component !== currentPage.get().component) { return } const propsToAppend = pageResponse.mergeProps || [] const propsToPrepend = pageResponse.prependProps || [] const propsToDeepMerge = pageResponse.deepMergeProps || [] const matchPropsOn = pageResponse.matchPropsOn || [] const mergeProp = (prop: string, shouldAppend: boolean) => { const currentProp = get(currentPage.get().props, prop) const incomingProp = get(pageResponse.props, prop) if (Array.isArray(incomingProp)) { const newArray = this.mergeOrMatchItems( (currentProp || []) as any[], incomingProp, prop, matchPropsOn, shouldAppend, ) set(pageResponse.props, prop, newArray) } else if (typeof incomingProp === 'object' && incomingProp !== null) { const newObject = { ...(currentProp || {}), ...incomingProp, } set(pageResponse.props, prop, newObject) } } propsToAppend.forEach((prop) => mergeProp(prop, true)) propsToPrepend.forEach((prop) => mergeProp(prop, false)) propsToDeepMerge.forEach((prop) => { const currentProp = currentPage.get().props[prop] const incomingProp = pageResponse.props[prop] // Function to recursively merge objects and arrays const deepMerge = (target: any, source: any, matchProp: string) => { if (Array.isArray(source)) { return this.mergeOrMatchItems(target, source, matchProp, matchPropsOn) } if (typeof source === 'object' && source !== null) { // Merge objects by iterating over keys return Object.keys(source).reduce( (acc, key) => { acc[key] = deepMerge(target ? target[key] : undefined, source[key], `${matchProp}.${key}`) return acc }, { ...target }, ) } // If the source is neither an array nor an object, simply return the it return source } // Apply the deep merge and update the page response pageResponse.props[prop] = deepMerge(currentProp, incomingProp, prop) }) pageResponse.props = { ...currentPage.get().props, ...pageResponse.props } if (this.requestParams.isDeferredPropsRequest()) { const currentErrors = currentPage.get().props.errors if (currentErrors && Object.keys(currentErrors).length > 0) { // Preserve existing errors during deferred props requests pageResponse.props.errors = currentErrors } } // Preserve the existing scrollProps if (currentPage.get().scrollProps) { pageResponse.scrollProps = { ...(currentPage.get().scrollProps || {}), ...(pageResponse.scrollProps || {}), } } // Preserve the existing onceProps if (currentPage.hasOnceProps()) { pageResponse.onceProps = { ...(currentPage.get().onceProps || {}), ...(pageResponse.onceProps || {}), } } // Preserve flash data and merge with new flash data on non-deferred requests pageResponse.flash = { ...currentPage.get().flash, ...(this.requestParams.isDeferredPropsRequest() ? {} : pageResponse.flash), } const currentOriginalDeferred = currentPage.get().initialDeferredProps if (currentOriginalDeferred && Object.keys(currentOriginalDeferred).length > 0) { pageResponse.initialDeferredProps = currentOriginalDeferred } } protected mergeOrMatchItems( existingItems: any[], newItems: any[], matchProp: string, matchPropsOn: string[], shouldAppend = true, ) { const items = Array.isArray(existingItems) ? existingItems : [] // Find the matching key for this specific property path const matchingKey = matchPropsOn.find((key) => { const keyPath = key.split('.').slice(0, -1).join('.') return keyPath === matchProp }) // If no matching key is configured, simply concatenate the arrays if (!matchingKey) { return shouldAppend ? [...items, ...newItems] : [...newItems, ...items] } // Extract the property name we'll use to match items (e.g., 'id' from 'users.data.id') const uniqueProperty = matchingKey.split('.').pop() || '' // Create a map of new items by their unique property lookups const newItemsMap = new Map() newItems.forEach((item) => { if (this.hasUniqueProperty(item, uniqueProperty)) { newItemsMap.set(item[uniqueProperty], item) } }) return shouldAppend ? this.appendWithMatching(items, newItems, newItemsMap, uniqueProperty) : this.prependWithMatching(items, newItems, newItemsMap, uniqueProperty) } protected appendWithMatching( existingItems: any[], newItems: any[], newItemsMap: Map<any, any>, uniqueProperty: string, ): any[] { // Update existing items with new values, keep non-matching items const updatedExisting = existingItems.map((item) => { if (this.hasUniqueProperty(item, uniqueProperty) && newItemsMap.has(item[uniqueProperty])) { return newItemsMap.get(item[uniqueProperty]) } return item }) // Filter new items to only include those not already in existing items const newItemsToAdd = newItems.filter((item) => { if (!this.hasUniqueProperty(item, uniqueProperty)) { return true // Always add items without unique property } return !existingItems.some( (existing) => this.hasUniqueProperty(existing, uniqueProperty) && existing[uniqueProperty] === item[uniqueProperty], ) }) return [...updatedExisting, ...newItemsToAdd] } protected prependWithMatching( existingItems: any[], newItems: any[], newItemsMap: Map<any, any>, uniqueProperty: string, ): any[] { // Filter existing items, keeping only those not being updated const untouchedExisting = existingItems.filter((item) => { if (this.hasUniqueProperty(item, uniqueProperty)) { return !newItemsMap.has(item[uniqueProperty]) } return true }) return [...newItems, ...untouchedExisting] } protected hasUniqueProperty(item: any, property: string): boolean { return item && typeof item === 'object' && property in item } protected async setRememberedState(pageResponse: Page): Promise<void> { const rememberedState = await history.getState<Page['rememberedState']>(history.rememberedState, {}) if ( this.requestParams.all().preserveState && rememberedState && pageResponse.component === currentPage.get().component ) { pageResponse.rememberedState = rememberedState } } protected getScopedErrors(errors: Errors & ErrorBag): Errors { if (!this.requestParams.all().errorBag) { return errors } return errors[this.requestParams.all().errorBag || ''] || {} } } ================================================ FILE: packages/core/src/router.ts ================================================ import { cloneDeep, get, set } from 'lodash-es' import { progress } from '.' import { config } from './config' import { eventHandler } from './eventHandler' import { fireBeforeEvent, fireFlashEvent } from './events' import { history } from './history' import { InitialVisit } from './initialVisit' import { page as currentPage } from './page' import { polls } from './polls' import { prefetchedRequests } from './prefetched' import Queue from './queue' import { Request } from './request' import { RequestParams } from './requestParams' import { RequestStream } from './requestStream' import { Scroll } from './scroll' import { ActiveVisit, ClientSideVisitOptions, Component, FlashData, GlobalEvent, GlobalEventNames, GlobalEventResult, InFlightPrefetch, Method, Page, PageFlashData, PendingVisit, PendingVisitOptions, PollOptions, PrefetchedResponse, PrefetchOptions, ReloadOptions, RequestPayload, RouterInitParams, UrlMethodPair, Visit, VisitCallbacks, VisitHelperOptions, VisitOptions, } from './types' import { hrefToUrl, isSameUrlWithoutHash, isSameUrlWithoutQueryOrHash, isUrlMethodPair, transformUrlAndData, } from './url' export class Router { protected syncRequestStream = new RequestStream({ maxConcurrent: 1, interruptible: true, }) protected asyncRequestStream = new RequestStream({ maxConcurrent: Infinity, interruptible: false, }) protected clientVisitQueue = new Queue<Promise<void>>() public init<ComponentType = Component>({ initialPage, resolveComponent, swapComponent, onFlash, }: RouterInitParams<ComponentType>): void { currentPage.init({ initialPage, resolveComponent, swapComponent, onFlash, }) InitialVisit.handle() eventHandler.init() eventHandler.on('missingHistoryItem', () => { if (typeof window !== 'undefined') { this.visit(window.location.href, { preserveState: true, preserveScroll: true, replace: true }) } }) eventHandler.on('loadDeferredProps', (deferredProps: Page['deferredProps']) => { this.loadDeferredProps(deferredProps) }) eventHandler.on('historyQuotaExceeded', (url) => { window.location.href = url }) } public get<T extends RequestPayload = RequestPayload>( url: URL | string | UrlMethodPair, data: T = {} as T, options: VisitHelperOptions<T> = {}, ): void { return this.visit(url, { ...options, method: 'get', data }) } public post<T extends RequestPayload = RequestPayload>( url: URL | string | UrlMethodPair, data: T = {} as T, options: VisitHelperOptions<T> = {}, ): void { return this.visit(url, { preserveState: true, ...options, method: 'post', data }) } public put<T extends RequestPayload = RequestPayload>( url: URL | string | UrlMethodPair, data: T = {} as T, options: VisitHelperOptions<T> = {}, ): void { return this.visit(url, { preserveState: true, ...options, method: 'put', data }) } public patch<T extends RequestPayload = RequestPayload>( url: URL | string | UrlMethodPair, data: T = {} as T, options: VisitHelperOptions<T> = {}, ): void { return this.visit(url, { preserveState: true, ...options, method: 'patch', data }) } public delete<T extends RequestPayload = RequestPayload>( url: URL | string | UrlMethodPair, options: Omit<VisitOptions<T>, 'method'> = {}, ): void { return this.visit(url, { preserveState: true, ...options, method: 'delete' }) } public reload<T extends RequestPayload = RequestPayload>(options: ReloadOptions<T> = {}): void { return this.doReload(options) } protected doReload<T extends RequestPayload = RequestPayload>( options: ReloadOptions<T> & { deferredProps?: boolean } = {}, ): void { if (typeof window === 'undefined') { return } return this.visit(window.location.href, { ...options, preserveScroll: true, preserveState: true, async: true, headers: { ...(options.headers || {}), 'Cache-Control': 'no-cache', }, }) } public remember(data: unknown, key = 'default'): void { history.remember(data, key) } public restore<T = unknown>(key = 'default'): T | undefined { return history.restore(key) as T | undefined } public on<TEventName extends GlobalEventNames>( type: TEventName, callback: (event: GlobalEvent<TEventName>) => GlobalEventResult<TEventName>, ): VoidFunction { if (typeof window === 'undefined') { return () => {} } return eventHandler.onGlobalEvent(type, callback) } /** * @deprecated Use cancelAll() instead. */ public cancel(): void { this.syncRequestStream.cancelInFlight() } public cancelAll({ async = true, prefetch = true, sync = true } = {}): void { if (async) { this.asyncRequestStream.cancelInFlight({ prefetch }) } if (sync) { this.syncRequestStream.cancelInFlight() } } public poll(interval: number, requestOptions: ReloadOptions = {}, options: PollOptions = {}) { return polls.add(interval, () => this.reload(requestOptions), { autoStart: options.autoStart ?? true, keepAlive: options.keepAlive ?? false, }) } public visit<T extends RequestPayload = RequestPayload>( href: string | URL | UrlMethodPair, options: VisitOptions<T> = {}, ): void { const visit: PendingVisit = this.getPendingVisit(href, { ...options, showProgress: options.showProgress ?? !options.async, } as VisitOptions) const events = this.getVisitEvents(options as VisitOptions) // If either of these return false, we don't want to continue if (events.onBefore(visit) === false || !fireBeforeEvent(visit)) { return } const currentPageUrl = hrefToUrl(currentPage.get().url) const isPartialReload = visit.only.length > 0 || visit.except.length > 0 || visit.reset.length > 0 // For partial reloads, only compare the base URL (origin + pathname) to allow // concurrent requests with different query params to the same page const isSamePage = isPartialReload ? isSameUrlWithoutQueryOrHash(visit.url, currentPageUrl) : isSameUrlWithoutHash(visit.url, currentPageUrl) if (!isSamePage) { // Only cancel non-prefetch requests (deferred props + partial reloads) this.asyncRequestStream.cancelInFlight({ prefetch: false }) } if (!visit.async) { this.syncRequestStream.interruptInFlight() } if (!currentPage.isCleared() && !visit.preserveUrl) { // Save scroll regions for the current page Scroll.save() } const requestParams: PendingVisit & VisitCallbacks = { ...visit, ...events, } const prefetched = prefetchedRequests.get(requestParams) if (prefetched) { progress.reveal(prefetched.inFlight) prefetchedRequests.use(prefetched, requestParams) } else { progress.reveal(true) const requestStream = visit.async ? this.asyncRequestStream : this.syncRequestStream requestStream.send(Request.create(requestParams, currentPage.get())) } } public getCached( href: string | URL | UrlMethodPair, options: VisitOptions = {}, ): InFlightPrefetch | PrefetchedResponse | null { return prefetchedRequests.findCached(this.getPrefetchParams(href, options)) } public flush(href: string | URL | UrlMethodPair, options: VisitOptions = {}): void { prefetchedRequests.remove(this.getPrefetchParams(href, options)) } public flushAll(): void { prefetchedRequests.removeAll() } public flushByCacheTags(tags: string | string[]): void { prefetchedRequests.removeByTags(Array.isArray(tags) ? tags : [tags]) } public getPrefetching( href: string | URL | UrlMethodPair, options: VisitOptions = {}, ): InFlightPrefetch | PrefetchedResponse | null { return prefetchedRequests.findInFlight(this.getPrefetchParams(href, options)) } public prefetch( href: string | URL | UrlMethodPair, options: VisitOptions = {}, prefetchOptions: Partial<PrefetchOptions> = {}, ) { const method: Method = options.method ?? (isUrlMethodPair(href) ? href.method : 'get') if (method !== 'get') { throw new Error('Prefetch requests must use the GET method') } const visit: PendingVisit = this.getPendingVisit(href, { ...options, async: true, showProgress: false, prefetch: true, viewTransition: false, }) const visitUrl = visit.url.origin + visit.url.pathname + visit.url.search const currentUrl = window.location.origin + window.location.pathname + window.location.search if (visitUrl === currentUrl) { // Don't prefetch the current page, you're already on it return } const events = this.getVisitEvents(options) // If either of these return false, we don't want to continue if (events.onBefore(visit) === false || !fireBeforeEvent(visit)) { return } progress.hide() this.asyncRequestStream.interruptInFlight() const requestParams: PendingVisit & VisitCallbacks = { ...visit, ...events, } const ensureCurrentPageIsSet = (): Promise<void> => { return new Promise((resolve) => { const checkIfPageIsDefined = () => { if (currentPage.get()) { resolve() } else { setTimeout(checkIfPageIsDefined, 50) } } checkIfPageIsDefined() }) } ensureCurrentPageIsSet().then(() => { prefetchedRequests.add( requestParams, (params) => { this.asyncRequestStream.send(Request.create(params, currentPage.get())) }, { cacheFor: config.get('prefetch.cacheFor'), cacheTags: [], ...prefetchOptions, }, ) }) } public clearHistory(): void { history.clear() } public decryptHistory(): Promise<Page> { return history.decrypt() } public resolveComponent(component: string): Promise<Component> { return currentPage.resolve(component) } public replace<TProps = Page['props']>(params: ClientSideVisitOptions<TProps>): void { this.clientVisit(params, { replace: true }) } public replaceProp<TProps = Page['props']>( name: string, value: unknown | ((oldValue: unknown, props: TProps) => unknown), options?: Pick<ClientSideVisitOptions, 'onError' | 'onFinish' | 'onSuccess'>, ): void { this.replace({ preserveScroll: true, preserveState: true, props(currentProps) { const newValue = typeof value === 'function' ? value(get(currentProps, name), currentProps) : value return set(cloneDeep(currentProps), name, newValue) }, ...(options || {}), }) } public appendToProp<TProps = Page['props']>( name: string, value: unknown | unknown[] | ((oldValue: unknown, props: TProps) => unknown | unknown[]), options?: Pick<ClientSideVisitOptions, 'onError' | 'onFinish' | 'onSuccess'>, ): void { this.replaceProp( name, (currentValue: unknown, currentProps: TProps) => { const newValue = typeof value === 'function' ? value(currentValue, currentProps) : value if (!Array.isArray(currentValue)) { currentValue = currentValue !== undefined ? [currentValue] : [] } return [...(currentValue as unknown[]), newValue] }, options, ) } public prependToProp<TProps = Page['props']>( name: string, value: unknown | unknown[] | ((oldValue: unknown, props: TProps) => unknown | unknown[]), options?: Pick<ClientSideVisitOptions, 'onError' | 'onFinish' | 'onSuccess'>, ): void { this.replaceProp( name, (currentValue: unknown, currentProps: TProps) => { const newValue = typeof value === 'function' ? value(currentValue, currentProps) : value if (!Array.isArray(currentValue)) { currentValue = currentValue !== undefined ? [currentValue] : [] } return [newValue, ...(currentValue as unknown[])] }, options, ) } public push<TProps = Page['props']>(params: ClientSideVisitOptions<TProps>): void { this.clientVisit(params) } public flash<TFlash extends PageFlashData = PageFlashData>( keyOrData: string | ((flash: FlashData) => TFlash) | TFlash, value?: unknown, ): void { const current = currentPage.get().flash let flash: PageFlashData if (typeof keyOrData === 'function') { flash = keyOrData(current) } else if (typeof keyOrData === 'string') { flash = { ...current, [keyOrData]: value } } else if (keyOrData && Object.keys(keyOrData).length) { flash = { ...current, ...keyOrData } } else { return } currentPage.setFlash(flash) if (Object.keys(flash).length) { fireFlashEvent(flash) } } protected clientVisit<TProps = Page['props']>( params: ClientSideVisitOptions<TProps>, { replace = false }: { replace?: boolean } = {}, ): void { this.clientVisitQueue.add(() => this.performClientVisit(params, { replace })) } protected performClientVisit<TProps = Page['props']>( params: ClientSideVisitOptions<TProps>, { replace = false }: { replace?: boolean } = {}, ): Promise<void> { const current = currentPage.get() const onceProps = typeof params.props === 'function' ? Object.fromEntries( Object.values(current.onceProps ?? {}).map((onceProp) => [onceProp.prop, current.props[onceProp.prop]]), ) : {} const props = typeof params.props === 'function' ? params.props(current.props as TProps, onceProps as Partial<TProps>) : (params.props ?? current.props) const flash = typeof params.flash === 'function' ? params.flash(current.flash) : params.flash const { viewTransition, onError, onFinish, onFlash, onSuccess, ...pageParams } = params const page = { ...current, ...pageParams, flash: flash ?? {}, props: props as Page['props'], } const preserveScroll = RequestParams.resolvePreserveOption(params.preserveScroll ?? false, page) const preserveState = RequestParams.resolvePreserveOption(params.preserveState ?? false, page) return currentPage .set(page, { replace, preserveScroll, preserveState, viewTransition, }) .then(() => { const currentFlash = currentPage.get().flash if (Object.keys(currentFlash).length > 0) { fireFlashEvent(currentFlash) onFlash?.(currentFlash) } const errors = currentPage.get().props.errors || {} if (Object.keys(errors).length === 0) { onSuccess?.(currentPage.get()) return } const scopedErrors = params.errorBag ? errors[params.errorBag || ''] || {} : errors onError?.(scopedErrors) }) .finally(() => onFinish?.(params)) } protected getPrefetchParams(href: string | URL | UrlMethodPair, options: VisitOptions): ActiveVisit { return { ...this.getPendingVisit(href, { ...options, async: true, showProgress: false, prefetch: true, viewTransition: false, }), ...this.getVisitEvents(options), } } protected getPendingVisit( href: string | URL | UrlMethodPair, options: VisitOptions, pendingVisitOptions: Partial<PendingVisitOptions> = {}, ): PendingVisit { if (isUrlMethodPair(href)) { const urlMethodPair = href href = urlMethodPair.url options.method = options.method ?? urlMethodPair.method } const defaultVisitOptionsCallback = config.get('visitOptions') const configuredOptions = defaultVisitOptionsCallback ? defaultVisitOptionsCallback(href.toString(), cloneDeep(options)) || {} : {} const mergedOptions: Visit = { method: 'get', data: {}, replace: false, preserveScroll: false, preserveState: false, only: [], except: [], headers: {}, errorBag: '', forceFormData: false, queryStringArrayFormat: 'brackets', async: false, showProgress: true, fresh: false, reset: [], preserveUrl: false, prefetch: false, invalidateCacheTags: [], viewTransition: false, ...options, ...configuredOptions, } const [url, _data] = transformUrlAndData( href, mergedOptions.data, mergedOptions.method, mergedOptions.forceFormData, mergedOptions.queryStringArrayFormat, ) const visit = { cancelled: false, completed: false, interrupted: false, ...mergedOptions, ...pendingVisitOptions, url, data: _data, } if (visit.prefetch) { visit.headers['Purpose'] = 'prefetch' } return visit } protected getVisitEvents(options: VisitOptions): VisitCallbacks { return { onCancelToken: options.onCancelToken || (() => {}), onBefore: options.onBefore || (() => {}), onBeforeUpdate: options.onBeforeUpdate || (() => {}), onStart: options.onStart || (() => {}), onProgress: options.onProgress || (() => {}), onFinish: options.onFinish || (() => {}), onCancel: options.onCancel || (() => {}), onSuccess: options.onSuccess || (() => {}), onError: options.onError || (() => {}), onFlash: options.onFlash || (() => {}), onPrefetched: options.onPrefetched || (() => {}), onPrefetching: options.onPrefetching || (() => {}), } } protected loadDeferredProps(deferred: Page['deferredProps']): void { if (deferred) { Object.entries(deferred).forEach(([_, group]) => { this.doReload({ only: group, deferredProps: true }) }) } } } ================================================ FILE: packages/core/src/scroll.ts ================================================ import { requestAnimationFrame } from './domUtils' import { history } from './history' import { ScrollRegion } from './types' const isServer = typeof window === 'undefined' const isFirefox = !isServer && /Firefox/i.test(window.navigator.userAgent) export class Scroll { public static save(): void { history.saveScrollPositions(this.getScrollRegions()) } public static getScrollRegions(): ScrollRegion[] { return Array.from(this.regions()).map((region) => ({ top: region.scrollTop, left: region.scrollLeft, })) } protected static regions(): NodeListOf<Element> { return document.querySelectorAll('[scroll-region]') } public static scrollToTop(): void { if (isFirefox && getComputedStyle(document.documentElement).scrollBehavior === 'smooth') { // Firefox has a bug with smooth scrolling to (0, 0) when navigating to pages that are shorter than the previous page. return requestAnimationFrame(() => window.scrollTo(0, 0), 2) } window.scrollTo(0, 0) } public static reset(): void { const anchorHash = isServer ? null : window.location.hash if (!anchorHash) { // Reset the document scroll position if there is no hash. this.scrollToTop() } this.regions().forEach((region) => { if (typeof region.scrollTo === 'function') { region.scrollTo(0, 0) } else { region.scrollTop = 0 region.scrollLeft = 0 } }) this.save() this.scrollToAnchor() } public static scrollToAnchor(): void { const anchorHash = isServer ? null : window.location.hash if (anchorHash) { // We're using a setTimeout() here as a workaround for a bug in the React adapter where the // rendering isn't completing fast enough, causing the anchor link to not be scrolled to. setTimeout(() => { const anchorElement = document.getElementById(anchorHash.slice(1)) anchorElement ? anchorElement.scrollIntoView() : this.scrollToTop() }) } } public static restore(scrollRegions: ScrollRegion[]): void { if (isServer) { return } window.requestAnimationFrame(() => { this.restoreDocument() this.restoreScrollRegions(scrollRegions) }) } public static restoreScrollRegions(scrollRegions: ScrollRegion[]): void { if (isServer) { return } this.regions().forEach((region: Element, index: number) => { const scrollPosition = scrollRegions[index] if (!scrollPosition) { return } if (typeof region.scrollTo === 'function') { region.scrollTo(scrollPosition.left, scrollPosition.top) } else { region.scrollTop = scrollPosition.top region.scrollLeft = scrollPosition.left } }) } public static restoreDocument(): void { const scrollPosition = history.getDocumentScrollPosition() window.scrollTo(scrollPosition.left, scrollPosition.top) } public static onScroll(event: Event): void { const target = event.target as Element if (typeof target.hasAttribute === 'function' && target.hasAttribute('scroll-region')) { this.save() } } public static onWindowScroll(): void { history.saveDocumentScrollPosition({ top: window.scrollY, left: window.scrollX, }) } } ================================================ FILE: packages/core/src/server.ts ================================================ import { createServer, IncomingMessage } from 'http' import cluster from 'node:cluster' import { availableParallelism } from 'node:os' import * as process from 'process' import { InertiaAppResponse, Page } from './types' type AppCallback = (page: Page) => InertiaAppResponse type RouteHandler = (request: IncomingMessage) => Promise<unknown> type ServerOptions = { port?: number cluster?: boolean } type Port = number const readableToString: (readable: IncomingMessage) => Promise<string> = (readable) => new Promise((resolve, reject) => { let data = '' readable.on('data', (chunk) => (data += chunk)) readable.on('end', () => resolve(data)) readable.on('error', (err) => reject(err)) }) export default (render: AppCallback, options?: Port | ServerOptions): void => { const _port = typeof options === 'number' ? options : (options?.port ?? 13714) const _useCluster = typeof options === 'object' && options?.cluster !== undefined ? options.cluster : false const log = (message: string) => { console.log( _useCluster && !cluster.isPrimary ? `[${cluster.worker?.id ?? 'N/A'} / ${cluster.worker?.process?.pid ?? 'N/A'}] ${message}` : message, ) } if (_useCluster && cluster.isPrimary) { log('Primary Inertia SSR server process started...') for (let i = 0; i < availableParallelism(); i++) { cluster.fork() } cluster.on('message', (_worker, message) => { if (message === 'shutdown') { for (const id in cluster.workers) { cluster.workers[id]?.kill() } process.exit() } }) return } const routes: Record<string, RouteHandler> = { '/health': async () => ({ status: 'OK', timestamp: Date.now() }), '/shutdown': async () => { if (cluster.isWorker) { process.send?.('shutdown') } process.exit() }, '/render': async (request) => render(JSON.parse(await readableToString(request))), '/404': async () => ({ status: 'NOT_FOUND', timestamp: Date.now() }), } createServer(async (request, response) => { const dispatchRoute = routes[<string>request.url] || routes['/404'] try { response.writeHead(200, { 'Content-Type': 'application/json', Server: 'Inertia.js SSR' }) response.write(JSON.stringify(await dispatchRoute(request))) } catch (e) { console.error(e) } response.end() }).listen(_port, () => log('Inertia SSR server started.')) log(`Starting SSR server on port ${_port}...`) } ================================================ FILE: packages/core/src/sessionStorage.ts ================================================ export class SessionStorage { public static locationVisitKey = 'inertiaLocationVisit' public static set(key: string, value: any): void { if (typeof window !== 'undefined') { window.sessionStorage.setItem(key, JSON.stringify(value)) } } public static get(key: string): any { if (typeof window !== 'undefined') { return JSON.parse(window.sessionStorage.getItem(key) || 'null') } } public static merge(key: string, value: any): void { const existing = this.get(key) if (existing === null) { this.set(key, value) } else { this.set(key, { ...existing, ...value }) } } public static remove(key: string): void { if (typeof window !== 'undefined') { window.sessionStorage.removeItem(key) } } public static removeNested(key: string, nestedKey: string): void { const existing = this.get(key) if (existing !== null) { delete existing[nestedKey] this.set(key, existing) } } public static exists(key: string): boolean { try { return this.get(key) !== null } catch (error) { return false } } public static clear(): void { if (typeof window !== 'undefined') { window.sessionStorage.clear() } } } ================================================ FILE: packages/core/src/time.ts ================================================ import { CacheForOption, TimeUnit } from './types' const conversionMap: Record<TimeUnit, number> = { ms: 1, s: 1000, m: 1000 * 60, h: 1000 * 60 * 60, d: 1000 * 60 * 60 * 24, } export const timeToMs = (time: CacheForOption): number => { if (typeof time === 'number') { return time } for (const [unit, conversion] of Object.entries(conversionMap)) { if (time.endsWith(unit)) { return parseFloat(time) * conversion } } return parseInt(time) } ================================================ FILE: packages/core/src/types.ts ================================================ import { AxiosProgressEvent, AxiosResponse } from 'axios' import { NamedInputEvent, ValidationConfig, Validator } from 'laravel-precognition' import { Response } from './response' declare module 'axios' { export interface AxiosProgressEvent { percentage: number | undefined } } export interface PageFlashData { [key: string]: unknown } export type DefaultInertiaConfig = { errorValueType: string flashDataType: PageFlashData sharedPageProps: PageProps } /** * Designed to allow overriding of some core types using TypeScript * interface declaration merging. * * @see {@link DefaultInertiaConfig} for keys to override * @example * ```ts * // global.d.ts * import '@inertiajs/core' * * declare module '@inertiajs/core' { * export interface InertiaConfig { * errorValueType: string[] * flashDataType: { * toast?: { type: 'success' | 'error', message: string } * } * sharedPageProps: { * auth: { user: User | null } * } * } * } * ``` */ export interface InertiaConfig {} export type InertiaConfigFor<Key extends keyof DefaultInertiaConfig> = Key extends keyof InertiaConfig ? InertiaConfig[Key] : DefaultInertiaConfig[Key] export type ErrorValue = InertiaConfigFor<'errorValueType'> export type FlashData = InertiaConfigFor<'flashDataType'> export type SharedPageProps = InertiaConfigFor<'sharedPageProps'> export type Errors = Record<string, ErrorValue> export type ErrorBag = Record<string, Errors> export type FormDataConvertibleValue = Blob | FormDataEntryValue | Date | boolean | number | null | undefined export type FormDataConvertible = | Array<FormDataConvertible> | { [key: string]: FormDataConvertible } | FormDataConvertibleValue export type FormDataType<T extends object> = { [K in keyof T]: T[K] extends infer U ? U extends FormDataConvertibleValue ? U : U extends (...args: unknown[]) => unknown ? never : U extends object | Array<unknown> ? FormDataType<U> : never : never } /** * Uses `0 extends 1 & T` to detect `any` type and prevent infinite recursion. */ export type FormDataKeys<T> = T extends Function | FormDataConvertibleValue ? never : T extends unknown[] ? ArrayFormDataKeys<T> : T extends object ? ObjectFormDataKeys<T> : never /** * Helper type for array form data keys */ type ArrayFormDataKeys<T extends unknown[]> = number extends T['length'] ? // Dynamic array | `${number}` | (0 extends 1 & T[number] ? never : T[number] extends FormDataConvertibleValue ? never : `${number}.${FormDataKeys<T[number]>}`) : // Tuple with known length | Extract<keyof T, `${number}`> | { [Key in Extract<keyof T, `${number}`>]: 0 extends 1 & T[Key] ? never : T[Key] extends FormDataConvertibleValue ? never : `${Key & string}.${FormDataKeys<T[Key & string] & string>}` }[Extract<keyof T, `${number}`>] /** * Helper type for object form data keys */ type ObjectFormDataKeys<T extends object> = string extends keyof T ? string : | Extract<keyof T, string> | { [Key in Extract<keyof T, string>]: 0 extends 1 & T[Key] ? never : T[Key] extends FormDataConvertibleValue ? never : T[Key] extends any[] ? `${Key}.${FormDataKeys<T[Key]> & string}` : T[Key] extends Record<string, any> ? `${Key}.${FormDataKeys<T[Key]> & string}` : Exclude<T[Key], null | undefined> extends any[] ? never : Exclude<T[Key], null | undefined> extends Record<string, any> ? `${Key}.${FormDataKeys<Exclude<T[Key], null | undefined>> & string}` : never }[Extract<keyof T, string>] type PartialFormDataErrors<T> = { [K in string extends keyof T ? string : Extract<keyof FormDataError<T>, string>]?: ErrorValue } export type FormDataErrors<T> = PartialFormDataErrors<T> & { [K in keyof PartialFormDataErrors<T>]: NonNullable<PartialFormDataErrors<T>[K]> } export type FormDataValues<T, K extends FormDataKeys<T>> = K extends `${infer P}.${infer Rest}` ? T extends unknown[] ? P extends `${infer I extends number}` ? Rest extends FormDataKeys<T[I]> ? FormDataValues<T[I], Rest> : never : never : P extends keyof T ? Rest extends FormDataKeys<T[P]> ? FormDataValues<T[P], Rest> : never : never : K extends keyof T ? T[K] : T extends unknown[] ? T[K & number] : never export type FormDataError<T> = Partial<Record<FormDataKeys<T>, ErrorValue>> export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' export type RequestPayload = Record<string, FormDataConvertible> | FormData export interface PageProps { [key: string]: unknown } export type ScrollProp = { pageName: string previousPage: number | string | null nextPage: number | string | null currentPage: number | string | null reset: boolean } export interface Page<SharedProps extends PageProps = PageProps> { component: string props: PageProps & SharedProps & { errors: Errors & ErrorBag deferred?: Record<string, VisitOptions['only']> } url: string version: string | null clearHistory: boolean encryptHistory: boolean deferredProps?: Record<string, NonNullable<VisitOptions['only']>> initialDeferredProps?: Record<string, NonNullable<VisitOptions['only']>> mergeProps?: string[] prependProps?: string[] deepMergeProps?: string[] matchPropsOn?: string[] scrollProps?: Record<keyof PageProps, ScrollProp> flash: FlashData onceProps?: Record< string, { prop: keyof PageProps expiresAt?: number | null } > /** @internal */ rememberedState: Record<string, unknown> } export type ScrollRegion = { top: number left: number } export interface ClientSideVisitOptions<TProps = Page['props']> { component?: Page['component'] url?: Page['url'] props?: ((props: TProps, onceProps: Partial<TProps>) => PageProps) | PageProps flash?: ((flash: FlashData) => PageFlashData) | PageFlashData clearHistory?: Page['clearHistory'] encryptHistory?: Page['encryptHistory'] preserveScroll?: VisitOptions['preserveScroll'] preserveState?: VisitOptions['preserveState'] errorBag?: string | null viewTransition?: VisitOptions['viewTransition'] onError?: (errors: Errors) => void onFinish?: (visit: ClientSideVisitOptions<TProps>) => void onFlash?: (flash: FlashData) => void onSuccess?: (page: Page<SharedPageProps>) => void } export type PageResolver = (name: string) => Component export type PageHandler<ComponentType = Component> = ({ component, page, preserveState, }: { component: ComponentType page: Page preserveState: boolean }) => Promise<unknown> export type PreserveStateOption = boolean | 'errors' | ((page: Page) => boolean) export type QueryStringArrayFormatOption = 'indices' | 'brackets' export type Progress = AxiosProgressEvent export type LocationVisit = { preserveScroll: boolean } export type CancelToken = { cancel: VoidFunction } export type CancelTokenCallback = (cancelToken: CancelToken) => void export type Visit<T extends RequestPayload = RequestPayload> = { method: Method data: T replace: boolean preserveScroll: PreserveStateOption preserveState: PreserveStateOption only: Array<string> except: Array<string> headers: Record<string, string> errorBag: string | null forceFormData: boolean queryStringArrayFormat: QueryStringArrayFormatOption async: boolean showProgress: boolean prefetch: boolean fresh: boolean reset: string[] preserveUrl: boolean invalidateCacheTags: string | string[] viewTransition: boolean | ((viewTransition: ViewTransition) => void) } export type GlobalEventsMap<T extends RequestPayload = RequestPayload> = { before: { parameters: [PendingVisit<T>] details: { visit: PendingVisit<T> } result: boolean | void } start: { parameters: [PendingVisit<T>] details: { visit: PendingVisit<T> } result: void } progress: { parameters: [Progress | undefined] details: { progress: Progress | undefined } result: void } finish: { parameters: [ActiveVisit<T>] details: { visit: ActiveVisit<T> } result: void } cancel: { parameters: [] details: {} result: void } beforeUpdate: { parameters: [Page<SharedPageProps>] details: { page: Page<SharedPageProps> } result: void } navigate: { parameters: [Page<SharedPageProps>] details: { page: Page<SharedPageProps> } result: void } success: { parameters: [Page<SharedPageProps>] details: { page: Page<SharedPageProps> } result: void } error: { parameters: [Errors] details: { errors: Errors } result: void } invalid: { parameters: [AxiosResponse] details: { response: AxiosResponse } result: boolean | void } exception: { parameters: [Error] details: { exception: Error } result: boolean | void } prefetched: { parameters: [AxiosResponse, ActiveVisit<T>] details: { response: AxiosResponse fetchedAt: number visit: ActiveVisit<T> } result: void } prefetching: { parameters: [ActiveVisit<T>] details: { visit: ActiveVisit<T> } result: void } flash: { parameters: [Page['flash']] details: { flash: Page['flash'] } result: void } } export type PageEvent = 'newComponent' | 'firstLoad' export type GlobalEventNames<T extends RequestPayload = RequestPayload> = keyof GlobalEventsMap<T> export type GlobalEvent< TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload, > = CustomEvent<GlobalEventDetails<TEventName, T>> export type GlobalEventParameters< TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload, > = GlobalEventsMap<T>[TEventName]['parameters'] export type GlobalEventResult< TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload, > = GlobalEventsMap<T>[TEventName]['result'] export type GlobalEventDetails< TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload, > = GlobalEventsMap<T>[TEventName]['details'] export type GlobalEventTrigger<TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload> = ( ...params: GlobalEventParameters<TEventName, T> ) => GlobalEventResult<TEventName, T> export type GlobalEventCallback<TEventName extends GlobalEventNames<T>, T extends RequestPayload = RequestPayload> = ( ...params: GlobalEventParameters<TEventName, T> ) => GlobalEventResult<TEventName, T> export type InternalEvent = 'missingHistoryItem' | 'loadDeferredProps' | 'historyQuotaExceeded' export type VisitCallbacks<T extends RequestPayload = RequestPayload> = { onCancelToken: CancelTokenCallback onBefore: GlobalEventCallback<'before', T> onBeforeUpdate: GlobalEventCallback<'beforeUpdate', T> onStart: GlobalEventCallback<'start', T> onProgress: GlobalEventCallback<'progress', T> onFinish: GlobalEventCallback<'finish', T> onCancel: GlobalEventCallback<'cancel', T> onSuccess: GlobalEventCallback<'success', T> onError: GlobalEventCallback<'error', T> onFlash: GlobalEventCallback<'flash', T> onPrefetched: GlobalEventCallback<'prefetched', T> onPrefetching: GlobalEventCallback<'prefetching', T> } export type VisitOptions<T extends RequestPayload = RequestPayload> = Partial<Visit<T> & VisitCallbacks<T>> export type ReloadOptions<T extends RequestPayload = RequestPayload> = Omit< VisitOptions<T>, 'preserveScroll' | 'preserveState' > export type PollOptions = { keepAlive?: boolean autoStart?: boolean } export type VisitHelperOptions<T extends RequestPayload = RequestPayload> = Omit<VisitOptions<T>, 'method' | 'data'> export type RouterInitParams<ComponentType = Component> = { initialPage: Page resolveComponent: PageResolver swapComponent: PageHandler<ComponentType> onFlash?: (flash: Page['flash']) => void } export type PendingVisitOptions = { url: URL completed: boolean cancelled: boolean interrupted: boolean } export type PendingVisit<T extends RequestPayload = RequestPayload> = Visit<T> & PendingVisitOptions export type ActiveVisit<T extends RequestPayload = RequestPayload> = PendingVisit<T> & Required<VisitOptions<T>> export type InternalActiveVisit = ActiveVisit & { onPrefetchResponse?: (response: Response) => void onPrefetchError?: (error: Error) => void deferredProps?: boolean } export type VisitId = unknown export type Component = unknown type FirstLevelOptional<T> = { [K in keyof T]?: T[K] extends object ? { [P in keyof T[K]]?: T[K][P] } : T[K] } interface CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> { resolve: TComponentResolver setup: (options: TSetupOptions) => TSetupReturn title?: HeadManagerTitleCallback defaults?: FirstLevelOptional<InertiaAppConfig & TAdditionalInertiaAppConfig> } export interface CreateInertiaAppOptionsForCSR< SharedProps extends PageProps, TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig, > extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> { id?: string page?: Page<SharedProps> progress?: | false | { delay?: number color?: string includeCSS?: boolean showSpinner?: boolean } render?: undefined } export interface CreateInertiaAppOptionsForSSR< SharedProps extends PageProps, TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig, > extends CreateInertiaAppOptions<TComponentResolver, TSetupOptions, TSetupReturn, TAdditionalInertiaAppConfig> { id?: undefined page: Page<SharedProps> progress?: undefined render: unknown } export type InertiaAppSSRResponse = { head: string[]; body: string } export type InertiaAppResponse = Promise<InertiaAppSSRResponse | void> export type HeadManagerTitleCallback = (title: string) => string export type HeadManagerOnUpdateCallback = (elements: string[]) => void export type HeadManager = { forceUpdate: () => void createProvider: () => { preferredAttribute: () => 'data-inertia' | 'inertia' reconnect: () => void update: HeadManagerOnUpdateCallback disconnect: () => void } } export type LinkPrefetchOption = 'mount' | 'hover' | 'click' export type TimeUnit = 'ms' | 's' | 'm' | 'h' | 'd' export type CacheForOption = number | `${number}${TimeUnit}` | string export type PrefetchOptions = { cacheFor: CacheForOption | CacheForOption[] cacheTags: string | string[] } export type InertiaAppConfig = { form: { recentlySuccessfulDuration: number forceIndicesArrayFormatInFormData: boolean withAllErrors: boolean } // experimental: { // /* not guaranteed */ // } future: { /* planned defaults */ preserveEqualProps: boolean useDataInertiaHeadAttribute: boolean useDialogForErrorModal: boolean useScriptElementForInitialPage: boolean } prefetch: { cacheFor: CacheForOption | CacheForOption[] hoverDelay: number } visitOptions?: (href: string, options: VisitOptions) => VisitOptions } export interface LinkComponentBaseProps extends Partial< Pick< Visit<RequestPayload>, | 'data' | 'method' | 'replace' | 'preserveScroll' | 'preserveState' | 'preserveUrl' | 'only' | 'except' | 'headers' | 'queryStringArrayFormat' | 'async' | 'viewTransition' > & VisitCallbacks & { href: string | UrlMethodPair prefetch: boolean | LinkPrefetchOption | LinkPrefetchOption[] cacheFor: CacheForOption | CacheForOption[] cacheTags: string | string[] } > {} type PrefetchObject = { params: ActiveVisit response: Promise<Response> } export type InFlightPrefetch = PrefetchObject & { staleTimestamp: null inFlight: true } export type PrefetchCancellationToken = { isCancelled: boolean cancel: () => void } export type PrefetchedResponse = PrefetchObject & { staleTimestamp: number timestamp: number expiresAt: number singleUse: boolean inFlight: false tags: string[] } export type PrefetchRemovalTimer = { params: ActiveVisit timer: number } export type ProgressSettings = { minimum: number easing: string positionUsing: 'translate3d' | 'translate' | 'margin' speed: number trickle: boolean trickleSpeed: number showSpinner: boolean barSelector: string spinnerSelector: string parent: string template: string includeCSS: boolean color: string } export type UrlMethodPair = { url: string; method: Method } export type UseFormTransformCallback<TForm> = (data: TForm) => object export type UseFormWithPrecognitionArguments = | [Method | (() => Method), string | (() => string)] | [UrlMethodPair | (() => UrlMethodPair)] type UseFormInertiaArguments<TForm> = | [] | [data: TForm | (() => TForm)] | [rememberKey: string, data: TForm | (() => TForm)] type UseFormPrecognitionArguments<TForm> = | [urlMethodPair: UrlMethodPair | (() => UrlMethodPair), data: TForm | (() => TForm)] | [method: Method | (() => Method), url: string | (() => string), data: TForm | (() => TForm)] export type UseFormArguments<TForm> = UseFormInertiaArguments<TForm> | UseFormPrecognitionArguments<TForm> export type UseFormSubmitOptions = Omit<VisitOptions, 'data'> export type UseFormSubmitArguments = | [Method, string, UseFormSubmitOptions?] | [UrlMethodPair, UseFormSubmitOptions?] | [UseFormSubmitOptions?] export type FormComponentOptions = Pick< VisitOptions, 'preserveScroll' | 'preserveState' | 'preserveUrl' | 'replace' | 'only' | 'except' | 'reset' | 'viewTransition' > export type FormComponentProps = Partial< Pick<Visit, 'headers' | 'queryStringArrayFormat' | 'errorBag' | 'showProgress' | 'invalidateCacheTags'> & Omit<VisitCallbacks, 'onPrefetched' | 'onPrefetching'> > & { method?: Method | Uppercase<Method> action?: string | UrlMethodPair transform?: (data: Record<string, FormDataConvertible>) => Record<string, FormDataConvertible> options?: FormComponentOptions onSubmitComplete?: (props: FormComponentonSubmitCompleteArguments) => void disableWhileProcessing?: boolean resetOnSuccess?: boolean | string[] resetOnError?: boolean | string[] setDefaultsOnSuccess?: boolean validateFiles?: boolean validationTimeout?: number withAllErrors?: boolean | null } export type FormComponentMethods = { clearErrors: (...fields: string[]) => void resetAndClearErrors: (...fields: string[]) => void setError: { (field: string, value: ErrorValue): void (errors: Record<string, ErrorValue>): void } reset: (...fields: string[]) => void submit: () => void defaults: () => void getData: () => Record<string, FormDataConvertible> getFormData: () => FormData valid: (field: string) => boolean invalid: (field: string) => boolean validate: (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) => void touch: (...fields: string[]) => void touched: (field?: string) => boolean validator: () => Validator } export type FormComponentonSubmitCompleteArguments = Pick<FormComponentMethods, 'reset' | 'defaults'> export type FormComponentState = { errors: Record<string, ErrorValue> hasErrors: boolean processing: boolean progress: Progress | null wasSuccessful: boolean recentlySuccessful: boolean isDirty: boolean validating: boolean } export type FormComponentSlotProps = FormComponentMethods & FormComponentState export type FormComponentRef = FormComponentSlotProps export interface UseInfiniteScrollOptions { // Core data getPropName: () => string inReverseMode: () => boolean shouldFetchNext: () => boolean shouldFetchPrevious: () => boolean shouldPreserveUrl: () => boolean // Elements getTriggerMargin: () => number getStartElement: () => HTMLElement getEndElement: () => HTMLElement getItemsElement: () => HTMLElement getScrollableParent: () => HTMLElement | null // Callbacks onBeforePreviousRequest: () => void onBeforeNextRequest: () => void onCompletePreviousRequest: () => void onCompleteNextRequest: () => void onDataReset?: () => void } export interface UseInfiniteScrollDataManager { getLastLoadedPage: () => number | string | null getPageName: () => string getRequestCount: () => number hasPrevious: () => boolean hasNext: () => boolean fetchNext: (reloadOptions?: ReloadOptions) => void fetchPrevious: (reloadOptions?: ReloadOptions) => void removeEventListener: () => void } export interface UseInfiniteScrollElementManager { setupObservers: () => void enableTriggers: () => void disableTriggers: () => void refreshTriggers: () => void flushAll: () => void processManuallyAddedElements: () => void processServerLoadedElements: (loadedPage: string | number | null) => void } export interface UseInfiniteScrollProps { dataManager: UseInfiniteScrollDataManager elementManager: UseInfiniteScrollElementManager flush: () => void } export interface InfiniteScrollSlotProps { loading: boolean loadingPrevious: boolean loadingNext: boolean } export interface InfiniteScrollActionSlotProps { loading: boolean loadingPrevious: boolean loadingNext: boolean fetch: () => void autoMode: boolean manualMode: boolean hasMore: boolean hasPrevious: boolean hasNext: boolean } export interface InfiniteScrollRef { fetchNext: (reloadOptions?: ReloadOptions) => void fetchPrevious: (reloadOptions?: ReloadOptions) => void hasPrevious: () => boolean hasNext: () => boolean } export interface InfiniteScrollComponentBaseProps { data: string buffer?: number as?: string manual?: boolean manualAfter?: number preserveUrl?: boolean reverse?: boolean autoScroll?: boolean onlyNext?: boolean onlyPrevious?: boolean } declare global { interface DocumentEventMap { 'inertia:before': GlobalEvent<'before'> 'inertia:start': GlobalEvent<'start'> 'inertia:progress': GlobalEvent<'progress'> 'inertia:success': GlobalEvent<'success'> 'inertia:error': GlobalEvent<'error'> 'inertia:invalid': GlobalEvent<'invalid'> 'inertia:exception': GlobalEvent<'exception'> 'inertia:finish': GlobalEvent<'finish'> 'inertia:beforeUpdate': GlobalEvent<'beforeUpdate'> 'inertia:navigate': GlobalEvent<'navigate'> 'inertia:flash': GlobalEvent<'flash'> } } ================================================ FILE: packages/core/src/url.ts ================================================ import * as qs from 'qs' import { config } from './config' import { hasFiles } from './files' import { isFormData, objectToFormData } from './formData' import type { FormDataConvertible, Method, QueryStringArrayFormatOption, RequestPayload, UrlMethodPair, VisitOptions, } from './types' export function hrefToUrl(href: string | URL): URL { return new URL(href.toString(), typeof window === 'undefined' ? undefined : window.location.toString()) } export const transformUrlAndData = ( href: string | URL, data: RequestPayload, method: Method, forceFormData: VisitOptions['forceFormData'], queryStringArrayFormat: VisitOptions['queryStringArrayFormat'], ): [URL, RequestPayload] => { let url = typeof href === 'string' ? hrefToUrl(href) : href if ((hasFiles(data) || forceFormData) && !isFormData(data)) { if (config.get('form.forceIndicesArrayFormatInFormData')) { queryStringArrayFormat = 'indices' } data = objectToFormData(data, new FormData(), null, queryStringArrayFormat) } if (isFormData(data)) { return [url, data] } const [_href, _data] = mergeDataIntoQueryString(method, url, data, queryStringArrayFormat) return [hrefToUrl(_href), _data] } type MergeDataIntoQueryStringDataReturnType<T extends RequestPayload> = T extends Record<string, FormDataConvertible> ? Record<string, FormDataConvertible> : RequestPayload export function mergeDataIntoQueryString<T extends RequestPayload>( method: Method, href: URL | string, data: T, qsArrayFormat: QueryStringArrayFormatOption = 'brackets', ): [string, MergeDataIntoQueryStringDataReturnType<T>] { const hasDataForQueryString = method === 'get' && !isFormData(data) && Object.keys(data).length > 0 const hasHost = urlHasProtocol(href.toString()) const hasAbsolutePath = hasHost || href.toString().startsWith('/') || href.toString() === '' const hasRelativePath = !hasAbsolutePath && !href.toString().startsWith('#') && !href.toString().startsWith('?') const hasRelativePathWithDotPrefix = /^[.]{1,2}([/]|$)/.test(href.toString()) const hasSearch = href.toString().includes('?') || hasDataForQueryString const hasHash = href.toString().includes('#') const url = new URL(href.toString(), typeof window === 'undefined' ? 'http://localhost' : window.location.toString()) if (hasDataForQueryString) { // If the original URL contains indices notation (e.g. [0], [1]), preserve it. // Indices notation cannot be converted to brackets notation without data loss. // We decode the URL search first because browsers may return URL-encoded brackets (%5B0%5D). const hasIndices = /\[\d+\]/.test(decodeURIComponent(url.search)) const parseOptions = { ignoreQueryPrefix: true, allowSparse: true } url.search = qs.stringify( { ...qs.parse(url.search, parseOptions), ...data }, { encodeValuesOnly: true, arrayFormat: hasIndices ? 'indices' : qsArrayFormat, }, ) } return [ [ hasHost ? `${url.protocol}//${url.host}` : '', hasAbsolutePath ? url.pathname : '', hasRelativePath ? url.pathname.substring(hasRelativePathWithDotPrefix ? 0 : 1) : '', hasSearch ? url.search : '', hasHash ? url.hash : '', ].join(''), (hasDataForQueryString ? {} : data) as MergeDataIntoQueryStringDataReturnType<T>, ] } export function urlWithoutHash(url: URL | Location): URL { url = new URL(url.href) url.hash = '' return url } export const setHashIfSameUrl = (originUrl: URL | Location, destinationUrl: URL | Location) => { if (originUrl.hash && !destinationUrl.hash && urlWithoutHash(originUrl).href === destinationUrl.href) { destinationUrl.hash = originUrl.hash } } export const isSameUrlWithoutHash = (url1: URL | Location, url2: URL | Location): boolean => { return urlWithoutHash(url1).href === urlWithoutHash(url2).href } export const isSameUrlWithoutQueryOrHash = (url1: URL | Location, url2: URL | Location): boolean => { return url1.origin === url2.origin && url1.pathname === url2.pathname } export function isUrlMethodPair(href: unknown): href is UrlMethodPair { return href !== null && typeof href === 'object' && href !== undefined && 'url' in href && 'method' in href } export function urlHasProtocol(url: string): boolean { return /^([a-z][a-z0-9+.-]*:)?\/\/[^/]/i.test(url) } export function urlToString(url: URL | string, absolute: boolean): string { const urlObj = typeof url === 'string' ? hrefToUrl(url) : url return absolute ? `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}${urlObj.search}${urlObj.hash}` : `${urlObj.pathname}${urlObj.search}${urlObj.hash}` } ================================================ FILE: packages/core/src/useFormUtils.ts ================================================ import { NamedInputEvent, ValidationConfig } from 'laravel-precognition' import { FormDataType, Method, UrlMethodPair, UseFormArguments, UseFormSubmitArguments, UseFormSubmitOptions, } from './types' import { isUrlMethodPair } from './url' export class UseFormUtils { /** * Creates a callback that returns a UrlMethodPair. * * createWayfinderCallback(urlMethodPair) * createWayfinderCallback(method, url) * createWayfinderCallback(() => urlMethodPair) * createWayfinderCallback(() => method, () => url) */ public static createWayfinderCallback( ...args: [UrlMethodPair | (() => UrlMethodPair)] | [Method | (() => Method), string | (() => string)] ): () => UrlMethodPair { return () => { if (args.length === 1) { // Wayfinder object, return as-is or call function... return isUrlMethodPair(args[0]) ? args[0] : args[0]() } // Separate method and url, reconstruct Wayfinder object... return { method: typeof args[0] === 'function' ? args[0]() : args[0], url: typeof args[1] === 'function' ? args[1]() : args[1], } } } /** * Parses all useForm() arguments into { rememberKey, data, precognitionEndpoint }. * * useForm() * useForm(data) * useForm(rememberKey, data) * useForm(method, url, data) * useForm(urlMethodPair, data) * */ public static parseUseFormArguments<TForm extends FormDataType<TForm>>( ...args: UseFormArguments<TForm> ): { rememberKey: string | null data: TForm | (() => TForm) precognitionEndpoint: (() => UrlMethodPair) | null } { if (args.length === 0) { // Empty form: useForm() return { rememberKey: null, data: {} as TForm, precognitionEndpoint: null, } } if (args.length === 1) { // Basic form: useForm(data) return { rememberKey: null, data: args[0], precognitionEndpoint: null, } } if (args.length === 2) { if (typeof args[0] === 'string') { // Rememberable form: useForm(rememberKey, data) return { rememberKey: args[0], data: args[1], precognitionEndpoint: null, } } // Form with Precognition + Wayfinder: useForm(wayfinder, data) return { rememberKey: null, data: args[1], precognitionEndpoint: this.createWayfinderCallback(args[0]), } } // Form with Precognition: useForm(method, url, data) return { rememberKey: null, data: args[2], precognitionEndpoint: this.createWayfinderCallback(args[0], args[1]), } } /** * Parses all submission arguments into { method, url, options }. * It uses the Precognition endpoint if no explicit method/url are provided. * * form.submit(method, url) * form.submit(method, url, options) * form.submit(urlMethodPair) * form.submit(urlMethodPair, options) * form.submit() * form.submit(options) */ public static parseSubmitArguments( args: UseFormSubmitArguments, precognitionEndpoint: (() => UrlMethodPair) | null, ): { method: Method; url: string; options: UseFormSubmitOptions } { if (args.length === 3 || (args.length === 2 && typeof args[0] === 'string')) { // Explicit method and url provided... return { method: args[0], url: args[1], options: args[2] ?? {} } } if (isUrlMethodPair(args[0])) { // Wayfinder object provided... return { ...args[0], options: (args[1] as UseFormSubmitOptions) ?? {} } } // Use Precognition endpoint with optional options... return { ...precognitionEndpoint!(), options: (args[0] as UseFormSubmitOptions) ?? {} } } /** * Merges headers into the Precognition validate() arguments. */ public static mergeHeadersForValidation( field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig, headers?: Record<string, string>, ): [string | NamedInputEvent | ValidationConfig | undefined, ValidationConfig | undefined] { const merge = (config: ValidationConfig): ValidationConfig => { config.headers = { ...(headers ?? {}), ...(config.headers ?? {}), } return config } if (field && typeof field === 'object' && !('target' in field)) { field = merge(field) } else if (config && typeof config === 'object') { config = merge(config) } else if (typeof field === 'string') { config = merge(config ?? {}) } else { field = merge(field ?? {}) } return [field, config] } } ================================================ FILE: packages/core/tsconfig.json ================================================ { "compilerOptions": { "rootDir": "src", "noEmitOnError": true, "lib": ["DOM", "DOM.Iterable", "ES2020"], "target": "ES2020", "types": ["node"], "declaration": true, "declarationDir": "types", "emitDeclarationOnly": true, "module": "ES2020", "moduleResolution": "Node", "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "noImplicitThis": false, "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, "removeComments": false, "typeRoots": ["./node_modules/@types"], "strict": true, "skipLibCheck": true } } ================================================ FILE: packages/react/.gitignore ================================================ dist types node_modules package-lock.json yarn.lock ================================================ FILE: packages/react/LICENSE ================================================ MIT License Copyright (c) Jonathan Reinink <jonathan@reinink.ca> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/react/build.js ================================================ #!/usr/bin/env node import esbuild from 'esbuild' import { nodeExternalsPlugin } from 'esbuild-node-externals' import { readFileSync } from 'fs' const watch = process.argv.slice(1).includes('--watch') const withDeps = process.argv.slice(1).includes('--with-deps') // For regular builds, externalize all dependencies to keep the bundle size small (using nodeExternalsPlugin). // For builds with dependencies, only externalize peer dependencies and bundle everything // else so we can check ES2020 compatibility without checking framework code. let externalDependencies = undefined if (withDeps) { const pkg = JSON.parse(readFileSync('./package.json', 'utf8')) externalDependencies = Object.keys(pkg.peerDependencies || {}) } const config = { bundle: true, minify: false, sourcemap: withDeps ? false : true, target: 'es2020', external: externalDependencies, plugins: [ ...(withDeps ? [] : [nodeExternalsPlugin()]), { name: 'inertia', setup(build) { let count = 0 build.onEnd((result) => { if (count++ !== 0) { console.log(`Rebuilding ${build.initialOptions.entryPoints} (${build.initialOptions.format})…`) } }) }, }, ], } const builds = [ { entryPoints: ['src/index.ts'], format: 'esm', outfile: 'dist/index.esm.js', platform: 'browser' }, { entryPoints: ['src/index.ts'], format: 'cjs', outfile: 'dist/index.js', platform: 'browser' }, { entryPoints: ['src/server.ts'], format: 'esm', outfile: 'dist/server.esm.js', platform: 'node' }, { entryPoints: ['src/server.ts'], format: 'cjs', outfile: 'dist/server.js', platform: 'node' }, ] builds.forEach(async (build) => { const context = await esbuild.context({ ...config, ...build }) if (watch) { console.log(`Watching ${build.entryPoints} (${build.format})…`) await context.watch() } else { await context.rebuild() context.dispose() console.log(`Built ${build.entryPoints} (${build.format}) ${withDeps ? '(with-deps)' : ''}…`) } }) ================================================ FILE: packages/react/package.json ================================================ { "name": "@inertiajs/react", "version": "2.3.18", "license": "MIT", "description": "The React adapter for Inertia.js", "contributors": [ "Jonathan Reinink <jonathan@reinink.ca>", "Sebastian De Deyne <sebastiandedeyne@gmail.com>" ], "homepage": "https://inertiajs.com/", "repository": { "type": "git", "url": "https://github.com/inertiajs/inertia.git", "directory": "packages/react" }, "bugs": { "url": "https://github.com/inertiajs/inertia/issues" }, "files": [ "dist", "types", "resources" ], "type": "module", "main": "dist/index.js", "types": "types/index.d.ts", "exports": { ".": { "types": "./types/index.d.ts", "import": "./dist/index.esm.js", "require": "./dist/index.js" }, "./server": { "types": "./types/server.d.ts", "import": "./dist/server.esm.js", "require": "./dist/server.js" } }, "typesVersions": { "*": { "server": [ "types/server.d.ts" ] } }, "scripts": { "build": "pnpm clean && ./build.js && tsc", "build:with-deps": "./build.js --with-deps", "clean": "rm -rf types && rm -rf dist", "dev": "pnpx concurrently -c \"#ffcf00,#3178c6\" \"pnpm dev:build\" \"pnpm dev:types\" --names build,types", "dev:build": "./build.js --watch", "dev:types": "tsc --watch --preserveWatchOutput", "es2020-check": "pnpm build:with-deps && es-check es2020 \"dist/index.esm.js\" --checkFeatures --module --noCache --verbose" }, "devDependencies": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "axios": "^1.13.5", "es-check": "^9.6.1", "esbuild": "^0.27.3", "esbuild-node-externals": "^1.20.1", "react": "^19.2.4", "react-dom": "^19.2.4", "typescript": "^5.9.3" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "dependencies": { "@inertiajs/core": "workspace:*", "@types/lodash-es": "^4.17.12", "laravel-precognition": "^1.0.2", "lodash-es": "^4.17.23" } } ================================================ FILE: packages/react/readme.md ================================================ # Inertia.js React Adapter Visit [inertiajs.com](https://inertiajs.com/) to learn more. ================================================ FILE: packages/react/resources/boost/guidelines/core.blade.php ================================================ # Inertia + React - IMPORTANT: Activate `inertia-react-development` when working with Inertia React client-side patterns. ================================================ FILE: packages/react/resources/boost/skills/inertia-react-development/SKILL.blade.php ================================================ --- name: inertia-react-development description: "Develops Inertia.js v2 React client-side applications. Activates when creating React pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions React with Inertia, React pages, React forms, or React navigation." license: MIT metadata: author: laravel --- @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp # Inertia React Development ## When to Apply Activate this skill when: - Creating or modifying React page components for Inertia - Working with forms in React (using `<Form>` or `useForm`) - Implementing client-side navigation with `<Link>` or `router` - Using v2 features: deferred props, prefetching, WhenVisible, InfiniteScroll, once props, flash data, or polling - Building React-specific features with the Inertia protocol ## Documentation Use `search-docs` for detailed Inertia v2 React patterns and documentation. ## Basic Usage ### Page Components Location React page components should be placed in the `{{ $assist->inertia()->pagesDirectory() }}` directory. ### Page Component Structure @boostsnippet("Basic React Page Component", "react") export default function UsersIndex({ users }) { return ( <div> <h1>Users</h1> <ul> {users.map(user => <li key={user.id}>{user.name}</li>)} </ul> </div> ) } @endboostsnippet ## Client-Side Navigation ### Basic Link Component Use `<Link>` for client-side navigation instead of traditional `<a>` tags: @boostsnippet("Inertia React Navigation", "react") import { Link, router } from '@inertiajs/react' <Link href="/">Home</Link> <Link href="/users">Users</Link> <Link href={`/users/${user.id}`}>View User</Link> @endboostsnippet ### Link with Method @boostsnippet("Link with POST Method", "react") import { Link } from '@inertiajs/react' <Link href="/logout" method="post" as="button"> Logout </Link> @endboostsnippet ### Prefetching Prefetch pages to improve perceived performance: @boostsnippet("Prefetch on Hover", "react") import { Link } from '@inertiajs/react' <Link href="/users" prefetch> Users </Link> @endboostsnippet ### Programmatic Navigation @boostsnippet("Router Visit", "react") import { router } from '@inertiajs/react' function handleClick() { router.visit('/users') } // Or with options router.visit('/users', { method: 'post', data: { name: 'John' }, onSuccess: () => console.log('Success!'), }) @endboostsnippet ## Form Handling @if($assist->inertia()->hasFormComponent()) ### Form Component (Recommended) The recommended way to build forms is with the `<Form>` component: @boostsnippet("Form Component Example", "react") import { Form } from '@inertiajs/react' export default function CreateUser() { return ( <Form action="/users" method="post"> {({ errors, processing, wasSuccessful }) => ( <> <input type="text" name="name" /> {errors.name && <div>{errors.name}</div>} <input type="email" name="email" /> {errors.email && <div>{errors.email}</div>} <button type="submit" disabled={processing}> {processing ? 'Creating...' : 'Create User'} </button> {wasSuccessful && <div>User created!</div>} </> )} </Form> ) } @endboostsnippet ### Form Component With All Props @boostsnippet("Form Component Full Example", "react") import { Form } from '@inertiajs/react' <Form action="/users" method="post"> {({ errors, hasErrors, processing, progress, wasSuccessful, recentlySuccessful, clearErrors, resetAndClearErrors, defaults, isDirty, reset, submit }) => ( <> <input type="text" name="name" defaultValue={defaults.name} /> {errors.name && <div>{errors.name}</div>} <button type="submit" disabled={processing}> {processing ? 'Saving...' : 'Save'} </button> {progress && ( <progress value={progress.percentage} max="100"> {progress.percentage}% </progress> )} {wasSuccessful && <div>Saved!</div>} </> )} </Form> @endboostsnippet @if($assist->inertia()->hasFormComponentResets()) ### Form Component Reset Props The `<Form>` component supports automatic resetting: - `resetOnError` - Reset form data when the request fails - `resetOnSuccess` - Reset form data when the request succeeds - `setDefaultsOnSuccess` - Update default values on success Use the `search-docs` tool with a query of `form component resetting` for detailed guidance. @boostsnippet("Form with Reset Props", "react") import { Form } from '@inertiajs/react' <Form action="/users" method="post" resetOnSuccess setDefaultsOnSuccess > {({ errors, processing, wasSuccessful }) => ( <> <input type="text" name="name" /> {errors.name && <div>{errors.name}</div>} <button type="submit" disabled={processing}> Submit </button> </> )} </Form> @endboostsnippet @else Note: This version of Inertia does not support `resetOnError`, `resetOnSuccess`, or `setDefaultsOnSuccess` on the `<Form>` component. Using these props will cause errors. Upgrade to Inertia v2.2.0+ to use these features. @endif Forms can also be built using the `useForm` helper for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance. @endif ### `useForm` Hook @if($assist->inertia()->hasFormComponent() === false) For Inertia v2.0.x: Build forms using the `useForm` helper as the `<Form>` component is not available until v2.1.0+. @else For more programmatic control or to follow existing conventions, use the `useForm` hook: @endif @boostsnippet("useForm Hook Example", "react") import { useForm } from '@inertiajs/react' export default function CreateUser() { const { data, setData, post, processing, errors, reset } = useForm({ name: '', email: '', password: '', }) function submit(e) { e.preventDefault() post('/users', { onSuccess: () => reset('password'), }) } return ( <form onSubmit={submit}> <input type="text" value={data.name} onChange={e => setData('name', e.target.value)} /> {errors.name && <div>{errors.name}</div>} <input type="email" value={data.email} onChange={e => setData('email', e.target.value)} /> {errors.email && <div>{errors.email}</div>} <input type="password" value={data.password} onChange={e => setData('password', e.target.value)} /> {errors.password && <div>{errors.password}</div>} <button type="submit" disabled={processing}> Create User </button> </form> ) } @endboostsnippet ## Inertia v2 Features ### Deferred Props Use deferred props to load data after initial page render: @boostsnippet("Deferred Props with Empty State", "react") export default function UsersIndex({ users }) { // users will be undefined initially, then populated return ( <div> <h1>Users</h1> {!users ? ( <div className="animate-pulse"> <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div> <div className="h-4 bg-gray-200 rounded w-1/2"></div> </div> ) : ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> )} </div> ) } @endboostsnippet ### Polling Automatically refresh data at intervals: @boostsnippet("Polling Example", "react") import { router } from '@inertiajs/react' import { useEffect } from 'react' export default function Dashboard({ stats }) { useEffect(() => { const interval = setInterval(() => { router.reload({ only: ['stats'] }) }, 5000) // Poll every 5 seconds return () => clearInterval(interval) }, []) return ( <div> <h1>Dashboard</h1> <div>Active Users: {stats.activeUsers}</div> </div> ) } @endboostsnippet ### WhenVisible Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold: @boostsnippet("WhenVisible Example", "react") import { WhenVisible } from '@inertiajs/react' export default function Dashboard({ stats }) { return ( <div> <h1>Dashboard</h1> {/* stats prop is loaded only when this section scrolls into view */} <WhenVisible data="stats" buffer={200} fallback={<div className="animate-pulse">Loading stats...</div>}> {({ fetching }) => ( <div> <p>Total Users: {stats.total_users}</p> <p>Revenue: {stats.revenue}</p> {fetching && <span>Refreshing...</span>} </div> )} </WhenVisible> </div> ) } @endboostsnippet ## Server-Side Patterns Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines. ## Common Pitfalls - Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior) - Forgetting to add loading states (skeleton screens) when using deferred props - Not handling the `undefined` state of deferred props before data loads - Using `<form>` without preventing default submission (use `<Form>` component or `e.preventDefault()`) - Forgetting to check if `<Form>` component is available in your Inertia version ================================================ FILE: packages/react/src/App.ts ================================================ import { createHeadManager, HeadManagerOnUpdateCallback, HeadManagerTitleCallback, Page, PageHandler, PageProps, router, } from '@inertiajs/core' import { createElement, FunctionComponent, ReactNode, useEffect, useMemo, useState } from 'react' import { flushSync } from 'react-dom' import HeadContext from './HeadContext' import PageContext from './PageContext' import { LayoutFunction, ReactComponent, ReactPageHandlerArgs } from './types' let currentIsInitialPage = true let routerIsInitialized = false let swapComponent: PageHandler<ReactComponent> = async () => { // Dummy function so we can init the router outside of the useEffect hook. This is // needed so `router.reload()` works right away (on mount) in any of the user's // components. We swap in the real function in the useEffect hook below. currentIsInitialPage = false } type CurrentPage = { component: ReactComponent | null page: Page key: number | null } export interface InertiaAppProps<SharedProps extends PageProps = PageProps> { children?: (options: { Component: ReactComponent; props: PageProps; key: number | null }) => ReactNode initialPage: Page<SharedProps> initialComponent?: ReactComponent resolveComponent?: (name: string) => ReactComponent | Promise<ReactComponent> titleCallback?: HeadManagerTitleCallback onHeadUpdate?: HeadManagerOnUpdateCallback } export type InertiaApp = FunctionComponent<InertiaAppProps> export default function App<SharedProps extends PageProps = PageProps>({ children, initialPage, initialComponent, resolveComponent, titleCallback, onHeadUpdate, }: InertiaAppProps<SharedProps>) { const [current, setCurrent] = useState<CurrentPage>({ component: initialComponent || null, page: { ...initialPage, flash: initialPage.flash ?? {} }, key: null, }) const headManager = useMemo(() => { return createHeadManager( typeof window === 'undefined', titleCallback || ((title) => title), onHeadUpdate || (() => {}), ) }, []) if (!routerIsInitialized) { router.init<ReactComponent>({ initialPage, resolveComponent: resolveComponent!, swapComponent: async (args) => swapComponent(args), onFlash: (flash) => { setCurrent((current) => ({ ...current, page: { ...current.page, flash }, })) }, }) routerIsInitialized = true } useEffect(() => { swapComponent = async ({ component, page, preserveState }: ReactPageHandlerArgs) => { if (currentIsInitialPage) { // We block setting the current page on the initial page to // prevent the initial page from being re-rendered again. currentIsInitialPage = false return } flushSync(() => setCurrent((current) => ({ component, page, key: preserveState ? current.key : Date.now(), })), ) } router.on('navigate', () => headManager.forceUpdate()) }, []) if (!current.component) { return createElement( HeadContext.Provider, { value: headManager }, createElement(PageContext.Provider, { value: current.page }, null), ) } const renderChildren = children || (({ Component, props, key }) => { const child = createElement(Component, { key, ...props }) if (typeof Component.layout === 'function') { return (Component.layout as LayoutFunction)(child) } if (Array.isArray(Component.layout)) { return (Component.layout as any) .concat(child) .reverse() .reduce((children: any, Layout: any) => createElement(Layout, { children, ...props })) } return child }) return createElement( HeadContext.Provider, { value: headManager }, createElement( PageContext.Provider, { value: current.page }, renderChildren({ Component: current.component, key: current.key, props: current.page.props, }), ), ) } App.displayName = 'Inertia' ================================================ FILE: packages/react/src/Deferred.ts ================================================ import { ReactNode, useEffect, useMemo, useState } from 'react' import { router } from '.' import usePage from './usePage' const urlWithoutHash = (url: URL | Location): URL => { url = new URL(url.href) url.hash = '' return url } const isSameUrlWithoutHash = (url1: URL | Location, url2: URL | Location): boolean => { return urlWithoutHash(url1).href === urlWithoutHash(url2).href } interface DeferredProps { children: ReactNode | (() => ReactNode) fallback: ReactNode | (() => ReactNode) data: string | string[] } const Deferred = ({ children, data, fallback }: DeferredProps) => { if (!data) { throw new Error('`<Deferred>` requires a `data` prop to be a string or array of strings') } const [loaded, setLoaded] = useState(false) const pageProps = usePage().props const keys = useMemo(() => (Array.isArray(data) ? data : [data]), [data]) useEffect(() => { const removeListener = router.on('start', (e) => { const isPartialVisit = e.detail.visit.only.length > 0 || e.detail.visit.except.length > 0 const isReloadingKey = e.detail.visit.only.find((key) => keys.includes(key)) if (isSameUrlWithoutHash(e.detail.visit.url, window.location) && (!isPartialVisit || isReloadingKey)) { setLoaded(false) } }) return () => { removeListener() } }, []) useEffect(() => { setLoaded(keys.every((key) => pageProps[key] !== undefined)) }, [pageProps, keys]) // Always check that props are actually defined before rendering children, // even if loaded is true, to prevent race conditions during reloads const propsAreDefined = useMemo(() => keys.every((key) => pageProps[key] !== undefined), [keys, pageProps]) if (loaded && propsAreDefined) { return typeof children === 'function' ? children() : children } return typeof fallback === 'function' ? fallback() : fallback } Deferred.displayName = 'InertiaDeferred' export default Deferred ================================================ FILE: packages/react/src/Form.ts ================================================ import { config, FormComponentProps, FormComponentRef, FormComponentResetSymbol, FormComponentSlotProps, FormDataConvertible, formDataToObject, isUrlMethodPair, mergeDataIntoQueryString, Method, resetFormFields, UseFormUtils, VisitOptions, } from '@inertiajs/core' import { NamedInputEvent, ValidationConfig } from 'laravel-precognition' import { isEqual } from 'lodash-es' import React, { createContext, createElement, FormEvent, forwardRef, ReactNode, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react' import { isReact19 } from './react' import useForm from './useForm' // Polyfill for startTransition to support React 16.9+ const deferStateUpdate = (callback: () => void) => { typeof React.startTransition === 'function' ? React.startTransition(callback) : setTimeout(callback, 0) } type ComponentProps = (FormComponentProps & Omit<React.FormHTMLAttributes<HTMLFormElement>, keyof FormComponentProps | 'children'> & Omit<React.AllHTMLAttributes<HTMLFormElement>, keyof FormComponentProps | 'children'>) & { children: ReactNode | ((props: FormComponentSlotProps) => ReactNode) } type FormSubmitOptions = Omit<VisitOptions, 'data' | 'onPrefetched' | 'onPrefetching'> type FormSubmitter = HTMLElement | null const noop = () => undefined const FormContext = createContext<FormComponentRef | undefined>(undefined) const Form = forwardRef<FormComponentRef, ComponentProps>( ( { action = '', method = 'get', headers = {}, queryStringArrayFormat = 'brackets', errorBag = null, showProgress = true, transform = (data) => data, options = {}, onStart = noop, onProgress = noop, onFinish = noop, onBefore = noop, onCancel = noop, onSuccess = noop, onError = noop, onCancelToken = noop, onSubmitComplete = noop, disableWhileProcessing = false, resetOnError = false, resetOnSuccess = false, setDefaultsOnSuccess = false, invalidateCacheTags = [], validateFiles = false, validationTimeout = 1500, withAllErrors = null, children, ...props }, ref, ) => { const getTransformedData = (): Record<string, FormDataConvertible> => { const [_url, data] = getUrlAndData() return transform(data) } const form = useForm<Record<string, any>>({}) .withPrecognition( () => resolvedMethod, () => getUrlAndData()[0], ) .setValidationTimeout(validationTimeout) if (validateFiles) { form.validateFiles() } if (withAllErrors ?? config.get('form.withAllErrors')) { form.withAllErrors() } form.transform(getTransformedData) const formElement = useRef<HTMLFormElement>(undefined) const resolvedMethod = useMemo(() => { return isUrlMethodPair(action) ? action.method : (method.toLowerCase() as Method) }, [action, method]) const [isDirty, setIsDirty] = useState(false) const defaultData = useRef<FormData>(new FormData()) const getFormData = (submitter?: FormSubmitter): FormData => new FormData(formElement.current, submitter) // Convert the FormData to an object because we can't compare two FormData // instances directly (which is needed for isDirty), mergeDataIntoQueryString() // expects an object, and submitting a FormData instance directly causes problems with nested objects. const getData = (submitter?: FormSubmitter): Record<string, FormDataConvertible> => formDataToObject(getFormData(submitter)) const getUrlAndData = (submitter?: FormSubmitter): [string, Record<string, FormDataConvertible>] => { return mergeDataIntoQueryString( resolvedMethod, isUrlMethodPair(action) ? action.url : action, getData(submitter), queryStringArrayFormat, ) } const updateDirtyState = (event: Event) => { if (event.type === 'reset' && (event as CustomEvent).detail?.[FormComponentResetSymbol]) { // When the form is reset programmatically, prevent native reset behavior event.preventDefault() } deferStateUpdate(() => setIsDirty(event.type === 'reset' ? false : !isEqual(getData(), formDataToObject(defaultData.current))), ) } const clearErrors = (...names: string[]) => { form.clearErrors(...names) return form } useEffect(() => { defaultData.current = getFormData() form.setDefaults(getData()) const formEvents: Array<keyof HTMLElementEventMap> = ['input', 'change', 'reset'] formEvents.forEach((e) => formElement.current!.addEventListener(e, updateDirtyState)) return () => { formEvents.forEach((e) => formElement.current?.removeEventListener(e, updateDirtyState)) } }, []) useEffect(() => { form.setValidationTimeout(validationTimeout) }, [validationTimeout]) useEffect(() => { if (validateFiles) { form.validateFiles() } else { form.withoutFileValidation() } }, [validateFiles]) const reset = (...fields: string[]) => { if (formElement.current) { resetFormFields(formElement.current, defaultData.current, fields) } form.reset(...fields) } const resetAndClearErrors = (...fields: string[]) => { clearErrors(...fields) reset(...fields) } const maybeReset = (resetOption: boolean | string[]) => { if (!resetOption) { return } if (resetOption === true) { reset() } else if (resetOption.length > 0) { reset(...resetOption) } } const submit = (submitter?: FormSubmitter) => { const [url, data] = getUrlAndData(submitter) const formTarget = (submitter as HTMLButtonElement | HTMLInputElement | null)?.getAttribute('formtarget') if (formTarget === '_blank' && resolvedMethod === 'get') { window.open(url, '_blank') return } const submitOptions: FormSubmitOptions = { headers, queryStringArrayFormat, errorBag, showProgress, invalidateCacheTags, onCancelToken, onBefore, onStart, onProgress, onFinish, onCancel, onSuccess: (...args) => { onSuccess(...args) onSubmitComplete({ reset, defaults, }) maybeReset(resetOnSuccess) if (setDefaultsOnSuccess === true) { defaults() } }, onError(...args) { onError(...args) maybeReset(resetOnError) }, ...options, } // We need transform because we can't override the default data with different keys (by design) form.transform(() => transform(data)) form.submit(resolvedMethod, url, submitOptions) // Reset the transformer back so the submitter is not used for future submissions form.transform(getTransformedData) } const defaults = () => { defaultData.current = getFormData() setIsDirty(false) } const exposed = { errors: form.errors, hasErrors: form.hasErrors, processing: form.processing, progress: form.progress, wasSuccessful: form.wasSuccessful, recentlySuccessful: form.recentlySuccessful, isDirty, clearErrors, resetAndClearErrors, setError: form.setError, reset, submit, defaults, getData, getFormData, // Precognition validator: () => form.validator(), validating: form.validating, valid: form.valid, invalid: form.invalid, validate: (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) => form.validate(...UseFormUtils.mergeHeadersForValidation(field, config, headers)), touch: form.touch, touched: form.touched, } useImperativeHandle(ref, () => exposed, [form, isDirty, submit]) const formNode = createElement( 'form', { ...props, ref: formElement, action: isUrlMethodPair(action) ? action.url : action, method: resolvedMethod, onSubmit: (event: FormEvent<HTMLFormElement>) => { event.preventDefault() submit((event.nativeEvent as SubmitEvent).submitter) }, // React 19 supports passing a boolean to the `inert` attribute, but shows // a warning when receiving a string. Earlier versions require the string 'true'. // See: https://github.com/inertiajs/inertia/pull/2536 inert: disableWhileProcessing && form.processing && (isReact19 ? true : 'true'), }, typeof children === 'function' ? children(exposed) : children, ) return createElement(FormContext.Provider, { value: exposed }, formNode) }, ) Form.displayName = 'InertiaForm' export function useFormContext(): FormComponentRef | undefined { return useContext(FormContext) } export default Form ================================================ FILE: packages/react/src/Head.ts ================================================ import { escape } from 'lodash-es' import React, { FunctionComponent, ReactElement, ReactNode, useContext, useEffect, useMemo } from 'react' import HeadContext from './HeadContext' type InertiaHeadProps = { title?: string children?: ReactNode } type InertiaHead = FunctionComponent<InertiaHeadProps> const Head: InertiaHead = function ({ children, title }) { const headManager = useContext(HeadContext) const provider = useMemo(() => headManager!.createProvider(), [headManager]) const isServer = typeof window === 'undefined' useEffect(() => { provider.reconnect() provider.update(renderNodes(children)) return () => { provider.disconnect() } }, [provider, children, title]) function isUnaryTag(node: ReactElement<any>) { return ( typeof node.type === 'string' && [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr', ].indexOf(node.type) > -1 ) } function renderTagStart(node: ReactElement<any>): string { const attrs = Object.keys(node.props).reduce((carry, name) => { if (['head-key', 'children', 'dangerouslySetInnerHTML'].includes(name)) { return carry } const value = String(node.props[name]) if (value === '') { return carry + ` ${name}` } return carry + ` ${name}="${escape(value)}"` }, '') return `<${String(node.type)}${attrs}>` } function renderTagChildren(node: ReactElement<any>): string { const { children } = node.props if (typeof children === 'string') { return children } if (Array.isArray(children)) { return children.reduce((html, child) => html + renderTag(child), '') } return '' } function renderTag(node: ReactElement<any>): string { let html = renderTagStart(node) if (node.props.children) { html += renderTagChildren(node) } if (node.props.dangerouslySetInnerHTML) { html += node.props.dangerouslySetInnerHTML.__html } if (!isUnaryTag(node)) { html += `</${String(node.type)}>` } return html } function ensureNodeHasInertiaProp(node: ReactElement<any>) { return React.cloneElement(node, { [provider.preferredAttribute()]: node.props['head-key'] !== undefined ? node.props['head-key'] : '', }) } function renderNode(node: ReactElement<any>) { return renderTag(ensureNodeHasInertiaProp(node)) } function renderNodes(nodes: ReactNode) { const elements = React.Children.toArray(nodes) .filter((node) => node) .map((node) => renderNode(node as ReactElement<any>)) if (title && !elements.find((tag) => tag.startsWith('<title'))) { elements.push(`<title ${provider.preferredAttribute()}>${title}`) } return elements } if (isServer) { provider.update(renderNodes(children)) } return null } export default Head ================================================ FILE: packages/react/src/HeadContext.ts ================================================ import { HeadManager } from '@inertiajs/core' import { createContext } from 'react' const headContext = createContext(null) headContext.displayName = 'InertiaHeadContext' export default headContext ================================================ FILE: packages/react/src/InfiniteScroll.ts ================================================ import { getScrollableParent, InfiniteScrollActionSlotProps, InfiniteScrollComponentBaseProps, InfiniteScrollRef, InfiniteScrollSlotProps, useInfiniteScroll, UseInfiniteScrollProps, } from '@inertiajs/core' import React, { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react' const resolveHTMLElement = ( value: string | React.RefObject | null, fallback: HTMLElement | null, ): HTMLElement | null => { if (!value) { return fallback } // React ref object { current: HTMLElement | null } if (value && typeof value === 'object' && 'current' in value) { return value.current } // CSS Selector string if (typeof value === 'string') { return document.querySelector(value) as HTMLElement | null } return fallback } // Helper function to render slot content const renderSlot = ( slotContent: React.ReactNode | ((props: InfiniteScrollActionSlotProps) => React.ReactNode) | undefined, slotProps: InfiniteScrollActionSlotProps, fallback: React.ReactNode = null, ): React.ReactNode => { if (!slotContent) { return fallback } return typeof slotContent === 'function' ? slotContent(slotProps) : slotContent } interface ComponentProps extends InfiniteScrollComponentBaseProps, Omit, keyof InfiniteScrollComponentBaseProps | 'children'> { children?: React.ReactNode | ((props: InfiniteScrollSlotProps) => React.ReactNode) // Element references for custom trigger detection (when you want different trigger elements) startElement?: string | React.RefObject endElement?: string | React.RefObject itemsElement?: string | React.RefObject // Render slots for UI components (when you want custom loading/action components) previous?: React.ReactNode | ((props: InfiniteScrollActionSlotProps) => React.ReactNode) next?: React.ReactNode | ((props: InfiniteScrollActionSlotProps) => React.ReactNode) loading?: React.ReactNode | ((props: InfiniteScrollActionSlotProps) => React.ReactNode) onlyNext?: boolean onlyPrevious?: boolean } const InfiniteScroll = forwardRef( ( { data, buffer = 0, as = 'div', manual = false, manualAfter = 0, preserveUrl = false, reverse = false, autoScroll, children, startElement, endElement, itemsElement, previous, next, loading, onlyNext = false, onlyPrevious = false, ...props }, ref, ) => { const [startElementFromRef, setStartElementFromRef] = useState(null) const startElementRef = useCallback((node: HTMLElement | null) => setStartElementFromRef(node), []) const [endElementFromRef, setEndElementFromRef] = useState(null) const endElementRef = useCallback((node: HTMLElement | null) => setEndElementFromRef(node), []) const [itemsElementFromRef, setItemsElementFromRef] = useState(null) const itemsElementRef = useCallback((node: HTMLElement | null) => setItemsElementFromRef(node), []) const [loadingPrevious, setLoadingPrevious] = useState(false) const [loadingNext, setLoadingNext] = useState(false) const [requestCount, setRequestCount] = useState(0) const [hasPreviousPage, setHasPreviousPage] = useState(false) const [hasNextPage, setHasNextPage] = useState(false) const [resolvedStartElement, setResolvedStartElement] = useState(null) const [resolvedEndElement, setResolvedEndElement] = useState(null) const [resolvedItemsElement, setResolvedItemsElement] = useState(null) // Update elements when refs or props change useEffect(() => { const element = startElement ? resolveHTMLElement(startElement, startElementFromRef) : startElementFromRef setResolvedStartElement(element) }, [startElement, startElementFromRef]) useEffect(() => { const element = endElement ? resolveHTMLElement(endElement, endElementFromRef) : endElementFromRef setResolvedEndElement(element) }, [endElement, endElementFromRef]) useEffect(() => { const element = itemsElement ? resolveHTMLElement(itemsElement, itemsElementFromRef) : itemsElementFromRef setResolvedItemsElement(element) }, [itemsElement, itemsElementFromRef]) const scrollableParent = useMemo(() => getScrollableParent(resolvedItemsElement), [resolvedItemsElement]) const callbackPropsRef = useRef({ buffer, onlyNext, onlyPrevious, reverse, preserveUrl, }) callbackPropsRef.current = { buffer, onlyNext, onlyPrevious, reverse, preserveUrl, } const [infiniteScroll, setInfiniteScroll] = useState(null) const dataManager = useMemo(() => infiniteScroll?.dataManager, [infiniteScroll]) const elementManager = useMemo(() => infiniteScroll?.elementManager, [infiniteScroll]) const scrollToBottom = useCallback(() => { if (scrollableParent) { scrollableParent.scrollTo({ top: scrollableParent.scrollHeight, behavior: 'instant', }) } else { window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant', }) } }, [scrollableParent]) // Main setup effect - only recreate when structural dependencies change useEffect(() => { if (!resolvedItemsElement) { return } function syncStateFromDataManager() { setRequestCount(infiniteScrollInstance.dataManager.getRequestCount()) setHasPreviousPage(infiniteScrollInstance.dataManager.hasPrevious()) setHasNextPage(infiniteScrollInstance.dataManager.hasNext()) } const infiniteScrollInstance = useInfiniteScroll({ // Data getPropName: () => data, inReverseMode: () => callbackPropsRef.current.reverse, shouldFetchNext: () => !callbackPropsRef.current.onlyPrevious, shouldFetchPrevious: () => !callbackPropsRef.current.onlyNext, shouldPreserveUrl: () => callbackPropsRef.current.preserveUrl, // Elements getTriggerMargin: () => callbackPropsRef.current.buffer, getStartElement: () => resolvedStartElement!, getEndElement: () => resolvedEndElement!, getItemsElement: () => resolvedItemsElement, getScrollableParent: () => scrollableParent, // Callbacks onBeforePreviousRequest: () => setLoadingPrevious(true), onBeforeNextRequest: () => setLoadingNext(true), onCompletePreviousRequest: () => { setLoadingPrevious(false) syncStateFromDataManager() }, onCompleteNextRequest: () => { setLoadingNext(false) syncStateFromDataManager() }, onDataReset: syncStateFromDataManager, }) setInfiniteScroll(infiniteScrollInstance) const { dataManager, elementManager } = infiniteScrollInstance syncStateFromDataManager() elementManager.setupObservers() elementManager.processServerLoadedElements(dataManager.getLastLoadedPage()) if (autoLoad) { elementManager.enableTriggers() } return () => { infiniteScrollInstance.flush() setInfiniteScroll(null) } }, [data, resolvedItemsElement, resolvedStartElement, resolvedEndElement, scrollableParent]) const manualMode = useMemo( () => manual || (manualAfter > 0 && requestCount >= manualAfter), [manual, manualAfter, requestCount], ) const autoLoad = useMemo(() => !manualMode, [manualMode]) useEffect(() => { autoLoad ? elementManager?.enableTriggers() : elementManager?.disableTriggers() }, [autoLoad, onlyNext, onlyPrevious, resolvedStartElement, resolvedEndElement]) useEffect(() => { // autoScroll defaults to reverse value if not explicitly set const shouldAutoScroll = autoScroll !== undefined ? autoScroll : reverse if (shouldAutoScroll) { scrollToBottom() } }, [scrollableParent]) useImperativeHandle( ref, () => ({ fetchNext: dataManager?.fetchNext || (() => {}), fetchPrevious: dataManager?.fetchPrevious || (() => {}), hasPrevious: dataManager?.hasPrevious || (() => false), hasNext: dataManager?.hasNext || (() => false), }), [dataManager], ) const headerAutoMode = autoLoad && !onlyNext const footerAutoMode = autoLoad && !onlyPrevious const sharedExposed: Pick< InfiniteScrollActionSlotProps, 'loadingPrevious' | 'loadingNext' | 'hasPrevious' | 'hasNext' > = { loadingPrevious, loadingNext, hasPrevious: hasPreviousPage, hasNext: hasNextPage, } const exposedPrevious: InfiniteScrollActionSlotProps = { loading: loadingPrevious, fetch: dataManager?.fetchPrevious ?? (() => {}), autoMode: headerAutoMode, manualMode: !headerAutoMode, hasMore: hasPreviousPage, ...sharedExposed, } const exposedNext: InfiniteScrollActionSlotProps = { loading: loadingNext, fetch: dataManager?.fetchNext ?? (() => {}), autoMode: footerAutoMode, manualMode: !footerAutoMode, hasMore: hasNextPage, ...sharedExposed, } const exposedSlot: InfiniteScrollSlotProps = { loading: loadingPrevious || loadingNext, loadingPrevious, loadingNext, } const renderElements = [] // Only render previous trigger if not using custom element selector/ref if (!startElement) { renderElements.push( createElement( 'div', { ref: startElementRef }, // Render previous slot or fallback to loading indicator renderSlot(previous, exposedPrevious, loadingPrevious ? renderSlot(loading, exposedPrevious) : null), ), ) } renderElements.push( createElement( as, { ...props, ref: itemsElementRef }, typeof children === 'function' ? children(exposedSlot) : children, ), ) // Only render next trigger if not using custom element selector/ref if (!endElement) { renderElements.push( createElement( 'div', { ref: endElementRef }, // Render next slot or fallback to loading indicator renderSlot(next, exposedNext, loadingNext ? renderSlot(loading, exposedNext) : null), ), ) } return createElement(React.Fragment, {}, ...(reverse ? [...renderElements].reverse() : renderElements)) }, ) InfiniteScroll.displayName = 'InertiaInfiniteScroll' export default InfiniteScroll ================================================ FILE: packages/react/src/Link.ts ================================================ import { ActiveVisit, isUrlMethodPair, LinkComponentBaseProps, LinkPrefetchOption, mergeDataIntoQueryString, Method, PendingVisit, router, shouldIntercept, shouldNavigate, VisitOptions, } from '@inertiajs/core' import { createElement, ElementType, forwardRef, useEffect, useMemo, useRef, useState } from 'react' import { config } from '.' const noop = () => undefined interface BaseInertiaLinkProps extends LinkComponentBaseProps { as?: ElementType onClick?: (event: React.MouseEvent) => void } export type InertiaLinkProps = BaseInertiaLinkProps & Omit, keyof BaseInertiaLinkProps> & Omit, keyof BaseInertiaLinkProps> const Link = forwardRef( ( { children, as = 'a', data = {}, href = '', method = 'get', preserveScroll = false, preserveState = null, preserveUrl = false, replace = false, only = [], except = [], headers = {}, queryStringArrayFormat = 'brackets', async = false, onClick = noop, onCancelToken = noop, onBefore = noop, onStart = noop, onProgress = noop, onFinish = noop, onCancel = noop, onSuccess = noop, onError = noop, onPrefetching = noop, onPrefetched = noop, prefetch = false, cacheFor = 0, cacheTags = [], viewTransition = false, ...props }, ref, ) => { const [inFlightCount, setInFlightCount] = useState(0) const hoverTimeout = useRef(undefined) const _method = useMemo(() => { return isUrlMethodPair(href) ? href.method : (method.toLowerCase() as Method) }, [href, method]) const _as = useMemo(() => { if (typeof as !== 'string' || as.toLowerCase() !== 'a') { // Custom component or element return as } return _method !== 'get' ? 'button' : as.toLowerCase() }, [as, _method]) const mergeDataArray = useMemo( () => mergeDataIntoQueryString(_method, isUrlMethodPair(href) ? href.url : href, data, queryStringArrayFormat), [href, _method, data, queryStringArrayFormat], ) const url = useMemo(() => mergeDataArray[0], [mergeDataArray]) const _data = useMemo(() => mergeDataArray[1], [mergeDataArray]) const baseParams = useMemo( () => ({ data: _data, method: _method, preserveScroll, preserveState: preserveState ?? _method !== 'get', preserveUrl, replace, only, except, headers, async, }), [_data, _method, preserveScroll, preserveState, preserveUrl, replace, only, except, headers, async], ) const visitParams = useMemo( () => ({ ...baseParams, viewTransition, onCancelToken, onBefore, onStart(visit: PendingVisit) { setInFlightCount((count) => count + 1) onStart(visit) }, onProgress, onFinish(visit: ActiveVisit) { setInFlightCount((count) => count - 1) onFinish(visit) }, onCancel, onSuccess, onError, }), [ baseParams, viewTransition, onCancelToken, onBefore, onStart, onProgress, onFinish, onCancel, onSuccess, onError, ], ) const prefetchModes: LinkPrefetchOption[] = useMemo( () => { if (prefetch === true) { return ['hover'] } if (prefetch === false) { return [] } if (Array.isArray(prefetch)) { return prefetch } return [prefetch] }, Array.isArray(prefetch) ? prefetch : [prefetch], ) const cacheForValue = useMemo(() => { if (cacheFor !== 0) { // If they've provided a value, respect it return cacheFor } if (prefetchModes.length === 1 && prefetchModes[0] === 'click') { // If they've only provided a prefetch mode of 'click', // we should only prefetch for the next request but not keep it around return 0 } // Otherwise, default to 30 seconds return config.get('prefetch.cacheFor') }, [cacheFor, prefetchModes]) const doPrefetch = useMemo(() => { return () => { router.prefetch( url, { ...baseParams, onPrefetching, onPrefetched, }, { cacheFor: cacheForValue, cacheTags }, ) } }, [url, baseParams, onPrefetching, onPrefetched, cacheForValue, cacheTags]) useEffect(() => { return () => { clearTimeout(hoverTimeout.current) } }, []) useEffect(() => { if (prefetchModes.includes('mount')) { setTimeout(() => doPrefetch()) } }, prefetchModes) const regularEvents = { onClick: (event: React.MouseEvent) => { onClick(event) if (shouldIntercept(event)) { event.preventDefault() router.visit(url, visitParams) } }, } const prefetchHoverEvents = { onMouseEnter: () => { hoverTimeout.current = window.setTimeout(() => { doPrefetch() }, config.get('prefetch.hoverDelay')) }, onMouseLeave: () => { clearTimeout(hoverTimeout.current) }, onClick: regularEvents.onClick, } const prefetchClickEvents = { onMouseDown: (event: React.MouseEvent) => { if (shouldIntercept(event)) { event.preventDefault() doPrefetch() } }, onKeyDown: (event: React.KeyboardEvent) => { if (shouldNavigate(event)) { event.preventDefault() doPrefetch() } }, onMouseUp: (event: React.MouseEvent) => { if (shouldIntercept(event)) { event.preventDefault() router.visit(url, visitParams) } }, onKeyUp: (event: React.KeyboardEvent) => { if (shouldNavigate(event)) { event.preventDefault() router.visit(url, visitParams) } }, onClick: (event: React.MouseEvent) => { onClick(event) if (shouldIntercept(event)) { // Let the mouseup/keyup event handle the visit event.preventDefault() } }, } const elProps = useMemo(() => { if (_as === 'button') { return { type: 'button' } } if (_as === 'a' || typeof _as !== 'string') { return { href: url } } return {} }, [_as, url]) return createElement( _as, { ...props, ...elProps, ref, ...(() => { if (prefetchModes.includes('hover')) { return prefetchHoverEvents } if (prefetchModes.includes('click')) { return prefetchClickEvents } return regularEvents })(), 'data-loading': inFlightCount > 0 ? '' : undefined, }, children, ) }, ) Link.displayName = 'InertiaLink' export default Link ================================================ FILE: packages/react/src/PageContext.ts ================================================ import { Page } from '@inertiajs/core' import { createContext } from 'react' const pageContext = createContext(null) pageContext.displayName = 'InertiaPageContext' export default pageContext ================================================ FILE: packages/react/src/WhenVisible.ts ================================================ import { ReloadOptions, router } from '@inertiajs/core' import { createElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import usePage from './usePage' interface WhenVisibleSlotProps { fetching: boolean } interface WhenVisibleProps { children: ReactNode | ((props: WhenVisibleSlotProps) => ReactNode) fallback: ReactNode | (() => ReactNode) data?: string | string[] params?: ReloadOptions buffer?: number as?: string always?: boolean } const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: WhenVisibleProps) => { always = always ?? false as = as ?? 'div' fallback = fallback ?? null const pageProps = usePage().props const keys = useMemo(() => (data ? (Array.isArray(data) ? data : [data]) : []), [data]) const [loaded, setLoaded] = useState(() => keys.length > 0 && keys.every((key) => pageProps[key] !== undefined)) const [isFetching, setIsFetching] = useState(false) const fetching = useRef(false) const ref = useRef(null) const observer = useRef(null) const getReloadParamsRef = useRef<() => Partial>(() => ({})) useEffect(() => { if (keys.length > 0) { setLoaded(keys.every((key) => pageProps[key] !== undefined)) } }, [pageProps, keys]) const getReloadParams = useCallback<() => Partial>(() => { const reloadParams: Partial = { ...params } if (data) { reloadParams.only = (Array.isArray(data) ? data : [data]) as string[] } return reloadParams }, [params, data]) getReloadParamsRef.current = getReloadParams const registerObserver = () => { observer.current?.disconnect() observer.current = new IntersectionObserver( (entries) => { if (!entries[0].isIntersecting) { return } if (fetching.current) { return } if (!always && loaded) { return } fetching.current = true setIsFetching(true) const reloadParams = getReloadParamsRef.current() router.reload({ ...reloadParams, onStart: (e) => { fetching.current = true setIsFetching(true) reloadParams.onStart?.(e) }, onFinish: (e) => { setLoaded(true) fetching.current = false setIsFetching(false) reloadParams.onFinish?.(e) if (!always) { observer.current?.disconnect() } }, }) }, { rootMargin: `${buffer || 0}px`, }, ) observer.current.observe(ref.current!) } useEffect(() => { if (!ref.current) { return } if (loaded && !always) { return } registerObserver() return () => { observer.current?.disconnect() } }, [always, loaded, buffer]) const resolveChildren = () => (typeof children === 'function' ? children({ fetching: isFetching }) : children) const resolveFallback = () => (typeof fallback === 'function' ? fallback() : fallback) if (always || !loaded) { return createElement( as, { props: null, ref, }, loaded ? resolveChildren() : resolveFallback(), ) } return loaded ? resolveChildren() : null } WhenVisible.displayName = 'InertiaWhenVisible' export default WhenVisible ================================================ FILE: packages/react/src/createInertiaApp.ts ================================================ import { CreateInertiaAppOptionsForCSR, CreateInertiaAppOptionsForSSR, getInitialPageFromDOM, InertiaAppResponse, InertiaAppSSRResponse, Page, PageProps, router, setupProgress, SharedPageProps, } from '@inertiajs/core' import { createElement, Fragment, ReactElement } from 'react' import { renderToString } from 'react-dom/server' import App, { InertiaAppProps, type InertiaApp } from './App' import { config } from './index' import { ReactComponent, ReactInertiaAppConfig } from './types' export type SetupOptions = { el: ElementType App: InertiaApp props: InertiaAppProps } // The 'unknown' type is necessary for backwards compatibility... type ComponentResolver = ( name: string, ) => ReactComponent | Promise | { default: ReactComponent } | unknown type InertiaAppOptionsForCSR = CreateInertiaAppOptionsForCSR< SharedProps, ComponentResolver, SetupOptions, void, ReactInertiaAppConfig > type InertiaAppOptionsForSSR = CreateInertiaAppOptionsForSSR< SharedProps, ComponentResolver, SetupOptions, ReactElement, ReactInertiaAppConfig > & { render: typeof renderToString } export default async function createInertiaApp( options: InertiaAppOptionsForCSR, ): Promise export default async function createInertiaApp( options: InertiaAppOptionsForSSR, ): Promise export default async function createInertiaApp({ id = 'app', resolve, setup, title, progress = {}, page, render, defaults = {}, }: InertiaAppOptionsForCSR | InertiaAppOptionsForSSR): InertiaAppResponse { config.replace(defaults) const isServer = typeof window === 'undefined' const useScriptElementForInitialPage = config.get('future.useScriptElementForInitialPage') const initialPage = page || getInitialPageFromDOM>(id, useScriptElementForInitialPage)! // @ts-expect-error - This can be improved once we remove the 'unknown' type from the resolver... const resolveComponent = (name) => Promise.resolve(resolve(name)).then((module) => module.default || module) let head: string[] = [] const reactApp = await Promise.all([ resolveComponent(initialPage.component), router.decryptHistory().catch(() => {}), ]).then(([initialComponent]) => { const props = { initialPage, initialComponent, resolveComponent, titleCallback: title, } if (isServer) { const ssrSetup = setup as (options: SetupOptions) => ReactElement return ssrSetup({ el: null, App, props: { ...props, onHeadUpdate: (elements: string[]) => (head = elements) }, }) } const csrSetup = setup as (options: SetupOptions) => void return csrSetup({ el: document.getElementById(id)!, App, props, }) }) if (!isServer && progress) { setupProgress(progress) } if (isServer && render) { const element = () => { if (!useScriptElementForInitialPage) { return createElement( 'div', { id, 'data-page': JSON.stringify(initialPage), }, reactApp as ReactElement, ) } return createElement( Fragment, null, createElement('script', { 'data-page': id, type: 'application/json', dangerouslySetInnerHTML: { __html: JSON.stringify(initialPage).replace(/\//g, '\\/') }, }), createElement('div', { id }, reactApp as ReactElement), ) } const body = await render(element()) return { head, body } } } ================================================ FILE: packages/react/src/index.ts ================================================ import { config as coreConfig, progress as Progress, router as Router } from '@inertiajs/core' import { ReactInertiaAppConfig } from './types' export const progress = Progress export const router = Router export { default as App } from './App' export { default as createInertiaApp } from './createInertiaApp' export { default as Deferred } from './Deferred' export { default as Form, useFormContext } from './Form' export { default as Head } from './Head' export { default as InfiniteScroll } from './InfiniteScroll' export { InertiaLinkProps, default as Link } from './Link' export { ReactComponent as ResolvedComponent } from './types' export { InertiaFormProps, InertiaPrecognitiveFormProps, SetDataAction, SetDataByKeyValuePair, SetDataByMethod, SetDataByObject, default as useForm, } from './useForm' export { default as usePage } from './usePage' export { default as usePoll } from './usePoll' export { default as usePrefetch } from './usePrefetch' export { default as useRemember } from './useRemember' export { default as WhenVisible } from './WhenVisible' export const config = coreConfig.extend() ================================================ FILE: packages/react/src/react.ts ================================================ import React, { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react' // Inspired by react-redux, this hook uses useLayoutEffect in the browser, and useEffect // when using SSR. Currently, useLayoutEffect doesn't work when rendered on the server. export function useIsomorphicLayoutEffect(effect: EffectCallback, deps?: DependencyList): void { typeof window === 'undefined' ? useEffect(effect, deps) : useLayoutEffect(effect, deps) } // React.use() was introduced in React 19 export const isReact19 = typeof React.use === 'function' ================================================ FILE: packages/react/src/server.ts ================================================ export { default as default } from '@inertiajs/core/server' ================================================ FILE: packages/react/src/types.ts ================================================ import { PageHandler } from '@inertiajs/core' import { ComponentType, ReactNode } from 'react' export type LayoutFunction = (page: ReactNode) => ReactNode export type LayoutComponent = ComponentType<{ children: ReactNode }> export type ReactComponent = ComponentType & { layout?: LayoutComponent | LayoutComponent[] | LayoutFunction } export type ReactPageHandlerArgs = Parameters>[0] export type ReactInertiaAppConfig = {} ================================================ FILE: packages/react/src/useForm.ts ================================================ import { CancelToken, Errors, ErrorValue, FormDataErrors, FormDataKeys, FormDataType, FormDataValues, Method, Progress, RequestPayload, router, UrlMethodPair, UseFormArguments, UseFormSubmitArguments, UseFormSubmitOptions, UseFormTransformCallback, UseFormUtils, UseFormWithPrecognitionArguments, VisitOptions, } from '@inertiajs/core' import { createValidator, NamedInputEvent, PrecognitionPath, resolveName, toSimpleValidationErrors, ValidationConfig, Validator, } from 'laravel-precognition' import { cloneDeep, get, has, isEqual, set } from 'lodash-es' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { config } from '.' import { useIsomorphicLayoutEffect } from './react' import useRemember from './useRemember' export type SetDataByObject = (data: Partial) => void export type SetDataByMethod = (data: (previousData: TForm) => TForm) => void export type SetDataByKeyValuePair = >( key: K, value: FormDataValues, ) => void export type SetDataAction> = SetDataByObject & SetDataByMethod & SetDataByKeyValuePair type PrecognitionValidationConfig = ValidationConfig & { only?: TKeys[] | Iterable | ArrayLike } export interface InertiaFormProps { data: TForm isDirty: boolean errors: FormDataErrors hasErrors: boolean processing: boolean progress: Progress | null wasSuccessful: boolean recentlySuccessful: boolean setData: SetDataAction transform: (callback: UseFormTransformCallback) => void setDefaults: { (): void >(field: T, value: FormDataValues): void (fields: Partial): void } reset: >(...fields: K[]) => void clearErrors: >(...fields: K[]) => void resetAndClearErrors: >(...fields: K[]) => void setError: { >(field: K, value: ErrorValue): void (errors: FormDataErrors): void } submit: (...args: UseFormSubmitArguments) => void get: (url: string, options?: UseFormSubmitOptions) => void patch: (url: string, options?: UseFormSubmitOptions) => void post: (url: string, options?: UseFormSubmitOptions) => void put: (url: string, options?: UseFormSubmitOptions) => void delete: (url: string, options?: UseFormSubmitOptions) => void cancel: () => void dontRemember: >(...fields: K[]) => InertiaFormProps withPrecognition: (...args: UseFormWithPrecognitionArguments) => InertiaPrecognitiveFormProps } export interface InertiaFormValidationProps { invalid: >(field: K) => boolean setValidationTimeout: (duration: number) => InertiaPrecognitiveFormProps touch: >( field: K | NamedInputEvent | Array, ...fields: K[] ) => InertiaPrecognitiveFormProps touched: >(field?: K) => boolean valid: >(field: K) => boolean validate: | PrecognitionPath>( field?: K | NamedInputEvent | PrecognitionValidationConfig, config?: PrecognitionValidationConfig, ) => InertiaPrecognitiveFormProps validateFiles: () => InertiaPrecognitiveFormProps validating: boolean validator: () => Validator withAllErrors: () => InertiaPrecognitiveFormProps withoutFileValidation: () => InertiaPrecognitiveFormProps // Backward compatibility for easy migration from the original Precognition libraries setErrors: (errors: FormDataErrors) => InertiaPrecognitiveFormProps forgetError: | NamedInputEvent>(field: K) => InertiaPrecognitiveFormProps } export type InertiaForm = InertiaFormProps export type InertiaPrecognitiveFormProps = InertiaFormProps & InertiaFormValidationProps export default function useForm>( method: Method | (() => Method), url: string | (() => string), data: TForm | (() => TForm), ): InertiaPrecognitiveFormProps export default function useForm>( urlMethodPair: UrlMethodPair | (() => UrlMethodPair), data: TForm | (() => TForm), ): InertiaPrecognitiveFormProps export default function useForm>( rememberKey: string, data: TForm | (() => TForm), ): InertiaFormProps export default function useForm>(data: TForm | (() => TForm)): InertiaFormProps export default function useForm>(): InertiaFormProps export default function useForm>( ...args: UseFormArguments ): InertiaFormProps | InertiaPrecognitiveFormProps { const isMounted = useRef(false) const parsedArgs = UseFormUtils.parseUseFormArguments(...args) const { rememberKey, data: initialData } = parsedArgs const precognitionEndpoint = useRef(parsedArgs.precognitionEndpoint) const [defaults, setDefaults] = useState( typeof initialData === 'function' ? cloneDeep(initialData()) : cloneDeep(initialData), ) const cancelToken = useRef(null) const recentlySuccessfulTimeoutId = useRef(undefined) const excludeKeysRef = useRef[]>([]) const [data, setData] = rememberKey ? useRemember(defaults, `${rememberKey}:data`, excludeKeysRef) : useState(defaults) const [errors, setErrors] = rememberKey ? useRemember({} as FormDataErrors, `${rememberKey}:errors`) : useState({} as FormDataErrors) const [hasErrors, setHasErrors] = useState(false) const [processing, setProcessing] = useState(false) const [progress, setProgress] = useState(null) const [wasSuccessful, setWasSuccessful] = useState(false) const [recentlySuccessful, setRecentlySuccessful] = useState(false) const transform = useRef>((data) => data) const isDirty = useMemo(() => !isEqual(data, defaults), [data, defaults]) // Precognition state const validatorRef = useRef(null) const [validating, setValidating] = useState(false) const [touchedFields, setTouchedFields] = useState([]) const [validFields, setValidFields] = useState([]) const withAllErrors = useRef(null) useEffect(() => { isMounted.current = true return () => { isMounted.current = false } }, []) // Track if setDefaults was called manually during onSuccess to avoid // overriding user's custom defaults with automatic behavior. const setDefaultsCalledInOnSuccess = useRef(false) const submit = useCallback( (...args: UseFormSubmitArguments) => { const { method, url, options } = UseFormUtils.parseSubmitArguments(args, precognitionEndpoint.current) setDefaultsCalledInOnSuccess.current = false const _options: VisitOptions = { ...options, onCancelToken: (token) => { cancelToken.current = token if (options.onCancelToken) { return options.onCancelToken(token) } }, onBefore: (visit) => { setWasSuccessful(false) setRecentlySuccessful(false) clearTimeout(recentlySuccessfulTimeoutId.current) if (options.onBefore) { return options.onBefore(visit) } }, onStart: (visit) => { setProcessing(true) if (options.onStart) { return options.onStart(visit) } }, onProgress: (event) => { setProgress(event || null) if (options.onProgress) { return options.onProgress(event) } }, onSuccess: async (page) => { if (isMounted.current) { setProcessing(false) setProgress(null) setErrors({} as FormDataErrors) setHasErrors(false) setWasSuccessful(true) setRecentlySuccessful(true) recentlySuccessfulTimeoutId.current = setTimeout(() => { if (isMounted.current) { setRecentlySuccessful(false) } }, config.get('form.recentlySuccessfulDuration')) } const onSuccess = options.onSuccess ? await options.onSuccess(page) : null if (isMounted.current && !setDefaultsCalledInOnSuccess.current) { setData((data) => { setDefaults(cloneDeep(data)) return data }) } return onSuccess }, onError: (errors) => { if (isMounted.current) { setProcessing(false) setProgress(null) setErrors(errors as FormDataErrors) setHasErrors(Object.keys(errors).length > 0) validatorRef.current?.setErrors(errors as Errors) } if (options.onError) { return options.onError(errors) } }, onCancel: () => { if (isMounted.current) { setProcessing(false) setProgress(null) } if (options.onCancel) { return options.onCancel() } }, onFinish: (visit) => { if (isMounted.current) { setProcessing(false) setProgress(null) } cancelToken.current = null if (options.onFinish) { return options.onFinish(visit) } }, } const transformedData = transform.current(data) as RequestPayload if (method === 'delete') { router.delete(url, { ..._options, data: transformedData }) } else { router[method](url, transformedData, _options) } }, [data, setErrors, transform], ) const setDataFunction = useCallback( (keyOrData: FormDataKeys | Function | Partial, maybeValue?: any) => { if (typeof keyOrData === 'string') { setData((data) => set(cloneDeep(data), keyOrData, maybeValue)) } else if (typeof keyOrData === 'function') { setData((data) => keyOrData(data)) } else { setData(keyOrData as TForm) } }, [setData], ) const [dataAsDefaults, setDataAsDefaults] = useState(false) const dataRef = useRef(data) useEffect(() => { dataRef.current = data }) const setDefaultsFunction = useCallback( (fieldOrFields?: FormDataKeys | Partial, maybeValue?: unknown) => { setDefaultsCalledInOnSuccess.current = true let newDefaults = {} as TForm if (typeof fieldOrFields === 'undefined') { newDefaults = { ...dataRef.current } setDefaults(dataRef.current) // If setData was called right before setDefaults, data was not // updated in that render yet, so we set a flag to update // defaults right after the next render. setDataAsDefaults(true) } else { setDefaults((defaults) => { newDefaults = typeof fieldOrFields === 'string' ? set(cloneDeep(defaults), fieldOrFields, maybeValue) : Object.assign(cloneDeep(defaults), fieldOrFields) return newDefaults as TForm }) } validatorRef.current?.defaults(newDefaults) }, [setDefaults], ) useIsomorphicLayoutEffect(() => { if (!dataAsDefaults) { return } if (isDirty) { // Data has been updated in this next render and is different from // the defaults, so now we can set defaults to the current data. setDefaults(data) } setDataAsDefaults(false) }, [dataAsDefaults]) const reset = useCallback( (...fields: string[]) => { if (fields.length === 0) { setData(defaults) } else { setData((data) => (fields as Array>) .filter((key) => has(defaults, key)) .reduce( (carry, key) => { return set(carry, key, get(defaults, key)) }, { ...data } as TForm, ), ) } validatorRef.current?.reset(...fields) }, [setData, defaults], ) const setError = useCallback( (fieldOrFields: FormDataKeys | FormDataErrors, maybeValue?: ErrorValue) => { setErrors((errors) => { const newErrors = { ...errors, ...(typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields), } setHasErrors(Object.keys(newErrors).length > 0) validatorRef.current?.setErrors(newErrors) return newErrors }) }, [setErrors, setHasErrors], ) const clearErrors = useCallback( (...fields: string[]) => { setErrors((errors) => { const newErrors = Object.keys(errors).reduce( (carry, field) => ({ ...carry, ...(fields.length > 0 && !fields.includes(field) ? { [field]: (errors as Errors)[field] } : {}), }), {}, ) setHasErrors(Object.keys(newErrors).length > 0) if (validatorRef.current) { if (fields.length === 0) { validatorRef.current.setErrors({}) } else { fields.forEach(validatorRef.current.forgetError) } } return newErrors as FormDataErrors }) }, [setErrors, setHasErrors], ) const resetAndClearErrors = useCallback( (...fields: string[]) => { reset(...fields) clearErrors(...fields) }, [reset, clearErrors], ) const createSubmitMethod = (method: Method) => (url: string, options: VisitOptions = {}) => { submit(method, url, options) } const getMethod = useCallback(createSubmitMethod('get'), [submit]) const post = useCallback(createSubmitMethod('post'), [submit]) const put = useCallback(createSubmitMethod('put'), [submit]) const patch = useCallback(createSubmitMethod('patch'), [submit]) const deleteMethod = useCallback(createSubmitMethod('delete'), [submit]) const cancel = useCallback(() => { if (cancelToken.current) { cancelToken.current.cancel() } }, []) const transformFunction = useCallback((callback: UseFormTransformCallback) => { transform.current = callback }, []) // Build base form properties const form = { data, setData: setDataFunction, isDirty, errors, hasErrors, processing, progress, wasSuccessful, recentlySuccessful, transform: transformFunction, setDefaults: setDefaultsFunction, reset, setError, clearErrors, resetAndClearErrors, submit, get: getMethod, post, put, patch, delete: deleteMethod, cancel, dontRemember: >(...keys: K[]) => { excludeKeysRef.current = keys return form }, } as InertiaFormProps const tap = (value: T, callback: (value: T) => unknown): T => { callback(value) return value } const valid = useCallback( >(field: K) => validFields.includes(field as string), [validFields], ) const invalid = useCallback(>(field: K) => field in errors, [errors]) const touched = useCallback( >(field?: K) => typeof field === 'string' ? touchedFields.includes(field as string) : touchedFields.length > 0, [touchedFields], ) const validate = (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) => { // Handle config object passed as first argument if (typeof field === 'object' && !('target' in field)) { config = field field = undefined } if (field === undefined) { validatorRef.current!.validate(config) } else { const fieldName = resolveName(field) const currentData = dataRef.current const transformedData = transform.current(currentData) as Record validatorRef.current!.validate(fieldName, get(transformedData, fieldName), config) } return form } // Create withPrecognition method that returns a precognitive form const withPrecognition = (...args: UseFormWithPrecognitionArguments): InertiaPrecognitiveFormProps => { precognitionEndpoint.current = UseFormUtils.createWayfinderCallback(...args) if (!validatorRef.current) { const validator = createValidator((client) => { const { method, url } = precognitionEndpoint.current!() // Get the current data from the ref, not the closure const currentData = dataRef.current const transformedData = transform.current(currentData) as Record return client[method](url, transformedData) }, cloneDeep(defaults)) validatorRef.current = validator validator .on('validatingChanged', () => { setValidating(validator.validating()) }) .on('validatedChanged', () => { setValidFields(validator.valid()) }) .on('touchedChanged', () => { setTouchedFields(validator.touched()) }) .on('errorsChanged', () => { const validationErrors = (withAllErrors.current ?? config.get('form.withAllErrors')) ? validator.errors() : toSimpleValidationErrors(validator.errors()) setErrors(validationErrors as FormDataErrors) setHasErrors(Object.keys(validationErrors).length > 0) setValidFields(validator.valid()) }) } // Create precognitive form with all validation methods const precognitiveForm = Object.assign(form, { validating, validator: () => validatorRef.current!, valid, invalid, touched, withoutFileValidation: () => tap(precognitiveForm, () => validatorRef.current?.withoutFileValidation()), touch: ( field: FormDataKeys | NamedInputEvent | Array>, ...fields: FormDataKeys[] ) => { if (Array.isArray(field)) { validatorRef.current?.touch(field) } else if (typeof field === 'string') { validatorRef.current?.touch([field, ...fields]) } else { validatorRef.current?.touch(field) } return precognitiveForm }, withAllErrors: () => tap(precognitiveForm, () => (withAllErrors.current = true)), setValidationTimeout: (duration: number) => tap(precognitiveForm, () => validatorRef.current?.setTimeout(duration)), validateFiles: () => tap(precognitiveForm, () => validatorRef.current?.validateFiles()), validate, setErrors: (errors: FormDataErrors) => tap(precognitiveForm, () => form.setError(errors)), forgetError: (field: FormDataKeys | NamedInputEvent) => tap(precognitiveForm, () => form.clearErrors(resolveName(field as string | NamedInputEvent) as FormDataKeys), ), }) as InertiaPrecognitiveFormProps return precognitiveForm } form.withPrecognition = withPrecognition return precognitionEndpoint.current ? form.withPrecognition(precognitionEndpoint.current) : form } ================================================ FILE: packages/react/src/usePage.ts ================================================ import { Page, PageProps, SharedPageProps } from '@inertiajs/core' import React from 'react' import PageContext from './PageContext' import { isReact19 } from './react' export default function usePage(): Page { // React.use() was introduced in React 19, fallback to React.useContext() for earlier versions const page = isReact19 ? React.use(PageContext) : React.useContext(PageContext) if (!page) { throw new Error('usePage must be used within the Inertia component') } return page as Page } ================================================ FILE: packages/react/src/usePoll.ts ================================================ import { PollOptions, ReloadOptions, router } from '@inertiajs/core' import { useEffect, useRef } from 'react' export default function usePoll( interval: number, requestOptions: ReloadOptions = {}, options: PollOptions = { keepAlive: false, autoStart: true, }, ) { const pollRef = useRef( router.poll(interval, requestOptions, { ...options, autoStart: false, }), ) useEffect(() => { if (options.autoStart ?? true) { pollRef.current.start() } return () => pollRef.current.stop() }, []) return { stop: pollRef.current.stop, start: pollRef.current.start, } } ================================================ FILE: packages/react/src/usePrefetch.ts ================================================ import { router, VisitOptions } from '@inertiajs/core' import { useEffect, useState } from 'react' export default function usePrefetch(options: VisitOptions = {}): { lastUpdatedAt: number | null isPrefetching: boolean isPrefetched: boolean flush: () => void } { const cached = typeof window === 'undefined' ? null : router.getCached(window.location.pathname, options) const inFlight = typeof window === 'undefined' ? null : router.getPrefetching(window.location.pathname, options) const [lastUpdatedAt, setLastUpdatedAt] = useState(cached?.staleTimestamp || null) const [isPrefetching, setIsPrefetching] = useState(inFlight !== null) const [isPrefetched, setIsPrefetched] = useState(cached !== null) useEffect(() => { const onPrefetchingListener = router.on('prefetching', (e) => { if (e.detail.visit.url.pathname === window.location.pathname) { setIsPrefetching(true) } }) const onPrefetchedListener = router.on('prefetched', (e) => { if (e.detail.visit.url.pathname === window.location.pathname) { setIsPrefetching(false) setIsPrefetched(true) setLastUpdatedAt(e.detail.fetchedAt) } }) return () => { onPrefetchedListener() onPrefetchingListener() } }, []) return { lastUpdatedAt, isPrefetching, isPrefetched, flush: () => router.flush(window.location.pathname, options), } } ================================================ FILE: packages/react/src/useRemember.ts ================================================ import { router } from '@inertiajs/core' import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react' export default function useRemember( initialState: State, key?: string, excludeKeysRef?: MutableRefObject, ): [State, Dispatch>] { const [state, setState] = useState(() => { const restored = router.restore(key) as State return restored !== undefined ? restored : initialState }) useEffect(() => { const keys = excludeKeysRef?.current if (keys && keys.length > 0 && typeof state === 'object' && state !== null) { const filtered = { ...state } as Record keys.forEach((k) => delete filtered[k]) router.remember(filtered, key) } else { router.remember(state, key) } }, [state, key]) return [state, setState] } ================================================ FILE: packages/react/test-app/Layouts/NestedLayout.tsx ================================================ import { usePage } from '@inertiajs/react' import { useId, useState } from 'react' export default ({ children }: { children: React.ReactNode }) => { const [createdAt] = useState(Date.now()) window._inertia_nested_layout_id = useId() window._inertia_nested_layout_props = usePage().props return (
Nested Layout {createdAt}
{children}
) } ================================================ FILE: packages/react/test-app/Layouts/Prefetch.tsx ================================================ import { Link } from '@inertiajs/react' export default ({ children }: { children: React.ReactNode }) => { return (
On Hover (Default) On Mount On Click On Hover + Mount On Mount (Once) On Enter On Spacebar
{children}
) } ================================================ FILE: packages/react/test-app/Layouts/SWR.tsx ================================================ import { Link } from '@inertiajs/react' export default ({ children }: { children: React.ReactNode }) => { return (
1s Expired 1s Expired (Number) 1s Stale, 2s Expired 1s Stale, 2s Expired (Number)
{children}
) } ================================================ FILE: packages/react/test-app/Layouts/SiteLayout.tsx ================================================ import { usePage } from '@inertiajs/react' import { useId, useState } from 'react' export default ({ children }: { children: React.ReactNode }) => { const [createdAt] = useState(Date.now()) window._inertia_layout_id = useId() window._inertia_site_layout_props = usePage().props return (
Site Layout {createdAt}
{children}
) } ================================================ FILE: packages/react/test-app/Layouts/WithScrollRegion.tsx ================================================ import { useEffect, useState } from 'react' export default ({ children }: { children: React.ReactNode }) => { const [documentScrollTop, setDocumentScrollTop] = useState(0) const [documentScrollLeft, setDocumentScrollLeft] = useState(0) const [slotScrollTop, setSlotScrollTop] = useState(0) const [slotScrollLeft, setSlotScrollLeft] = useState(0) const handleScrollEvent = () => { setDocumentScrollLeft(document.documentElement.scrollLeft) setDocumentScrollTop(document.documentElement.scrollTop) const slot = document.getElementById('slot') if (slot) { setSlotScrollTop(slot.scrollTop) setSlotScrollLeft(slot.scrollLeft) } } useEffect(() => { document.addEventListener('scroll', handleScrollEvent) return () => { document.removeEventListener('scroll', handleScrollEvent) } }) return (
With scroll regions
Document scroll position is {documentScrollLeft} & {documentScrollTop}
Slot scroll position is {slotScrollLeft} & {slotScrollTop}
{children}
) } ================================================ FILE: packages/react/test-app/Layouts/WithoutScrollRegion.tsx ================================================ import { useEffect, useState } from 'react' export default ({ children }: { children: React.ReactNode }) => { const [documentScrollTop, setDocumentScrollTop] = useState(0) const [documentScrollLeft, setDocumentScrollLeft] = useState(0) const [slotScrollTop, setSlotScrollTop] = useState(0) const [slotScrollLeft, setSlotScrollLeft] = useState(0) const handleScrollEvent = () => { setDocumentScrollLeft(document.documentElement.scrollLeft) setDocumentScrollTop(document.documentElement.scrollTop) const slot = document.getElementById('slot') if (slot) { setSlotScrollTop(slot.scrollTop) setSlotScrollLeft(slot.scrollLeft) } } useEffect(() => { document.addEventListener('scroll', handleScrollEvent) return () => { document.removeEventListener('scroll', handleScrollEvent) } }) return (
Without scroll regions
Document scroll position is {documentScrollLeft} & {documentScrollTop}
Slot scroll position is {slotScrollLeft} & {slotScrollTop}
{children}
) } ================================================ FILE: packages/react/test-app/Pages/Article.tsx ================================================ import { Link } from '@inertiajs/react' import { useEffect, useState } from 'react' export default () => { const enableSmoothScroll = () => { document.documentElement.style.scrollBehavior = 'smooth' } const [scrollLog, setScrollLog] = useState([]) const handleScrollEvent = () => { setScrollLog((prev) => [...prev, document.documentElement.scrollTop]) } useEffect(() => { document.addEventListener('scroll', handleScrollEvent) return () => document.removeEventListener('scroll', handleScrollEvent) }) return ( <>

Article Header

Sunt culpa sit sunt enim aliquip. Esse ea ea quis voluptate. Enim consectetur aliqua ex ex magna cupidatat id minim sit elit. Amet pariatur occaecat pariatur duis eiusmod dolore magna. Et commodo cupidatat in commodo elit cupidatat minim qui id non enim ad. Culpa aliquip ad Lorem sit consectetur ullamco culpa duis nisi et fugiat mollit eiusmod. Laboris voluptate veniam consequat proident in nulla irure velit.

Sit sint laboris sunt eiusmod ipsum laborum eiusmod amet commodo exercitation in duis magna. Proident sunt minim in elit qui. Id pariatur commodo fugiat excepteur in deserunt Lorem ipsum occaecat est. Excepteur sit tempor ipsum ex officia veniam enim amet velit fugiat mollit cillum. Incididunt aliqua nulla id occaecat nulla. Non ea ad est occaecat deserunt officia qui commodo exercitation.

Voluptate laborum quis aliqua ullamco magna amet ullamco laborum qui cillum eu. Dolore dolore aliqua proident proident sunt ipsum in. Enim velit dolore labore dolor quis incididunt duis culpa Lorem. Eu adipisicing non elit fugiat voluptate labore ipsum dolore consectetur commodo. Et in et cillum duis consequat quis ex eu commodo. Eiusmod aliqua excepteur consectetur eiusmod aute et consectetur sit pariatur dolore qui officia pariatur.

Non sunt eu mollit qui reprehenderit. Aute culpa anim voluptate do in esse duis laborum ad dolore. Ullamco nisi in nostrud officia do. Duis pariatur officia id duis. Deserunt ad incididunt est sint consectetur reprehenderit mollit est Lorem ea pariatur anim dolor adipisicing. Nostrud irure magna nostrud laboris aute sunt veniam laboris veniam incididunt sit. Nulla proident ad aliqua fugiat culpa sunt est in dolor velit ad irure nulla.

Do aute laborum deserunt non laborum voluptate voluptate. Anim ut laborum magna sunt cupidatat irure. Cupidatat fugiat minim sint cillum laborum excepteur irure id est irure ad occaecat adipisicing enim. Deserunt nulla anim proident velit irure nostrud est est reprehenderit consequat pariatur qui. Fugiat Lorem sint eu laborum minim pariatur cillum mollit nulla consequat ullamco ex. Ex consectetur ad ut irure fugiat occaecat aliqua exercitation cillum ipsum anim dolore tempor.

Adipisicing consequat irure fugiat Lorem deserunt aliquip do cupidatat. Lorem labore elit ex qui nostrud qui cillum sunt adipisicing occaecat. Sunt nostrud amet amet cupidatat fugiat Lorem quis nulla id cillum esse eu. Ullamco aliqua dolore irure amet mollit anim velit dolore.

Veniam cupidatat ipsum ea officia ipsum nisi laborum culpa qui dolore. Aliqua Lorem nisi labore ea velit aliquip irure excepteur eu. Laboris proident duis non labore sunt quis aute tempor laboris enim anim eiusmod.

Minim proident ut aliqua ea ut culpa fugiat ullamco nisi esse nostrud reprehenderit id. Id id ullamco velit anim nisi magna Lorem tempor. Et veniam occaecat ut labore consequat fugiat duis.

Adipisicing ea consectetur adipisicing aute eu pariatur enim labore consequat occaecat consectetur minim nisi. Cillum commodo sunt labore reprehenderit. Duis esse excepteur magna tempor eiusmod exercitation Lorem reprehenderit excepteur pariatur. Esse cupidatat occaecat magna do aliquip Lorem. Consectetur adipisicing consequat dolore nostrud esse eu cillum id commodo duis. Aliquip dolor cillum cupidatat fugiat.

Ex eiusmod id est laborum sunt ex ea aute adipisicing ad magna deserunt duis. Nostrud velit dolore id commodo quis enim fugiat. Sint non quis consectetur voluptate aliqua dolore ad voluptate nulla. Irure sit reprehenderit sint laboris non elit. Duis minim nisi esse dolor. Sit ex in consequat non occaecat commodo irure et. Commodo qui ipsum Lorem magna consequat consequat et minim eiusmod Lorem eiusmod cupidatat voluptate.

Far down

Ex eiusmod id est laborum sunt ex ea aute adipisicing ad magna deserunt duis. Nostrud velit dolore id commodo quis enim fugiat. Sint non quis consectetur voluptate aliqua dolore ad voluptate nulla. Irure sit reprehenderit sint laboris non elit. Duis minim nisi esse dolor. Sit ex in consequat non occaecat commodo irure et. Commodo qui ipsum Lorem magna consequat consequat et minim eiusmod Lorem eiusmod cupidatat voluptate.

Scroll log: {JSON.stringify(scrollLog)}
{' '} Home{' '} {' '} Article Far Down{' '} ) } ================================================ FILE: packages/react/test-app/Pages/ClientSideVisit/Page1.tsx ================================================ import { Page } from '@inertiajs/core' import { router } from '@inertiajs/react' import { useState } from 'react' interface PageProps { foo: string bar: string } export default ({ foo, bar }: PageProps) => { const [errors, setErrors] = useState(0) const [finished, setFinished] = useState(0) const [success, setSuccess] = useState(0) const [random] = useState(Math.random()) const bagErrors = () => { router.replace({ preserveState: true, props: (props: Page['props']) => ({ ...props, errors: { bag: { foo: 'bar' } } }), errorBag: 'bag', onError: (err) => { setErrors(Object.keys(err).length) }, onFinish: () => setFinished(finished + 1), onSuccess: () => setSuccess(success + 1), }) } const defaultErrors = () => { router.replace({ preserveState: true, props: (props: PageProps) => ({ ...props, errors: { foo: 'bar', baz: 'qux' } }), onError: (err) => { setErrors(Object.keys(err).length) }, onFinish: () => setFinished(finished + 1), onSuccess: () => setSuccess(success + 1), }) } const replace = () => { router.replace({ preserveState: true, props: (props) => ({ ...props, foo: 'foo from client' }), onFinish: () => setFinished(finished + 1), onSuccess: () => setSuccess(success + 1), }) } const replaceAndPreserveStateWithErrors = (errors = {}) => { router.replace({ preserveState: 'errors', props: (props: PageProps) => ({ ...props, errors }), }) } const push = () => { router.push({ url: '/client-side-visit-2', component: 'ClientSideVisit/Page2', props: { baz: 'baz from client' }, }) } return (
{foo}
{bar}
Errors: {errors}
Finished: {finished}
Success: {success}
Random: {random}
) } ================================================ FILE: packages/react/test-app/Pages/ClientSideVisit/Page2.tsx ================================================ export default ({ baz }: { baz: string }) => { return
{baz}
} ================================================ FILE: packages/react/test-app/Pages/ClientSideVisit/Props.tsx ================================================ import { router } from '@inertiajs/react' interface Tag { id: number name: string } interface User { name: string age: number } export default ({ items = [], tags = [], user, count = 0, singleValue, undefinedValue, }: { items?: string[] tags?: Tag[] user?: User count?: number singleValue?: string | string[] undefinedValue?: string | string[] }) => { const replacePropString = () => { router.replaceProp('user.name', 'Jane Smith') } const replacePropNumber = () => { router.replaceProp('count', 10) } const replacePropFunction = () => { router.replaceProp('count', (oldValue: number) => oldValue * 2) } const appendToPropArray = () => { router.appendToProp('items', 'item3') } const appendToPropMultiple = () => { router.appendToProp('items', ['item4', 'item5']) } const appendToPropFunction = () => { router.appendToProp('tags', () => ({ id: 3, name: 'tag3' })) } const appendArrayToArray = () => { router.appendToProp('tags', [ { id: 3, name: 'tag3' }, { id: 4, name: 'tag4' }, ]) } const prependToPropArray = () => { router.prependToProp('items', 'item0') } const prependToPropMultiple = () => { router.prependToProp('items', ['itemA', 'itemB']) } const prependToPropFunction = () => { router.prependToProp('tags', () => ({ id: 0, name: 'tag0' })) } // Edge case tests for mergeArrays behavior const appendToNonArray = () => { router.appendToProp('singleValue', 'world') } const prependToNonArray = () => { router.prependToProp('singleValue', 'hey') } const appendArrayToNonArray = () => { router.appendToProp('singleValue', ['there', 'world']) } const prependArrayToNonArray = () => { router.prependToProp('singleValue', ['hey', 'hi']) } const appendToUndefined = () => { router.appendToProp('undefinedValue', 'new value') } const prependToUndefined = () => { router.prependToProp('undefinedValue', 'start value') } return (

Client Side Visit Props Testing

User: {user?.name || 'Unknown'} (Age: {user?.age || 'Unknown'})
Count: {count}
Items: {JSON.stringify(items)}
Tags: {JSON.stringify(tags)}
Single Value: {JSON.stringify(singleValue)}
Undefined Value: {JSON.stringify(undefinedValue)}

Replace Prop Tests

Append To Prop Tests

Prepend To Prop Tests

Edge Case Tests (mergeArrays behavior)

) } ================================================ FILE: packages/react/test-app/Pages/ClientSideVisit/Sequential.tsx ================================================ import { router } from '@inertiajs/react' export default ({ foo = '', bar = '' }: { foo?: string; bar?: string }) => { const replaceSequentially = () => { router.replaceProp('foo', 'baz') router.replaceProp('bar', 'qux') } return (

Foo: {foo}

Bar: {bar}

) } ================================================ FILE: packages/react/test-app/Pages/ComplexMergeSelective.tsx ================================================ import { router } from '@inertiajs/react' export default ({ mixed, }: { mixed: { name: string users: string[] chat: { data: number[] } post: { id: number; comments: { allowed: boolean; data: string[] } } } }) => { const reload = () => { router.reload({ only: ['mixed'], }) } return (
name is {mixed.name}
users: {mixed.users.join(', ')}
chat.data: {mixed.chat.data.join(', ')}
post.id: {mixed.post.id}
post.comments.allowed: {mixed.post.comments.allowed ? 'true' : 'false'}
post.comments.data: {mixed.post.comments.data.join(', ')}
) } ================================================ FILE: packages/react/test-app/Pages/CustomConfig.tsx ================================================ import type { VisitOptions } from '@inertiajs/core' import { config, Link, useForm, usePage } from '@inertiajs/react' export default () => { const page = usePage() const form = useForm({}) const submit = () => { form.post(page.url) } config.set({ 'form.recentlySuccessfulDuration': 1000, 'prefetch.cacheFor': '2s', }) config.set('visitOptions', (href: string, options: VisitOptions) => { if (href !== '/dump/post') { return {} } return { headers: { ...options.headers, 'X-From-Callback': 'bar' } } }) return (
Prefetch Link Post Dump {form.recentlySuccessful &&

Form was recently successful!

}
) } ================================================ FILE: packages/react/test-app/Pages/DeepMergeProps.tsx ================================================ import { router } from '@inertiajs/react' import { useState } from 'react' type PageProps = { bar: number[] foo: { page: number; data: number[]; per_page: number; meta: { label: string } } baz: number[] } export default ({ bar, foo, baz }: PageProps) => { const [page, setPage] = useState(foo.page) const reloadIt = () => { router.reload({ data: { page, }, only: ['foo', 'baz'], onSuccess(visit) { setPage((visit.props as unknown as PageProps).foo.page) }, }) } const getFresh = () => { setPage(0) router.visit('/deep-merge-props', { reset: ['foo', 'baz'], }) } return ( <>
bar count is {bar.length}
baz count is {baz.length}
foo.data count is {foo.data.length}
foo.page is {foo.page}
foo.per_page is {foo.per_page}
foo.meta.label is {foo.meta.label}
) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/BackButton/PageA.tsx ================================================ import { Deferred, Link, usePage } from '@inertiajs/react' const FastProp = () => { const { fastProp } = usePage<{ fastProp?: string }>().props return fastProp } const SlowProp = () => { const { slowProp } = usePage<{ slowProp?: string }>().props return slowProp } export default () => { return ( <> Loading fast prop...}> Loading slow prop...}> Go to Page B ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/BackButton/PageB.tsx ================================================ import { Deferred, Link, usePage } from '@inertiajs/react' const Data = () => { const { data } = usePage<{ data?: string }>().props return data } export default () => { return ( <> Loading data...}> Go to Page A ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/InstantReload.tsx ================================================ import { Deferred, router } from '@inertiajs/react' import { useEffect } from 'react' export default ({ foo, bar }: { foo?: { text: string }; bar?: { text: string } }) => { useEffect(() => { router.reload({ only: ['foo'], }) }, []) return ( <> Loading foo...}>
{foo?.text}
Loading bar...}>
{bar?.text}
) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/ManyGroups.tsx ================================================ import { Deferred, Link, usePage } from '@inertiajs/react' export default () => { const { foo, bar, baz, qux, quux } = usePage<{ foo?: { text: string } bar?: { text: string } baz?: { text: string } qux?: { text: string } quux?: { text: string } }>().props return ( <> Loading foo...}> {foo?.text} Loading bar...}> {bar?.text} Loading baz...}> {baz?.text} Loading qux...}> {qux?.text} Loading quux...}> {quux?.text} Page 1 Page 2 Many groups ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/Page1.tsx ================================================ import { Deferred, Link, usePage } from '@inertiajs/react' const Foo = () => { const { foo } = usePage<{ foo?: { text: string } }>().props return foo?.text } const Bar = () => { const { bar } = usePage<{ bar?: { text: string } }>().props return bar?.text } export default () => { return ( <> Loading foo...}>
Loading bar...
}> {() => }
Page 1 Page 2 Page 3 ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/Page2.tsx ================================================ import { Deferred, Link, usePage } from '@inertiajs/react' const Baz = () => { const { baz } = usePage<{ baz?: string }>().props return baz } const Qux = () => { const { qux } = usePage<{ qux?: string }>().props return qux } const Both = () => { const { baz, qux } = usePage<{ baz?: string; qux?: string }>().props return `both ${baz} and ${qux}` } export default () => { return ( <> Loading baz...}> Loading qux...}> Loading baz and qux...}> Page 2 ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/Page3.tsx ================================================ import { Deferred, usePage } from '@inertiajs/react' const Alpha = () => { const { alpha } = usePage<{ alpha?: string }>().props return alpha } const Beta = () => { const { beta } = usePage<{ beta?: string }>().props return beta } export default () => { return ( <> Loading alpha...}> Loading beta...}> ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/PartialReloads.tsx ================================================ import { Deferred, router, usePage } from '@inertiajs/react' const FooTimestamp = () => { const { foo } = usePage<{ foo?: { timestamp: string } }>().props return
{foo?.timestamp}
} const BarTimestamp = () => { const { bar } = usePage<{ bar?: { timestamp: string } }>().props return
{bar?.timestamp}
} const PartialReloads = () => { const reloadOnlyFoo = () => { router.reload({ only: ['foo'], }) } const reloadOnlyBar = () => { router.reload({ only: ['bar'], }) } const reloadBoth = () => { router.reload({ only: ['foo', 'bar'], }) } return ( <> Loading foo...}> Loading bar...}> ) } export default PartialReloads ================================================ FILE: packages/react/test-app/Pages/DeferredProps/RapidNavigation.tsx ================================================ import { Deferred, Link, router, usePage } from '@inertiajs/react' const Users = () => { const { users } = usePage<{ users?: { text: string } }>().props return
{users?.text}
} const Stats = () => { const { stats } = usePage<{ stats?: { text: string } }>().props return
{stats?.text}
} const Activity = () => { const { activity } = usePage<{ activity?: { text: string } }>().props return
{activity?.text}
} export default () => { const { id } = usePage<{ id: string }>().props return ( <>
Page: {id}
Loading users...}> Loading stats...}> Loading activity...}> Page A Page B Page C Navigate Away ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/ReloadWithoutOptionalChaining.tsx ================================================ import { Deferred, router, usePage } from '@inertiajs/react' const Results = () => { const { results } = usePage<{ results: { data: string[]; page: number } }>().props return ( <>
{results.data.join(', ')}
Page: {results.page}
) } export default () => { const handleReload = () => { router.reload({ data: { page: 2 }, }) } return ( <> Loading results...}> ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/WithErrors.tsx ================================================ import { Deferred, useForm, usePage } from '@inertiajs/react' const Foo = () => { const { foo } = usePage<{ foo?: { text: string } }>().props return
{foo?.text}
} export default () => { const { errors } = usePage<{ errors: { name?: string } }>().props const form = useForm({ name: '', }) const submit = () => { form.post('/deferred-props/with-errors') } return ( <> Loading foo...}> {errors?.name &&

{errors.name}

} {form.errors.name &&

{form.errors.name}

} ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/WithPartialReload.tsx ================================================ import { Deferred, Link, router, usePage } from '@inertiajs/react' const WithPartialReload = ({ withOnly, withExcept }: { withOnly?: string[]; withExcept?: string[] }) => { const handleTriggerPartialReload = () => { router.reload({ only: withOnly, except: withExcept, }) } return (
Loading...}> Prefetch
) } const DeferredUsers = () => { const props = usePage<{ users?: Array<{ id: number; name: string }> }>().props return (
{props.users?.map((user) => ( {user.name} ))}
) } export default WithPartialReload ================================================ FILE: packages/react/test-app/Pages/DeferredProps/WithQueryParams.tsx ================================================ import { Deferred, usePage } from '@inertiajs/react' const Users = () => { const { users } = usePage<{ users?: { text: string } }>().props return
{users?.text}
} export default () => { const { filter } = usePage<{ filter: string }>().props return ( <>
Filter: {filter}
Loading users...}> ) } ================================================ FILE: packages/react/test-app/Pages/DeferredProps/WithReload.tsx ================================================ import { Deferred, router, usePage } from '@inertiajs/react' const Results = () => { const { results } = usePage<{ results?: { data: string[]; page: number } }>().props return ( <>
{results?.data?.join(', ')}
Page: {results?.page}
) } export default () => { const handleReload = () => { router.reload({ data: { page: 2 }, }) } return ( <> Loading results...}> ) } ================================================ FILE: packages/react/test-app/Pages/Dump.tsx ================================================ import type { Method } from '@inertiajs/core' import { usePage } from '@inertiajs/react' import { useEffect, useMemo } from 'react' import type { MulterFile } from '../types' export default ({ headers, method, form, query, url, files, }: { headers: Record method: Method form: Record query: Record url: string files: MulterFile[] | object }) => { const page = usePage() const dump = useMemo( () => ({ headers, method, form, files: files ? files : {}, query, url, $page: page, }), [headers, method, form, files, query, url, page], ) useEffect(() => { window._inertia_request_dump = dump }, [dump]) return (
This is Inertia page component containing a data dump of the request

{JSON.stringify(dump, null, 2)}
) } ================================================ FILE: packages/react/test-app/Pages/ErrorModal.tsx ================================================ import { config, router } from '@inertiajs/react' export default ({ dialog }: { dialog: boolean }) => { const invalidVisit = () => { router.post('/non-inertia') } const invalidVisitJson = () => { router.post('/json') } if (dialog) { config.set('future.useDialogForErrorModal', true) } return (
Invalid Visit Invalid Visit (JSON response)
) } ================================================ FILE: packages/react/test-app/Pages/Events.tsx ================================================ import { Link, router, usePage } from '@inertiajs/react' declare global { interface Window { messages: unknown[] } } window.messages = [] export default () => { const payloadWithFile = { file: new File(['foobar'], 'example.bin'), } const page = usePage() const internalAlert = (...args: unknown[]) => { args.forEach((arg) => window.messages.push(arg)) } const withoutEventListeners = (e: React.MouseEvent) => { e.preventDefault() router.post(page.url, {}) } const removeInertiaListener = (e: React.MouseEvent) => { e.preventDefault() const removeEventListener = router.on('before', () => internalAlert('Inertia.on(before)')) internalAlert('Removing Inertia.on Listener') removeEventListener() router.post( page.url, {}, { onBefore: () => internalAlert('onBefore'), onStart: () => internalAlert('onStart'), }, ) } const beforeVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('before', (event) => { internalAlert('Inertia.on(before)') internalAlert(event) }) document.addEventListener('inertia:before', (event) => { internalAlert('addEventListener(inertia:before)') internalAlert(event) }) router.post( page.url, {}, { onBefore: (event) => { internalAlert('onBefore') internalAlert(event) }, onStart: () => internalAlert('onStart'), }, ) } const beforeVisitPreventLocal = (e: React.MouseEvent) => { e.preventDefault() document.addEventListener('inertia:before', () => internalAlert('addEventListener(inertia:before)')) router.on('before', () => internalAlert('Inertia.on(before)')) router.post( page.url, {}, { onBefore: () => { internalAlert('onBefore') return false }, onStart: () => internalAlert('This listener should not have been called.'), }, ) } const beforeVisitPreventGlobalInertia = (e: React.MouseEvent) => { e.preventDefault() document.addEventListener('inertia:before', () => internalAlert('addEventListener(inertia:before)')) router.on('before', () => { internalAlert('Inertia.on(before)') return false }) router.post( page.url, {}, { onBefore: () => internalAlert('onBefore'), onStart: () => internalAlert('This listener should not have been called.'), }, ) } const beforeVisitPreventGlobalNative = (e: React.MouseEvent) => { e.preventDefault() router.on('before', () => internalAlert('Inertia.on(before)')) document.addEventListener('inertia:before', (event) => { internalAlert('addEventListener(inertia:before)') event.preventDefault() }) router.post( page.url, {}, { onBefore: () => internalAlert('onBefore'), onStart: () => internalAlert('This listener should not have been called.'), }, ) } const cancelTokenVisit = (e: React.MouseEvent) => { e.preventDefault() // @ts-expect-error - We're testing that the router doesn't have an onCancelToken listener router.on('cancelToken', () => internalAlert('This listener should not have been called.')) document.addEventListener('inertia:cancelToken', () => internalAlert('This listener should not have been called.')) router.post( page.url, {}, { onCancelToken: (event) => { internalAlert('onCancelToken') internalAlert(event) }, }, ) } const startVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('start', (event) => { internalAlert('Inertia.on(start)') internalAlert(event) }) document.addEventListener('inertia:start', (event) => { internalAlert('addEventListener(inertia:start)') internalAlert(event) }) router.post( page.url, {}, { onStart: (event) => { internalAlert('onStart') internalAlert(event) }, }, ) } const progressVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('progress', (event) => { internalAlert('Inertia.on(progress)') internalAlert(event) }) document.addEventListener('inertia:progress', (event) => { internalAlert('addEventListener(inertia:progress)') internalAlert(event) }) router.post(page.url, payloadWithFile, { onProgress: (event) => { internalAlert('onProgress') internalAlert(event) }, }) } const progressNoFilesVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('progress', (event) => { internalAlert('Inertia.on(progress)') internalAlert(event) }) document.addEventListener('inertia:progress', (event) => { internalAlert('addEventListener(inertia:progress)') internalAlert(event) }) router.post( page.url, {}, { onBefore: () => internalAlert('progressNoFilesOnBefore'), onProgress: (event) => { internalAlert('onProgress') internalAlert(event) }, }, ) } const cancelVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('cancel', (event) => { internalAlert('Inertia.on(cancel)') internalAlert(event) }) document.addEventListener('inertia:cancel', (event) => { internalAlert('addEventListener(inertia:cancel)') internalAlert(event) }) router.post( page.url, {}, { onCancelToken: (token) => token.cancel(), // @ts-expect-error - We're testing that the onCancel callback has no arguments, so event will be undefined onCancel: (event) => { internalAlert('onCancel') internalAlert(event) }, }, ) } const errorVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('error', (event) => { internalAlert('Inertia.on(error)') internalAlert(event) }) document.addEventListener('inertia:error', (event) => { internalAlert('addEventListener(inertia:error)') internalAlert(event) }) router.post( '/events/errors', {}, { onError: (errors) => { internalAlert('onError') internalAlert(errors) }, }, ) } const errorPromiseVisit = (e: React.MouseEvent) => { e.preventDefault() router.post( '/events/errors', {}, { onError: () => callbackSuccessErrorPromise('onError'), onSuccess: () => internalAlert('This listener should not have been called'), onFinish: () => internalAlert('onFinish'), }, ) } const successVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('success', (event) => { internalAlert('Inertia.on(success)') internalAlert(event) }) document.addEventListener('inertia:success', (event) => { internalAlert('addEventListener(inertia:success)') internalAlert(event) }) router.post( page.url, {}, { onError: () => internalAlert('This listener should not have been called'), onSuccess: (page) => { internalAlert('onSuccess') internalAlert(page) }, }, ) } const successPromiseVisit = (e: React.MouseEvent) => { e.preventDefault() router.post( page.url, {}, { onSuccess: () => callbackSuccessErrorPromise('onSuccess'), onError: () => internalAlert('This listener should not have been called'), onFinish: () => internalAlert('onFinish'), }, ) } const finishVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('finish', (event) => { internalAlert('Inertia.on(finish)') internalAlert(event) }) document.addEventListener('inertia:finish', (event) => { internalAlert('addEventListener(inertia:finish)') internalAlert(event) }) router.post( page.url, {}, { onFinish: (event) => { internalAlert('onFinish') internalAlert(event) }, }, ) } const invalidVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('invalid', (event) => { internalAlert('Inertia.on(invalid)') internalAlert(event) }) document.addEventListener('inertia:invalid', (event) => { internalAlert('addEventListener(inertia:invalid)') internalAlert(event) }) router.post( '/non-inertia', {}, { // @ts-expect-error - We're testing that the VisitCallbacks interface does not have an onInvalid method onInvalid: () => internalAlert('This listener should not have been called.'), }, ) } const exceptionVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('exception', (event) => { internalAlert('Inertia.on(exception)') internalAlert(event) }) document.addEventListener('inertia:exception', (event) => { internalAlert('addEventListener(inertia:exception)') internalAlert(event) }) router.post( '/disconnect', {}, { // @ts-expect-error - We're testing that the VisitCallbacks interface does not have an onException method onException: () => internalAlert('This listener should not have been called.'), }, ) } const navigateVisit = (e: React.MouseEvent) => { e.preventDefault() router.on('navigate', (event) => { internalAlert('Inertia.on(navigate)') internalAlert(event) }) document.addEventListener('inertia:navigate', (event) => { internalAlert('addEventListener(inertia:navigate)') internalAlert(event) }) router.get( '/', {}, { // @ts-expect-error - We're testing that the VisitCallbacks interface does not have an onNavigate method onNavigate: () => internalAlert('This listener should not have been called.'), }, ) } const registerAllListeners = () => { router.on('before', () => internalAlert('Inertia.on(before)')) // @ts-expect-error - We're testing that the router doesn't have an onCancelToken listener router.on('cancelToken', () => internalAlert('Inertia.on(cancelToken)')) router.on('cancel', () => internalAlert('Inertia.on(cancel)')) router.on('start', () => internalAlert('Inertia.on(start)')) router.on('progress', () => internalAlert('Inertia.on(progress)')) router.on('error', () => internalAlert('Inertia.on(error)')) router.on('success', () => internalAlert('Inertia.on(success)')) router.on('invalid', () => internalAlert('Inertia.on(invalid)')) router.on('exception', () => internalAlert('Inertia.on(exception)')) router.on('finish', () => internalAlert('Inertia.on(finish)')) router.on('navigate', () => internalAlert('Inertia.on(navigate)')) document.addEventListener('inertia:before', () => internalAlert('addEventListener(inertia:before)')) document.addEventListener('inertia:cancelToken', () => internalAlert('addEventListener(inertia:cancelToken)')) document.addEventListener('inertia:cancel', () => internalAlert('addEventListener(inertia:cancel)')) document.addEventListener('inertia:start', () => internalAlert('addEventListener(inertia:start)')) document.addEventListener('inertia:progress', () => internalAlert('addEventListener(inertia:progress)')) document.addEventListener('inertia:error', () => internalAlert('addEventListener(inertia:error)')) document.addEventListener('inertia:success', () => internalAlert('addEventListener(inertia:success)')) document.addEventListener('inertia:invalid', () => internalAlert('addEventListener(inertia:invalid)')) document.addEventListener('inertia:exception', () => internalAlert('addEventListener(inertia:exception)')) document.addEventListener('inertia:finish', () => internalAlert('addEventListener(inertia:finish)')) document.addEventListener('inertia:navigate', () => internalAlert('addEventListener(inertia:navigate)')) return { onBefore: () => internalAlert('onBefore'), onCancelToken: () => internalAlert('onCancelToken'), onCancel: () => internalAlert('onCancel'), onStart: () => internalAlert('onStart'), onProgress: () => internalAlert('onProgress'), onError: () => internalAlert('onError'), onSuccess: () => internalAlert('onSuccess'), onInvalid: () => internalAlert('onInvalid'), // Does not exist. onException: () => internalAlert('onException'), // Does not exist. onFinish: () => internalAlert('onFinish'), onNavigate: () => internalAlert('onNavigate'), // Does not exist. } } const lifecycleSuccess = (e: React.MouseEvent) => { e.preventDefault() router.post(page.url, payloadWithFile, registerAllListeners()) } const lifecycleError = (e: React.MouseEvent) => { e.preventDefault() router.post('/events/errors', payloadWithFile, registerAllListeners()) } const lifecycleCancel = (e: React.MouseEvent) => { e.preventDefault() router.post('/sleep', payloadWithFile, { ...registerAllListeners(), onCancelToken: (token) => { internalAlert('onCancelToken') setTimeout(() => { internalAlert('CANCELLING!') token.cancel() }, 250) }, }) } const lifecycleCancelAfterFinish = (e: React.MouseEvent) => { e.preventDefault() type CancelToken = { cancel: () => void } let cancelToken = null as CancelToken | null router.post(page.url, payloadWithFile, { ...registerAllListeners(), onCancelToken: (token: CancelToken) => { internalAlert('onCancelToken') cancelToken = token }, onFinish: () => { internalAlert('onFinish') internalAlert('CANCELLING!') cancelToken?.cancel() }, }) } const callbackSuccessErrorPromise = (eventName: string) => { internalAlert(eventName) setTimeout(() => internalAlert('onFinish should have been fired by now if Promise functionality did not work'), 5) return new Promise((resolve) => setTimeout(resolve, 20)) } return (
{/* Listeners */} Basic Visit Remove Inertia Listener {/* Events: Before */} Before Event Before Event (Prevent) internalAlert('linkOnBefore', visit)} onStart={() => internalAlert('linkOnStart')} className="link-before" > Before Event Link { internalAlert('linkOnBefore') return false }} onStart={() => internalAlert('This listener should not have been called.')} className="link-before-prevent-local" > Before Event Link (Prevent) Before Event - Prevent globally using Inertia Event Listener Before Event - Prevent globally using Native Event Listeners {/* Events: CancelToken */} Cancel Token Event internalAlert('linkOnCancelToken', event)} className="link-canceltoken" > Cancel Token Event Link {/* Events: Cancel */} Cancel Event token.cancel()} // @ts-expect-error - We're testing that the onCancel callback has no arguments, so event will be undefined onCancel={(event) => internalAlert('linkOnCancel', event)} className="link-cancel" > Cancel Event Link {/* Events: Start */} Start Event internalAlert('linkOnStart', event)} className="link-start" > Start Event Link {/* Events: Progress */} Progress Event Missing Progress Event (no files) internalAlert('linkOnProgress', event)} className="link-progress" > Progress Event Link internalAlert('linkProgressNoFilesOnBefore')} onProgress={(event) => internalAlert('linkOnProgress', event)} className="link-progress-no-files" > Progress Event Link (no files) {/* Events: Error */} Error Event Error Event (delaying onFinish w/ Promise) internalAlert('linkOnError', errors)} onSuccess={() => internalAlert('This listener should not have been called')} className="link-error" > Error Event Link callbackSuccessErrorPromise('linkOnError')} onSuccess={() => internalAlert('This listener should not have been called')} onFinish={() => internalAlert('linkOnFinish')} className="link-error-promise" > Error Event Link (delaying onFinish w/ Promise) {/* Events: Success */} Success Event Success Event (delaying onFinish w/ Promise) internalAlert('This listener should not have been called')} onSuccess={(event) => internalAlert('linkOnSuccess', event)} className="link-success" > Success Event Link internalAlert('This listener should not have been called')} onSuccess={() => callbackSuccessErrorPromise('linkOnSuccess')} onFinish={() => internalAlert('linkOnFinish')} className="link-success-promise" > Success Event Link (delaying onFinish w/ Promise) {/* Events: Invalid */} Invalid Event {/* Events: Exception */} Exception Event {/* Events: Finish */} Finish Event internalAlert('linkOnFinish', event)} className="link-finish" > Finish Event Link {/* Events: Navigate */} Navigate Event {/* Events: Prefetch */} internalAlert('linkOnPrefetching', visit)} onPrefetched={(response, visit) => internalAlert('linkOnPrefetched', response, visit)} className="link-prefetch-hover" > Prefetch Event Link (Hover) {/* Lifecycles */} Lifecycle Success Lifecycle Error Lifecycle Cancel Lifecycle Cancel - After Finish
) } ================================================ FILE: packages/react/test-app/Pages/Flash/ClientSideVisits.tsx ================================================ import { router, usePage } from '@inertiajs/react' declare global { interface Window { flashCount: number } } window.flashCount ??= 0 export default () => { const page = usePage() const withFlash = () => { router.replace({ flash: { foo: 'bar' }, onFlash: () => window.flashCount++, }) } const withFlashFunction = () => { router.replace({ flash: (flash) => ({ ...flash, bar: 'baz' }), onFlash: () => window.flashCount++, }) } const withoutFlash = () => { router.replace({ props: (props) => ({ ...props }), onFlash: () => window.flashCount++, }) } return (
{JSON.stringify(page.flash)}
) } ================================================ FILE: packages/react/test-app/Pages/Flash/Events.tsx ================================================ import { router, usePage } from '@inertiajs/react' declare global { interface Window { messages: unknown[] } } window.messages = [] const internalAlert = (...args: unknown[]) => { window.messages.push(...args) } export default () => { const page = usePage() const visitWithFlash = () => { router.on('flash', (event) => { internalAlert('Inertia.on(flash)') internalAlert(event.detail.flash) }) document.addEventListener('inertia:flash', (event) => { internalAlert('addEventListener(inertia:flash)') internalAlert((event as CustomEvent).detail.flash) }) router.post( '/flash/events/with-data', {}, { onFlash: (flash) => { internalAlert('onFlash') internalAlert(flash) }, onSuccess: (page) => { internalAlert('onSuccess') internalAlert(page.flash) }, }, ) } const visitWithoutFlash = () => { router.on('flash', () => { internalAlert('Inertia.on(flash)') }) document.addEventListener('inertia:flash', () => { internalAlert('addEventListener(inertia:flash)') }) router.post( '/flash/events/without-data', {}, { onFlash: () => { internalAlert('onFlash') }, onSuccess: () => { internalAlert('onSuccess') }, }, ) } const navigateAway = () => { router.get('/') } return ( ) } ================================================ FILE: packages/react/test-app/Pages/Flash/InitialFlash.tsx ================================================ import { router, usePage } from '@inertiajs/react' import { useRef, useState } from 'react' export default () => { const page = usePage() const [, forceUpdate] = useState(0) const flashEvents = useRef[]>([]) const listenerSetup = useRef(false) if (!listenerSetup.current) { listenerSetup.current = true router.on('flash', (e) => { flashEvents.current.push(e.detail.flash) forceUpdate((n) => n + 1) }) } return (
{page.flash ? JSON.stringify(page.flash) : 'no-flash'} {JSON.stringify(flashEvents.current)}
) } ================================================ FILE: packages/react/test-app/Pages/Flash/Partial.tsx ================================================ import { router, usePage } from '@inertiajs/react' import { useRef, useState } from 'react' export default ({ count }: { count: number }) => { const page = usePage() const [flashEventCount, setFlashEventCount] = useState(0) const listenerSetup = useRef(false) if (!listenerSetup.current) { listenerSetup.current = true router.on('flash', () => { setFlashEventCount((n) => n + 1) }) } const reloadWithSameFlash = () => { router.reload({ only: ['count'], data: { flashType: 'same', count: Date.now() } }) } const reloadWithDifferentFlash = () => { router.reload({ only: ['count'], data: { flashType: 'different', count: Date.now() } }) } return (
{JSON.stringify(page.flash)} {flashEventCount} {count}
) } ================================================ FILE: packages/react/test-app/Pages/Flash/RouterFlash.tsx ================================================ import { router, usePage } from '@inertiajs/react' export default () => { const page = usePage() const setFlash = () => { router.flash({ foo: 'bar' }) } const setFlashKeyValue = () => { router.flash('foo', 'bar') } const mergeFlash = () => { router.flash((current) => ({ ...current, bar: 'baz' })) } const clearFlash = () => { router.flash(() => ({})) } return (
{JSON.stringify(page.flash)}
) } ================================================ FILE: packages/react/test-app/Pages/Flash/WithDeferred.tsx ================================================ import { Deferred, router, usePage } from '@inertiajs/react' import { useRef, useState } from 'react' export default ({ data }: { data?: string }) => { const page = usePage() const [flashEventCount, setFlashEventCount] = useState(0) const listenerSetup = useRef(false) if (!listenerSetup.current) { listenerSetup.current = true router.on('flash', () => { setFlashEventCount((n) => n + 1) }) } return (
{JSON.stringify(page.flash)} {flashEventCount} Loading...
}>
{data}
) } ================================================ FILE: packages/react/test-app/Pages/Flash/WithInfiniteScroll.tsx ================================================ import { InfiniteScroll, router, usePage } from '@inertiajs/react' import { useRef, useState } from 'react' export default ({ users }: { users: { data: { id: number; name: string }[] } }) => { const page = usePage() const [flashEventCount, setFlashEventCount] = useState(0) const listenerSetup = useRef(false) if (!listenerSetup.current) { listenerSetup.current = true router.on('flash', () => { setFlashEventCount((n) => n + 1) }) } return (
{JSON.stringify(page.flash)} {flashEventCount} {users.data.map((user) => (
{user.name}
))}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/ChildComponent.tsx ================================================ import { Form } from '@inertiajs/react' import { useMemo, useState } from 'react' const ChildElement = ({ name }: { name: string }) => { const [internalState, setInternalState] = useState('') const transformedState = useMemo(() => internalState.toUpperCase(), [internalState]) return (
setInternalState(e.target.value)} />
) } export default () => { return ( {({ isDirty }) => ( <>

Form Elements

Form is {isDirty ? 'dirty' : 'clean'}
)} ) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Context/ChildComponent.tsx ================================================ import { useFormContext } from '@inertiajs/react' export default ({ formId }: { formId?: string }) => { const form = useFormContext() return ( <> {form ? (
Child: Form is {form.isDirty ? 'dirty' : 'clean'} {form.hasErrors && | Child: Form has errors} {form.processing && | Child: Form is processing} {form.wasSuccessful && | Child: Form was successful} {form.recentlySuccessful && | Child: Form recently successful} {form.errors.name && | Error: {form.errors.name}}
) : (
No form context available
)} {!formId && ( <> )} ) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Context/DeeplyNestedComponent.tsx ================================================ import { useFormContext } from '@inertiajs/react' export default () => { const form = useFormContext() return form ? (
Deeply Nested: Form is {form.isDirty ? 'dirty' : 'clean'}
) : (
No context
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Context/Default.tsx ================================================ import { Form } from '@inertiajs/react' import ChildComponent from './ChildComponent' import NestedComponent from './NestedComponent' import OutsideFormComponent from './OutsideFormComponent' export default () => ( <>
{({ isDirty, hasErrors, errors }) => ( <>
Parent: Form is {isDirty ? 'dirty' : 'clean'} {hasErrors && | Parent: Form has errors} {errors.name && | {errors.name}}
)} ) ================================================ FILE: packages/react/test-app/Pages/FormComponent/Context/Methods.tsx ================================================ import { Form } from '@inertiajs/react' import MethodsTestComponent from './MethodsTestComponent' export default () => (
{({ errors }) => ( <> {Object.keys(errors).length > 0 &&
{JSON.stringify(errors, null, 2)}
} {/* Hidden input */}
{/* Number input */}
{/* Deep nested input */}
{/* Indexed array of objects */}
{/* Disabled input (should be ignored) */}
)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/EmptyAction.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Empty Action Test

{({ errors }) => ( <>
{errors.name &&

{errors.name}

}
)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Errors.tsx ================================================ import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [errorBag, setErrorBag] = useState(null) return (
{({ errors, hasErrors, setError, clearErrors }) => ( <>

Form Errors

{hasErrors ?
Form has errors
:
No errors
}
{errors.name}
{errors.handle}
)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Events.tsx ================================================ import { Form } from '@inertiajs/react' import { useCallback, useMemo, useState } from 'react' export default () => { const [events, setEvents] = useState([]) const [cancelInOnBefore, setCancelInOnBefore] = useState(false) const [shouldFail, setShouldFail] = useState(false) const [shouldDelay, setShouldDelay] = useState(false) const [cancelToken, setCancelToken] = useState<{ cancel: () => void } | null>(null) function log(eventName: string) { setEvents((previousEvents) => [...previousEvents, eventName]) } const action = useMemo(() => { if (shouldFail) { return '/form-component/events/errors' } if (shouldDelay) { return '/form-component/events/delay' } return '/form-component/events/success' }, [shouldFail, shouldDelay]) const formEvents = useMemo( () => ({ onBefore: () => { log('onBefore') if (cancelInOnBefore) { log('onCancel') return false } }, onStart: () => log('onStart'), onProgress: () => log('onProgress'), onFinish: () => log('onFinish'), onCancel: () => log('onCancel'), onSuccess: () => log('onSuccess'), onError: () => log('onError'), onCancelToken: (token: { cancel: () => void }) => { log('onCancelToken') setCancelToken(token) }, }), [cancelInOnBefore], ) const cancelVisit = useCallback(() => { if (cancelToken) { cancelToken.cancel() setCancelToken(null) } }, [cancelToken]) return (
{({ processing, progress, wasSuccessful, recentlySuccessful }) => ( <>

Form Events & State

Events: {events.join(',')}
Processing: {String(processing)}
Progress:{' '} {progress?.percentage || 0}
Was successful: {String(wasSuccessful)}
Recently successful: {String(recentlySuccessful)}
)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/FormTarget.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Headers.tsx ================================================ import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [headers, setHeaders] = useState>({ 'X-Foo': 'Bar', }) function addCustomHeader() { setHeaders((previousHeaders) => ({ ...previousHeaders, 'X-Custom': 'MyCustomValue', })) } return (

Form Headers

) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/InvalidateTags.tsx ================================================ import { Form, Link } from '@inertiajs/react' export default ({ lastLoaded, propType }: { lastLoaded: number; propType: string }) => { return (

Form Component with invalidateCacheTags

Form Component Invalidate Tags Test Page
Last loaded at {lastLoaded}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Methods.tsx ================================================ import type { Method } from '@inertiajs/core' import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [method, setMethod] = useState('get') return (

HTTP Methods

Current method: {method}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/MixedKeySerialization.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Mixed Key Serialization

) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Options.tsx ================================================ import type { Method, QueryStringArrayFormatOption } from '@inertiajs/core' import { Form } from '@inertiajs/react' import { useMemo, useState } from 'react' import Article from './../Article' export default () => { const [only, setOnlyValues] = useState([]) const [except, setExceptValues] = useState([]) const [reset, setResetValues] = useState([]) const [replace, setReplace] = useState(false) const [state, setState] = useState('Default State') const [preserveScroll, setPreserveScroll] = useState(false) const [preserveState, setPreserveState] = useState(false) const [preserveUrl, setPreserveUrl] = useState(false) const [queryStringArrayFormat, setQueryStringArrayFormat] = useState( undefined, ) function setOnly() { setOnlyValues(['users']) } function setExcept() { setExceptValues(['stats']) } function setReset() { setResetValues(['orders']) } function enableReplace() { setReplace(true) } function enablePreserveScroll() { setPreserveScroll(true) } function enablePreserveState() { setPreserveState(true) setState('Replaced State') } function enablePreserveUrl() { setPreserveUrl(true) } const action = useMemo(() => { if (preserveScroll) { return '/article' } if (preserveState) { return '/form-component/options' } if (preserveUrl) { return '/form-component/options?page=2' } return queryStringArrayFormat ? '/dump/get' : '/dump/post' }, [preserveScroll, preserveState, preserveUrl, queryStringArrayFormat]) const method = useMemo((): Method => { if (preserveScroll || preserveState || preserveUrl) { return 'get' } return queryStringArrayFormat ? 'get' : 'post' }, [preserveScroll, preserveState, preserveUrl, queryStringArrayFormat]) return (
{() => ( <>

Form Options

State: {state}
{preserveScroll &&
} )} ) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/BeforeValidation.tsx ================================================ import { Form } from '@inertiajs/react' import { isEqual } from 'lodash-es' export default function PrecognitionBefore() { const handleBeforeValidation = ( newRequest: { data: Record | null; touched: string[] }, oldRequest: { data: Record | null; touched: string[] }, ) => { const payloadIsCorrect = isEqual(newRequest, { data: { name: 'block' }, touched: ['name'] }) && isEqual(oldRequest, { data: {}, touched: [] }) if (payloadIsCorrect && newRequest.data?.name === 'block') { return false } return true } return (

Precognition - onBefore

{({ errors, invalid, validate, validating }) => ( <>
validate('name', { onBeforeValidation: handleBeforeValidation, }) } /> {invalid('name') &&

{errors.name}

}
validate('email')} /> {invalid('email') &&

{errors.email}

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Callbacks.tsx ================================================ import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [successCalled, setSuccessCalled] = useState(false) const [errorCalled, setErrorCalled] = useState(false) const [finishCalled, setFinishCalled] = useState(false) return (

Form Precognition Callbacks

Callbacks Test

{({ validate, validating, touch }) => ( <>
touch('name')} />
{validating &&

Validating...

} {successCalled &&

onPrecognitionSuccess called!

} {errorCalled &&

onValidationError called!

} {finishCalled &&

onFinish called!

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Cancel.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Precognition - Cancel Tests

Auto Cancel Test

{({ invalid, errors, validate, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Default.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Precognition

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

} {valid('name') &&

Name is valid!

}
validate('email')} /> {invalid('email') &&

{errors.email}

} {valid('email') &&

Email is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/DynamicArrayInputs.tsx ================================================ import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [items, setItems] = useState>([]) function addItem() { setItems([...items, { name: '' }]) } function updateItem(index: number, value: string) { const newItems = [...items] newItems[index] = { name: value } setItems(newItems) } return (
{({ invalid, errors, validate, validating }) => ( <> {items.map((item, idx) => (
updateItem(idx, e.target.value)} onBlur={() => validate(`items.${idx}.name`)} /> {invalid(`items.${idx}.name`) &&

{errors[`items.${idx}.name`]}

}
))} {validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/ErrorSync.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Precognition Error Sync Test

{({ invalid, errors, validate, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

}
validate('email')} /> {invalid('email') &&

{errors.email}

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Files.tsx ================================================ import { Form } from '@inertiajs/react' import { useState } from 'react' export default () => { const [validateFilesEnabled, setValidateFilesEnabled] = useState(false) return (

Form Precognition Files

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

} {valid('name') &&

Name is valid!

}
{invalid('avatar') &&

{errors.avatar}

} {valid('avatar') &&

Avatar is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Headers.tsx ================================================ import { Form } from '@inertiajs/react' export default function PrecognitionHeaders() { return (

Precognition - Custom Headers

{({ invalid, errors, validate, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Methods.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Precognition - Touch, Reset & Validate

{({ invalid, errors, validate, touch, touched, validating, reset }) => ( <>
touch('name')} /> {invalid('name') &&

{errors.name}

}
touch('email')} /> {invalid('email') &&

{errors.email}

}
{validating &&

Validating...

}

{touched('name') ? 'Name is touched' : 'Name is not touched'}

{touched('email') ? 'Email is touched' : 'Email is not touched'}

{touched() ? 'Form has touched fields' : 'Form has no touched fields'}

)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/Transform.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Precognition Transform

({ name: String(data.name || '').repeat(2) })} > {({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

} {valid('name') &&

Name is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/TransformKeys.tsx ================================================ import { Form } from '@inertiajs/react' // eslint-disable-next-line @typescript-eslint/no-explicit-any const transformData = (data: Record) => { const document = data.document || {} return document } export default () => { return (

Form Precognition Transform Keys

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('customer.email')} /> {invalid('customer.email') &&

{errors['customer.email']}

} {valid('customer.email') &&

Email is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/WithAllErrors.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Precognition - All Errors

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') && (
{Array.isArray(errors.name) ? ( errors.name.map((error, index) => (

{error}

)) ) : (

{errors.name}

)}
)} {valid('name') &&

Name is valid!

}
validate('email')} /> {invalid('email') && (
{Array.isArray(errors.email) ? ( errors.email.map((error, index) => (

{error}

)) ) : (

{errors.email}

)}
)} {valid('email') &&

Email is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/WithAllErrorsConfig.tsx ================================================ import { config, Form } from '@inertiajs/react' export default () => { // Set global config for withAllErrors (no prop on the Form component) config.set('form.withAllErrors', true) return (

Form Precognition - All Errors via Config

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') && (
{Array.isArray(errors.name) ? ( errors.name.map((error, index) => (

{error}

)) ) : (

{errors.name}

)}
)} {valid('name') &&

Name is valid!

}
validate('email')} /> {invalid('email') && (
{Array.isArray(errors.email) ? ( errors.email.map((error, index) => (

{error}

)) ) : (

{errors.email}

)}
)} {valid('email') &&

Email is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Precognition/WithoutAllErrors.tsx ================================================ import { Form } from '@inertiajs/react' export default () => { return (

Form Precognition - Array Errors

{({ invalid, errors, validate, valid, validating }) => ( <>
validate('name')} /> {invalid('name') &&

{errors.name}

} {valid('name') &&

Name is valid!

}
validate('email')} /> {invalid('email') &&

{errors.email}

} {valid('email') &&

Email is valid!

}
{validating &&

Validating...

} )}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Progress.tsx ================================================ import { Form } from '@inertiajs/react' import { useEffect, useRef, useState } from 'react' export default () => { const [showProgress, setShowProgress] = useState(undefined) const [nprogressVisible, setNprogressVisible] = useState(false) const [nprogressAppearances, setNprogressAppearances] = useState(0) const observerRef = useRef(null) function disableProgress() { setShowProgress(false) } useEffect(() => { observerRef.current = new MutationObserver(() => { const nprogressElement = document.querySelector('#nprogress') as HTMLElement | null const nprogressIsCurrentlyVisible = nprogressElement && nprogressElement.style.display !== 'none' if (nprogressIsCurrentlyVisible) { if (!nprogressVisible) { setNprogressVisible(true) setNprogressAppearances((previousCount) => previousCount + 1) } } else { setNprogressVisible(false) } }) observerRef.current.observe(document.body, { childList: true, subtree: true }) return () => observerRef.current?.disconnect() }, [nprogressVisible]) return (

Progress

Nprogress appearances: {nprogressAppearances}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Ref.tsx ================================================ import { FormComponentRef } from '@inertiajs/core' import { Form } from '@inertiajs/react' import { useRef } from 'react' export default function Ref() { const formRef = useRef(null) const submitProgrammatically = () => { formRef.current?.submit() } const resetForm = () => { formRef.current?.reset() } const resetNameField = () => { formRef.current?.reset('name') } const clearAllErrors = () => { formRef.current?.clearErrors() } const setTestError = () => { formRef.current?.setError('name', 'This is a test error') } const setCurrentAsDefaults = () => { formRef.current?.defaults() } const callPrecognitionMethods = () => { const validator = formRef.current?.validator() if (validator && !formRef.current?.touched('company') && !formRef.current?.valid('company')) { formRef.current?.validate({ only: ['company'] }) } } return (

Form Ref Test

{({ isDirty, hasErrors, errors }) => ( <> {/* State display for testing */}
Form is {isDirty ? 'dirty' : 'clean'}
{hasErrors &&
Form has errors
} {errors.name &&
{errors.name}
}
)}
) } ================================================ FILE: packages/react/test-app/Pages/FormComponent/Reset.tsx ================================================ import { FormComponentRef } from '@inertiajs/core' import { Form } from '@inertiajs/react' import { useRef } from 'react' declare global { interface Window { resetForm: (...fields: string[]) => void } } export default function Reset() { const formRef = useRef(null) // Expose reset function to window for testing window.resetForm = (...fields: string[]) => { formRef.current?.reset(...fields) } return (

Form Reset

{/* Basic Text Inputs */}

Basic Text Inputs

{/* Select Elements */}

Select Elements

{/* Radio Buttons */}

Radio Buttons

{/* Radio buttons with default checked */}
{/* Radio buttons without default */}
{/* Radio buttons designed to test multiple defaults edge case */}
{/* Checkboxes */}

Checkboxes

{/* Checkbox (single) with default checked */}
{/* Checkbox (single) without default */}
{/* Checkbox (multiple) with some checked */}
{/* Multiple Select Elements */}

Multiple Select Elements

{/* File Inputs & Textareas */}

File Inputs & Textareas

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Context/MethodsTestComponent.svelte ================================================ {#if $form} {#if $form.processing}Child: processing{/if} {#if $form.wasSuccessful}Child: was successful{/if} {#if $form.recentlySuccessful}Child: recently successful{/if} {#if $form.hasErrors}
{JSON.stringify($form.errors, null, 2)}
{/if} {#if getDataResult}
{getDataResult}
{/if} {#if getFormDataResult}
{getFormDataResult}
{/if} {:else}
No form context available
{/if} ================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Context/Multiple.svelte ================================================
Form 1 Parent: {isDirty ? 'dirty' : 'clean'} {#if errors.name} | Error: {errors.name}{/if}
Form 2 Parent: {isDirty ? 'dirty' : 'clean'} {#if errors.name} | Error: {errors.name}{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Context/NestedComponent.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Context/OutsideFormComponent.svelte ================================================ {#if $form === undefined}
Correctly returns undefined when used outside a Form component
{:else}
Unexpectedly has form context
{/if} ================================================ FILE: packages/svelte/test-app/Pages/FormComponent/DataMethods.svelte ================================================

Test getData() and getFormData() Methods

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/DefaultValue.svelte ================================================

Form Default Values

{errors['user.name']}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/DisableWhileProcessing.svelte ================================================

Form Disable While Processing Test

{#if errors.name}

{errors.name}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/DottedKeys.svelte ================================================

Dotted Keys Form Test

Basic Dotted Keys

Escaped Dots

Mixed Notation

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Elements.svelte ================================================

Form Elements

Form is {isDirty ? 'dirty' : 'clean'}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/EmptyAction.svelte ================================================

Form Empty Action Test

{#if errors.name}

{errors.name}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Errors.svelte ================================================

Form Errors

{#if hasErrors}
Form has errors
{:else}
No errors
{/if}
{errors.name || ''}
{errors.handle || ''}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Events.svelte ================================================

Form Events & State

Events: {events.join(',')}
Processing: {String(processing)}
Progress: {progress?.percentage || 0}
Was successful: {String(wasSuccessful)}
Recently successful: {String(recentlySuccessful)}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/FormTarget.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Headers.svelte ================================================

Form Headers

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/InvalidateTags.svelte ================================================

Form Component with invalidateCacheTags

Form Component Invalidate Tags Test Page
Last loaded at {lastLoaded}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Methods.svelte ================================================

HTTP Methods

Current method: {method}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/MixedKeySerialization.svelte ================================================

Mixed Key Serialization

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Options.svelte ================================================

Form Options

State: {state}
{#if preserveScroll}
{/if} ================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/BeforeValidation.svelte ================================================

Precognition - onBefore

validate('name', { onBeforeValidation: handleBeforeValidation, })} /> {#if invalid('name')}

{errors.name}

{/if}
validate('email')} /> {#if invalid('email')}

{errors.email}

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Callbacks.svelte ================================================

Form Precognition Callbacks

Callbacks Test

touch('name')} />
{#if validating}

Validating...

{/if} {#if successCalled}

onPrecognitionSuccess called!

{/if} {#if errorCalled}

onValidationError called!

{/if} {#if finishCalled}

onFinish called!

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Cancel.svelte ================================================

Precognition - Cancel Tests

Auto Cancel Test

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Default.svelte ================================================

Form Precognition

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if} {#if valid('name')}

Name is valid!

{/if}
validate('email')} /> {#if invalid('email')}

{errors.email}

{/if} {#if valid('email')}

Email is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/DynamicArrayInputs.svelte ================================================
{#each items as item, idx (idx)}
validate(`items.${idx}.name`)} /> {#if invalid(`items.${idx}.name`)}

{errors[`items.${idx}.name`]}

{/if}
{/each} {#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/ErrorSync.svelte ================================================

Precognition Error Sync Test

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if}
validate('email')} /> {#if invalid('email')}

{errors.email}

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Files.svelte ================================================

Form Precognition Files

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if} {#if valid('name')}

Name is valid!

{/if}
{#if invalid('avatar')}

{errors.avatar}

{/if} {#if valid('avatar')}

Avatar is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Headers.svelte ================================================

Precognition - Custom Headers

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Methods.svelte ================================================

Form Precognition - Touch, Reset & Validate

touch('name')} /> {#if invalid('name')}

{errors.name}

{/if}
touch('email')} /> {#if invalid('email')}

{errors.email}

{/if}
{#if validating}

Validating...

{/if}

{touched('name') ? 'Name is touched' : 'Name is not touched'}

{touched('email') ? 'Email is touched' : 'Email is not touched'}

{touched() ? 'Form has touched fields' : 'Form has no touched fields'}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/Transform.svelte ================================================

Form Precognition Transform

({ name: String(data.name || '').repeat(2) })} let:invalid let:errors let:validate let:valid let:validating >
validate('name')} /> {#if invalid('name')}

{errors.name}

{/if} {#if valid('name')}

Name is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/TransformKeys.svelte ================================================

Form Precognition Transform Keys

validate('customer.email')} /> {#if invalid('customer.email')}

{errors['customer.email']}

{/if} {#if valid('customer.email')}

Email is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/WithAllErrors.svelte ================================================

Form Precognition - All Errors

validate('name')} /> {#if invalid('name')}
{#if Array.isArray(errors.name)} {#each errors.name as error, index (index)}

{error}

{/each} {:else}

{errors.name}

{/if}
{/if} {#if valid('name')}

Name is valid!

{/if}
validate('email')} /> {#if invalid('email')}
{#if Array.isArray(errors.email)} {#each errors.email as error, index (index)}

{error}

{/each} {:else}

{errors.email}

{/if}
{/if} {#if valid('email')}

Email is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/WithAllErrorsConfig.svelte ================================================

Form Precognition - All Errors via Config

validate('name')} /> {#if invalid('name')}
{#if Array.isArray(errors.name)} {#each errors.name as error, index (index)}

{error}

{/each} {:else}

{errors.name}

{/if}
{/if} {#if valid('name')}

Name is valid!

{/if}
validate('email')} /> {#if invalid('email')}
{#if Array.isArray(errors.email)} {#each errors.email as error, index (index)}

{error}

{/each} {:else}

{errors.email}

{/if}
{/if} {#if valid('email')}

Email is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Precognition/WithoutAllErrors.svelte ================================================

Form Precognition - Array Errors

validate('name')} /> {#if invalid('name')}

{errors.name}

{/if} {#if valid('name')}

Name is valid!

{/if}
validate('email')} /> {#if invalid('email')}

{errors.email}

{/if} {#if valid('email')}

Email is valid!

{/if}
{#if validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Progress.svelte ================================================

Progress

Nprogress appearances: {nprogressAppearances}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Ref.svelte ================================================

Form Ref Test

Form is {isDirty ? 'dirty' : 'clean'}
{#if hasErrors}
Form has errors
{/if} {#if errors.name}
{errors.name}
{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Reset.svelte ================================================

Form Reset

Basic Text Inputs

Select Elements

Radio Buttons

Checkboxes

Multiple Select Elements

File Inputs & Textareas

HTML5 Input Types

Complex Nested Fields

Special Cases

Dotted & Array Notation

Numeric Values

Submit

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/ResetAttributes/ResetOnError.svelte ================================================

{errors.name || ''}

{errors.email || ''}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/ResetAttributes/ResetOnErrorFields.svelte ================================================

{errors.name || ''}

{errors.email || ''}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/ResetAttributes/ResetOnSuccess.svelte ================================================

{errors.name || ''}

{errors.email || ''}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/ResetAttributes/ResetOnSuccessFields.svelte ================================================

{errors.name || ''}

{errors.email || ''}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/SetDefaultsOnSuccess.svelte ================================================

{isDirty ? 'Form is dirty' : 'Form is clean'}

{errors.name || ''}

{errors.email || ''}

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/SubmitButton.svelte ================================================

Submit Button Test

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/SubmitComplete/Defaults.svelte ================================================

OnSubmitComplete Defaults Test

props.defaults()}>

{isDirty ? 'Form is dirty' : 'Form is clean'}

{#if errors.name}

{errors.name}

{/if}
{#if errors.email}

{errors.email}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/SubmitComplete/Redirect.svelte ================================================

Form Empty Action Test

props.reset('name')}>
{#if errors.name}

{errors.name}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/SubmitComplete/Reset.svelte ================================================

OnSubmitComplete Reset Test

props.reset('name')}>
{#if errors.name}

{errors.name}

{/if}
{#if errors.email}

{errors.email}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Transform.svelte ================================================

Transform Function

Current transform: {transformType}
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/UppercaseMethod.svelte ================================================

Uppercase Method Test

================================================ FILE: packages/svelte/test-app/Pages/FormComponent/ViewTransition.svelte ================================================
{ viewTransition.ready.then(() => console.log('ready')) viewTransition.updateCallbackDone.then(() => console.log('updateCallbackDone')) viewTransition.finished.then(() => console.log('finished')) }, }} >
================================================ FILE: packages/svelte/test-app/Pages/FormComponent/Wayfinder.svelte ================================================

Wayfinder Example

================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Data.svelte ================================================
{#if $form.errors.name} {$form.errors.name} {/if} {#if $form.errors.handle} {$form.errors.handle} {/if} {#if $form.errors.remember} {$form.errors.remember} {/if} Form has {$form.hasErrors ? '' : 'no '}errors
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Dirty.svelte ================================================
Form is {#if $form.isDirty}dirty{:else}clean{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/EmptyForm.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Errors.svelte ================================================
{#if $form.errors.name} {$form.errors.name} {/if} {#if $form.errors.handle} {$form.errors.handle} {/if} {#if $form.errors.remember} {$form.errors.remember} {/if} Form has {$form.hasErrors ? '' : 'no '}errors
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/ErrorsClearOnResubmit.svelte ================================================
{#if $form.errors.name} {$form.errors.name} {/if} {#if $form.errors.handle} {$form.errors.handle} {/if} Form has {$form.hasErrors ? '' : 'no '}errors
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Events.svelte ================================================
Form was {$form.wasSuccessful ? '' : 'not '}successful Form was {$form.recentlySuccessful ? '' : 'not '}recently successful
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Methods.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Nested.svelte ================================================
Repository Tags
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/BeforeValidation.svelte ================================================
$form.validate('name', { onBeforeValidation: handleBeforeValidation, })} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if} {#if $form.valid('email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Callbacks.svelte ================================================
$form.touch('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
{#if $form.validating}

Validating...

{/if} {#if successCalled}

onPrecognitionSuccess called!

{/if} {#if errorCalled}

onValidationError called!

{/if} {#if finishCalled}

onFinish called!

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Cancel.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Compatibility.svelte ================================================

Compatibility Test Page

$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if} {#if $form.valid('email')}

Email is valid!

{/if}
$form.validate('company')} /> {#if $form.invalid('company')}

{$form.errors.company}

{/if} {#if $form.valid('company')}

company is valid!

{/if}
{#if $form.validating}

Validating...

{/if}

Name touched: {$form.touched('name') ? 'yes' : 'no'}

Email touched: {$form.touched('email') ? 'yes' : 'no'}

Company touched: {$form.touched('company') ? 'yes' : 'no'}

Any touched: {$form.touched() ? 'yes' : 'no'}

================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Default.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if} {#if $form.valid('email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/DynamicArrayInputs.svelte ================================================
{#each $form.items as item, idx (idx)}
$form.validate(`items.${idx}.name`)} /> {#if $form.invalid(`items.${idx}.name`)}

{$form.errors[`items.${idx}.name`]}

{/if} {#if $form.valid(`items.${idx}.name`)}

Valid!

{/if}
{/each} {#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/ErrorSync.svelte ================================================

Precognition Error Sync Test (Form Helper)

$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Files.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
{#if $form.invalid('avatar')}

{$form.errors.avatar}

{/if} {#if $form.valid('avatar')}

Avatar is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Headers.svelte ================================================
$form.validate('name', { headers: { 'X-Custom-Header': 'custom-value' }, })} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Instantiate.svelte ================================================ {#if $currentFormStore && $currentFormStore.validating}

Validating...

{/if} {#if $currentFormStore && $currentFormStore.errors && $currentFormStore.errors.name}

{$currentFormStore.errors.name}

{/if} ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Methods.svelte ================================================
$form.touch('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if}
$form.touch('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if}
{#if $form.validating}

Validating...

{/if}

{$form.touched('name') ? 'Name is touched' : 'Name is not touched'}

{$form.touched('email') ? 'Email is touched' : 'Email is not touched'}

{$form.touched() ? 'Form has touched fields' : 'Form has no touched fields'}

================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/Transform.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/TransformKeys.svelte ================================================
$form.validate('customer.email')} /> {#if $form.invalid('customer.email')}

{$form.errors['customer.email']}

{/if} {#if $form.valid('customer.email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/WithAllErrors.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}
{#if Array.isArray($form.errors.name)} {#each $form.errors.name as error, index (index)}

{error}

{/each} {:else}

{$form.errors.name}

{/if}
{/if} {#if $form.valid('name')}

Name is valid!

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}
{#if Array.isArray($form.errors.email)} {#each $form.errors.email as error, index (index)}

{error}

{/each} {:else}

{$form.errors.email}

{/if}
{/if} {#if $form.valid('email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/WithAllErrorsConfig.svelte ================================================
form.validate('name')} /> {#if $form.invalid('name')}
{#if Array.isArray($form.errors.name)} {#each $form.errors.name as error, index (index)}

{error}

{/each} {:else}

{$form.errors.name}

{/if}
{/if} {#if $form.valid('name')}

Name is valid!

{/if}
form.validate('email')} /> {#if $form.invalid('email')}
{#if Array.isArray($form.errors.email)} {#each $form.errors.email as error, index (index)}

{error}

{/each} {:else}

{$form.errors.email}

{/if}
{/if} {#if $form.valid('email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Precognition/WithoutAllErrors.svelte ================================================
$form.validate('name')} /> {#if $form.invalid('name')}

{$form.errors.name}

{/if} {#if $form.valid('name')}

Name is valid!

{/if}
$form.validate('email')} /> {#if $form.invalid('email')}

{$form.errors.email}

{/if} {#if $form.valid('email')}

Email is valid!

{/if}
{#if $form.validating}

Validating...

{/if}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/RememberEdit.svelte ================================================

Edit User {user.id}

================================================ FILE: packages/svelte/test-app/Pages/FormHelper/RememberIndex.svelte ================================================

Users Index

    {#each users as user (user.id)}
  • Edit {user.name}
  • {/each}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/ReservedKeys.svelte ================================================
Form created with progress value: {$form.progress}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/Transform.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Any.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Child.svelte ================================================

Name: {form.name}

Email: {form.email}

================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/CircularReferences.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Data.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/DynamicInputName.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Errors.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/FormDataCallback.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Generic.svelte ================================================
{form}
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Nullable.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/NullableNestedObject.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/OptionalProps.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Parent.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/Precognition.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/ValidationKey.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/WrapperChild.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/FormHelper/TypeScript/WrapperParent.svelte ================================================ {form.children.map((child) => child.title)} ================================================ FILE: packages/svelte/test-app/Pages/History/Page.svelte ================================================ Page 1 Page 2 Page 3 Page 4 Page 5
This is page {pageNumber}.
Multi byte character: {multiByte}
================================================ FILE: packages/svelte/test-app/Pages/History/Version.svelte ================================================ Page 1 Page 2 ================================================ FILE: packages/svelte/test-app/Pages/HistoryQuota/Page.svelte ================================================

History Quota Test - Page {pageNumber}

Data size: {largeData?.length?.toLocaleString()} bytes

{#each Array.from({ length: 20 }, (_, i) => i + 1) as n (n)} Page {n} {/each}
================================================ FILE: packages/svelte/test-app/Pages/HistoryThrottle.svelte ================================================

History Throttle Test

State updates: {callCount}

Go Home
================================================ FILE: packages/svelte/test-app/Pages/Home.svelte ================================================
This is the Test App Entrypoint page Basic Links 'Replace' Links Manual basic visits Manual 'Replace' visits Manual Redirect visit Manual External Redirect visit
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/CustomElement.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/CustomTriggersRef.svelte ================================================

Custom Triggers with Refs Test

tableHeader} endElement={() => tableFooter} itemsElement={() => tableBody} let:loading >
Spacer
{#each users.data as user (user.id)} {/each} {#if loading} {/if}
ID Name
{user.id} {user.name}
Loading...
Table Footer - Triggers when this comes into view
Spacer
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/CustomTriggersRefObject.svelte ================================================

Custom Triggers with Svelte Ref Objects Test

tableHeader} endElement={() => tableFooter} itemsElement={() => tableBody} let:loading >
Spacer
{#each users.data as user (user.id)} {/each} {#if loading} {/if}
ID Name
{user.id} {user.name}
Loading...
Table Footer - Triggers when this comes into view
Spacer
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/CustomTriggersSelector.svelte ================================================

Custom Triggers with Selectors Test

Spacer
{#each users.data as user (user.id)} {/each} {#if loading} {/if}
ID Name
{user.id} {user.name}
Loading...
Table Footer - Triggers when this comes into view
Spacer
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/DataTable.svelte ================================================ {#each users.data as user (user.id)} {/each} {#if loading} {/if}
ID Name
{user.id} {user.name}
Loading...
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Deferred.svelte ================================================
Loading deferred scroll prop...

Has more previous items: {hasMore}

{#each users?.data ?? [] as user (user.id)} {/each}

Has more next items: {hasMore}

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/DualContainers.svelte ================================================

Users1 Container

Loading more users1...
{#each users1.data as user (user.id)} {/each}

Users2 Container

Loading more users2...
{#each users2.data as user (user.id)} {/each}

Content below the scroll containers to verify page doesn't scroll.

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/DualSibling.svelte ================================================

Dual Sibling InfiniteScroll

Two InfiniteScroll components side by side, sharing the window scroll

Users 1

{#each users1.data as user (user.id)} {/each}

Users 2

{#each users2.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Empty.svelte ================================================

Empty Dataset Test

Loading...
{#each users.data as user (user.id)} {/each} {#if users.data.length === 0}
No users found.
{/if}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte ================================================
No Filter A-M N-Z
Current filter: {filter || 'none'}
Current search: {search || 'none'}
Loading...
{#each users.data as user (user.id)} {/each}
No Filter A-M N-Z
Current filter: {filter || 'none'}
Current search: {search || 'none'}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/FilteringManual.svelte ================================================
Current search: {search || 'none'}

Has more previous items: {hasMore}

{#each users.data as user (user.id)} {/each}

Has more next items: {hasMore}

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/FilteringReset.svelte ================================================
Current search: {search || 'none'}
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Grid.svelte ================================================
Loading more users...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/HorizontalScroll.svelte ================================================
Loading...
{#each users.data as user (user.id)}
{user.name}
{/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/InfiniteScrollWithLink.svelte ================================================
Go back to Links
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/InvisibleFirstChild.svelte ================================================

Infinite Scroll with Invisible First Child

Loading...
Hidden first element
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Links.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Manual.svelte ================================================

Has more previous items: {hasMore}

{#each users.data as user (user.id)} {/each}

Has more next items: {hasMore}

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ManualAfter.svelte ================================================ {#each users.data as user (user.id)} {/each}
{#if loading}

Loading...

{/if}

Manual mode: {manualMode}

{#if manualMode} {/if}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/OverflowX.svelte ================================================
{#each users.data as user (user.id)}
{user.name}
{/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/PreserveUrl.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ProgrammaticRef.svelte ================================================

Programmatic Ref Test

Has more previous items: {hasPrevious.toString()}

Has more next items: {hasNext.toString()}

Loading...
{#each users.data as user (user.id)} {/each}

Total items on page: {users.data.length}

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ReloadUnrelated.svelte ================================================
Current time: {time}
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/RememberState.svelte ================================================
Go Home
{#each users.data as user (user.id)} {/each}
{#if loading}

Loading...

{/if}

Manual mode: {manualMode}

{#if manualMode} {/if}
Go to Home
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Reverse.svelte ================================================
Loading...
{#each reversedUsers as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ReverseShortContent.svelte ================================================
Header
Loading...
{#each reversedUsers as user (user.id)}
{user.name}
{/each}
Footer
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ScrollContainer.svelte ================================================

Infinite Scroll in Container

This component scrolls within a fixed-height container, not the full page.

Loading more users...
{#each users.data as user (user.id)} {/each}

Content below the scroll container to verify page doesn't scroll.

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/ShortContent.svelte ================================================ {#each users.data as user (user.id)} {/each}
{user.id} {user.name}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/Toggles.svelte ================================================

Loading...
{#each users.data as user (user.id)} {/each}

Total items on page: {users.data.length}

================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/TriggerBoth.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/TriggerEndBuffer.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/TriggerStartBuffer.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/UpdateQueryString.svelte ================================================
Loading...
{#each users.data as user (user.id)} {/each}
================================================ FILE: packages/svelte/test-app/Pages/InfiniteScroll/UserCard.svelte ================================================
{user.name}
================================================ FILE: packages/svelte/test-app/Pages/Links/AsWarning.svelte ================================================
This is the links page that demonstrates inertia-links with an 'as' warning {method} Link
================================================ FILE: packages/svelte/test-app/Pages/Links/AsWarningFalse.svelte ================================================
This is the links page that demonstrates inertia-links without the 'as' warning
================================================ FILE: packages/svelte/test-app/Pages/Links/AutomaticCancellation.svelte ================================================
This is the links page that demonstrates that only one visit can be active at a time console.log('cancelled')} on:start={() => console.log('started')} class="visit" > Link
================================================ FILE: packages/svelte/test-app/Pages/Links/CancelSyncRequest.svelte ================================================

Page {page}

Go to Page 1 Go to Page 2 Go to Page 3 ================================================ FILE: packages/svelte/test-app/Pages/Links/Data/AutoConverted.svelte ================================================
This is the links page that demonstrates the automatic conversion of plain objects to form-data GET Link
================================================ FILE: packages/svelte/test-app/Pages/Links/Data/FormData.svelte ================================================
This is the links page that demonstrates passing data through FormData objects GET Link
================================================ FILE: packages/svelte/test-app/Pages/Links/Data/Object.svelte ================================================
This is the links page that demonstrates passing data through plain objects GET Link QSAF Defaults QSAF Indices QSAF Brackets
================================================ FILE: packages/svelte/test-app/Pages/Links/DataLoading.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Links/Headers.svelte ================================================
This is the links page that demonstrates passing custom headers Standard visit Link GET Link
================================================ FILE: packages/svelte/test-app/Pages/Links/Location.svelte ================================================
This is the links page that demonstrates location visits inertia-links Location visit
================================================ FILE: packages/svelte/test-app/Pages/Links/Method.svelte ================================================
This is the links page that demonstrates inertia-link methods GET Link
================================================ FILE: packages/svelte/test-app/Pages/Links/PartialReloads.svelte ================================================
This is the links page that demonstrates partial reloads Foo is now {foo} Bar is now {bar} Baz is now {baz}
{JSON.stringify(headers, null, 2)}
Update All Only foo + bar Only baz Except foo + bar Except baz
================================================ FILE: packages/svelte/test-app/Pages/Links/PathTraversal.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Links/PreserveScroll.svelte ================================================
This is the links page that demonstrates scroll preservation with scroll regions Foo is now {foo} Preserve Scroll Reset Scroll Preserve Scroll (Callback) Reset Scroll (Callback) Off-site link Article
================================================ FILE: packages/svelte/test-app/Pages/Links/PreserveScrollFalse.svelte ================================================
This is the links page that demonstrates scroll preservation without scroll regions Foo is now {foo} Preserve Scroll Reset Scroll Preserve Scroll (Callback) Reset Scroll (Callback) Off-site link
================================================ FILE: packages/svelte/test-app/Pages/Links/PreserveState.svelte ================================================
This is the links page that demonstrates preserve state on Links Foo is now {foo} [State] Preserve: true [State] Preserve: false [State] Preserve Callback: true [State] Preserve Callback: false
================================================ FILE: packages/svelte/test-app/Pages/Links/PreserveUrl.svelte ================================================
This is the links page that demonstrates preserve url on Links Foo is now {foo} [URL] Preserve: true [URL] Preserve: false {#if items}
{#each items.data as item, index (index)}
{item}
{/each}
Items loaded: {items.data.length} {items.next_page_url ? 'true' : 'false'} {#if items.next_page_url} Load More {/if} {#if items.next_page_url} {/if}
{/if}
================================================ FILE: packages/svelte/test-app/Pages/Links/PropUpdate.svelte ================================================
The Link
================================================ FILE: packages/svelte/test-app/Pages/Links/Reactivity.svelte ================================================
This page demonstrates reactivity in Inertia links. Click the button to change the link properties. Submit Prefetch Link
================================================ FILE: packages/svelte/test-app/Pages/Links/Replace.svelte ================================================
This is the links page that demonstrates replace on Links [State] Replace: true [State] Replace: false
================================================ FILE: packages/svelte/test-app/Pages/Links/ScrollRegionList.svelte ================================================
Scrollable list with scroll region
Clicked user: {user_id || 'none'}
{#each users as user (user.id)}
{user.name}
{/each}
================================================ FILE: packages/svelte/test-app/Pages/Links/UrlFragments.svelte ================================================
This is the links page that demonstrates url fragment behaviour
Document scroll position is {documentScrollLeft} & {documentScrollTop}
Basic link Fragment link Non-existent fragment link
This is the element with id 'target'
================================================ FILE: packages/svelte/test-app/Pages/MatchPropsOnKey.svelte ================================================
bar count is {bar.length}
baz count is {baz.length}
foo.data count is {foo.data.length}
first foo.data name is {foo.data[0].name}
last foo.data name is {foo.data[foo.data.length - 1].name}
foo.companies count is {foo.companies.length}
first foo.companies name is {foo.companies[0].name}
last foo.companies name is {foo.companies[foo.companies.length - 1].name}
foo.teams count is {foo.teams.length}
first foo.teams name is {foo.teams[0].name}
last foo.teams name is {foo.teams[foo.teams.length - 1].name}
foo.page is {foo.page}
foo.per_page is {foo.per_page}
foo.meta.label is {foo.meta.label}
================================================ FILE: packages/svelte/test-app/Pages/MergeNestedProps.svelte ================================================

{users.data.map((user) => user.name).join(', ')}

Page: {users.meta.page}, Per Page: {users.meta.perPage}

================================================ FILE: packages/svelte/test-app/Pages/MergeProps.svelte ================================================
bar count is {bar.length}
foo count is {foo.length}
================================================ FILE: packages/svelte/test-app/Pages/NavigateNonInertia.svelte ================================================

Navigate Non-Inertia

Go to non-Inertia page

================================================ FILE: packages/svelte/test-app/Pages/NetworkError.svelte ================================================

Network Error

{#if error}
Network error occurred
{/if}
================================================ FILE: packages/svelte/test-app/Pages/OnceProps/ClientSideVisit.svelte ================================================

Foo: {foo}

Bar: {bar}

================================================ FILE: packages/svelte/test-app/Pages/OnceProps/CustomKeyPageA.svelte ================================================

Permissions: {userPermissions}

Bar: {bar}

Go to Custom Key Page B ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/CustomKeyPageB.svelte ================================================

Permissions: {permissions}

Bar: {bar}

Go to Custom Key Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/DeferredPageA.svelte ================================================
Loading foo...

Foo: {foo?.text}

Bar: {bar}

Go to Deferred Page B Go to Deferred Page C ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/DeferredPageB.svelte ================================================
Loading foo...

Foo: {foo?.text}

Bar: {bar}

Go to Deferred Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/DeferredPageC.svelte ================================================
Loading foo...

Foo: {foo?.text}

Bar: {bar}

Go to Deferred Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/MergePageA.svelte ================================================

Items count: {items.length}

Bar: {bar}

Go to Merge Page B ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/MergePageB.svelte ================================================

Items count: {items.length}

Bar: {bar}

Go to Merge Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/OptionalPageA.svelte ================================================

Foo: {foo ?? 'not loaded'}

Bar: {bar}

Go to Optional Page B ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/OptionalPageB.svelte ================================================

Foo: {foo ?? 'not loaded'}

Bar: {bar}

Go to Optional Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PageA.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to Page B Go to Page C Go to Page D Go to Page E (short cache) ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PageB.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PageC.svelte ================================================ Go to Page A Go to Page B Go to Page D ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PageD.svelte ================================================

Foo: {foo}

Bar: {bar}

================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PageE.svelte ================================================

Foo: {foo}

Bar: {bar}

================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PartialReloadA.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to Partial Reload B ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/PartialReloadB.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to Partial Reload A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/SlowDeferredPageA.svelte ================================================
Loading foo...

Foo: {foo}

Bar: {bar}

Go to Page B ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/SlowDeferredPageB.svelte ================================================
Loading foo...

Foo: {foo}

Bar: {bar}

Go to Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/TtlPageA.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to TTL Page B Go to TTL Page C ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/TtlPageB.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to TTL Page A ================================================ FILE: packages/svelte/test-app/Pages/OnceProps/TtlPageC.svelte ================================================

Foo: {foo}

Bar: {bar}

Go to TTL Page A ================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/RenderFunction/Nested/PageA.svelte ================================================
Nested Persistent Layout - Page A Page B
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/RenderFunction/Nested/PageB.svelte ================================================
Nested Persistent Layout - Page B Page A
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/RenderFunction/Simple/PageA.svelte ================================================
Simple Persistent Layout - Page A Page B
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/RenderFunction/Simple/PageB.svelte ================================================
Simple Persistent Layout - Page B Page A
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/Shorthand/Nested/PageA.svelte ================================================
Nested Persistent Layout - Page A Page B
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/Shorthand/Nested/PageB.svelte ================================================
Nested Persistent Layout - Page B Page A
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/Shorthand/Simple/PageA.svelte ================================================
Simple Persistent Layout - Page A Page B
================================================ FILE: packages/svelte/test-app/Pages/PersistentLayouts/Shorthand/Simple/PageB.svelte ================================================
Simple Persistent Layout - Page B Page A
================================================ FILE: packages/svelte/test-app/Pages/Poll/Hook.svelte ================================================ Home ================================================ FILE: packages/svelte/test-app/Pages/Poll/HookManual.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Poll/RouterManual.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Poll/UnchangedData.svelte ================================================

replaceState calls: {replaceStateCalls}

polls finished: {pollsFinished}

================================================ FILE: packages/svelte/test-app/Pages/Prefetch/AfterError.svelte ================================================
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/Form.svelte ================================================

Random Value: {$page.props.randomValue}

Back to Test Page
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/Page.svelte ================================================
This is page {pageNumber}
Last loaded at {lastLoaded}
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/PreserveState.svelte ================================================
Current Page: {page}
Timestamp: {timestamp}

Prefetch:

Load (should use cache if prefetched):

================================================ FILE: packages/svelte/test-app/Pages/Prefetch/SWR.svelte ================================================
This is page {pageNumber}
Last loaded at {lastLoaded}
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/Tags.svelte ================================================

Form Test

This is tags page {pageNumber}
Last loaded at {lastLoaded}
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/TestPage.svelte ================================================
Go to Prefetch Form
================================================ FILE: packages/svelte/test-app/Pages/Prefetch/Wayfinder.svelte ================================================

Is Prefetched: {isPrefetched}

Is Prefetching: {isPrefetching}

================================================ FILE: packages/svelte/test-app/Pages/PreserveEqualProps.svelte ================================================

Preserve Equal Props

Count A: {nestedA.count}

Date B: {nestedB.date}

Effect A Count: {effectACount}

Effect B Count: {effectBCount}

Submit and redirect back
================================================ FILE: packages/svelte/test-app/Pages/ProgressComponent.svelte ================================================

Progress API Test

Logs: {logs.join(', ')}
================================================ FILE: packages/svelte/test-app/Pages/Reload/Concurrent.svelte ================================================
Foo: {foo}
Bar: {bar}
================================================ FILE: packages/svelte/test-app/Pages/Reload/ConcurrentWithData.svelte ================================================
Foo: {foo}
Bar: {bar}
Timeframe: {timeframe}
================================================ FILE: packages/svelte/test-app/Pages/Remember/Components/ComponentA.svelte ================================================
This component uses a string 'key' for the remember functionality.
================================================ FILE: packages/svelte/test-app/Pages/Remember/Components/ComponentB.svelte ================================================
This component uses a string 'key' for the remember functionality.
================================================ FILE: packages/svelte/test-app/Pages/Remember/Default.svelte ================================================
Navigate away
================================================ FILE: packages/svelte/test-app/Pages/Remember/FormHelper/Default.svelte ================================================
{#if $form.errors.name} {$form.errors.name} {/if} {#if $form.errors.handle} {$form.errors.handle} {/if} {#if $form.errors.remember} {$form.errors.remember} {/if} Navigate away
================================================ FILE: packages/svelte/test-app/Pages/Remember/FormHelper/Password.svelte ================================================
Navigate away
================================================ FILE: packages/svelte/test-app/Pages/Remember/FormHelper/Remember.svelte ================================================
{#if $form.errors.name} {$form.errors.name} {/if} {#if $form.errors.handle} {$form.errors.handle} {/if} {#if $form.errors.remember} {$form.errors.remember} {/if} Navigate away
================================================ FILE: packages/svelte/test-app/Pages/Remember/MultipleComponents.svelte ================================================
Navigate away Navigate off-site
================================================ FILE: packages/svelte/test-app/Pages/Remember/Object.svelte ================================================
Navigate away
================================================ FILE: packages/svelte/test-app/Pages/Remember/Router.svelte ================================================

Foo: {foo}

Bar: {bar}

================================================ FILE: packages/svelte/test-app/Pages/SSR/Page1.svelte ================================================

SSR Page 1

Name: {user.name}

Email: {user.email}

    {#each items as item (item)}
  • {item}
  • {/each}

Count: {count}

Navigate to another page
================================================ FILE: packages/svelte/test-app/Pages/SSR/Page2.svelte ================================================

SSR Page 2

Navigated: {navigatedTo}

Go back
================================================ FILE: packages/svelte/test-app/Pages/SSR/PageWithScriptElement.svelte ================================================

SSR Page With Script Element

{message}

================================================ FILE: packages/svelte/test-app/Pages/ScrollAfterRender.svelte ================================================

Article Header

Page {page}

Sunt culpa sit sunt enim aliquip. Esse ea ea quis voluptate. Enim consectetur aliqua ex ex magna cupidatat id minim sit elit.

Go to page {page + 1} {#each Array(500).keys() as i (i)}

Sunt culpa sit sunt enim aliquip. Esse ea ea quis voluptate. Enim consectetur aliqua ex ex magna cupidatat id minim sit elit. Amet pariatur occaecat pariatur duis eiusmod dolore magna. Et commodo cupidatat in commodo elit cupidatat minim qui id non enim ad. Culpa aliquip ad Lorem sit consectetur ullamco culpa duis nisi et fugiat mollit eiusmod. Laboris voluptate veniam consequat proident in nulla irure velit.

Sit sint laboris sunt eiusmod ipsum laborum eiusmod amet commodo exercitation in duis magna. Proident sunt minim in elit qui. Id pariatur commodo fugiat excepteur in deserunt Lorem ipsum occaecat est. Excepteur sit tempor ipsum ex officia veniam enim amet velit fugiat mollit cillum. Incididunt aliqua nulla id occaecat nulla. Non ea ad est occaecat deserunt officia qui commodo exercitation.

Voluptate laborum quis aliqua ullamco magna amet ullamco laborum qui cillum eu. Dolore dolore aliqua proident proident sunt ipsum in. Enim velit dolore labore dolor quis incididunt duis culpa Lorem. Eu adipisicing non elit fugiat voluptate labore ipsum dolore consectetur commodo. Et in et cillum duis consequat quis ex eu commodo. Eiusmod aliqua excepteur consectetur eiusmod aute et consectetur sit pariatur dolore qui officia pariatur.

Non sunt eu mollit qui reprehenderit. Aute culpa anim voluptate do in esse duis laborum ad dolore. Ullamco nisi in nostrud officia do. Duis pariatur officia id duis. Deserunt ad incididunt est sint consectetur reprehenderit mollit est Lorem ea pariatur anim dolor adipisicing. Nostrud irure magna nostrud laboris aute sunt veniam laboris veniam incididunt sit. Nulla proident ad aliqua fugiat culpa sunt est in dolor velit ad irure nulla.

Do aute laborum deserunt non laborum voluptate voluptate. Anim ut laborum magna sunt cupidatat irure. Cupidatat fugiat minim sint cillum laborum excepteur irure id est irure ad occaecat adipisicing enim. Deserunt nulla anim proident velit irure nostrud est est reprehenderit consequat pariatur qui. Fugiat Lorem sint eu laborum minim pariatur cillum mollit nulla consequat ullamco ex. Ex consectetur ad ut irure fugiat occaecat aliqua exercitation cillum ipsum anim dolore tempor.

Adipisicing consequat irure fugiat Lorem deserunt aliquip do cupidatat. Lorem labore elit ex qui nostrud qui cillum sunt adipisicing occaecat. Sunt nostrud amet amet cupidatat fugiat Lorem quis nulla id cillum esse eu. Ullamco aliqua dolore irure amet mollit anim velit dolore.

Veniam cupidatat ipsum ea officia ipsum nisi laborum culpa qui dolore. Aliqua Lorem nisi labore ea velit aliquip irure excepteur eu. Laboris proident duis non labore sunt quis aute tempor laboris enim anim eiusmod.

Minim proident ut aliqua ea ut culpa fugiat ullamco nisi esse nostrud reprehenderit id. Id id ullamco velit anim nisi magna Lorem tempor. Et veniam occaecat ut labore consequat fugiat duis.

Adipisicing ea consectetur adipisicing aute eu pariatur enim labore consequat occaecat consectetur minim nisi. Cillum commodo sunt labore reprehenderit. Duis esse excepteur magna tempor eiusmod exercitation Lorem reprehenderit excepteur pariatur. Esse cupidatat occaecat magna do aliquip Lorem. Consectetur adipisicing consequat dolore nostrud esse eu cillum id commodo duis. Aliquip dolor cillum cupidatat fugiat.

Ex eiusmod id est laborum sunt ex ea aute adipisicing ad magna deserunt duis. Nostrud velit dolore id commodo quis enim fugiat. Sint non quis consectetur voluptate aliqua dolore nulla. Irure sit reprehenderit sint laboris non elit. Duis minim nisi esse dolor. Sit ex in consequat non occaecat commodo irure et. Commodo qui ipsum Lorem magna consequat consequat et minim eiusmod Lorem eiusmod cupidatat voluptate.

{/each}
================================================ FILE: packages/svelte/test-app/Pages/ScrollRegionPreserveUrl.svelte ================================================
Page: {page}
{#each items as num (num)}
Item {num}
{/each}
================================================ FILE: packages/svelte/test-app/Pages/ScrollSmooth.svelte ================================================

{page === 'long' ? 'Long Page' : 'Short Page'}

{#if page === 'long'} Go to Short Page {:else} Go to Long Page {/if}
================================================ FILE: packages/svelte/test-app/Pages/ScrollableParent.svelte ================================================

ScrollableParent Tests

overflow-x: hidden

Test

{results.overflowXHidden?.tagName || 'null'}

overflow-x: scroll

Wide content

{results.overflowXScroll?.dataset?.testid || 'null'}

overflow-y: auto + height

Content

Content

Content

{results.overflowYAuto?.dataset?.testid || 'null'}

overflow-y: auto (no height)

Content

{results.overflowYAutoNoHeight?.tagName || 'null'}

overflow-x: scroll, overflow-y: hidden

Wide content

{results.overflowXScrollOverflowYHidden?.dataset?.testid || 'null'}

overflow-x: scroll + max-width

Wide content

{results.horizontalScrollCalc?.dataset?.testid || 'null'}

overflow-y: auto + max-height

Content

Content

Content

{results.verticalScrollMaxHeight?.dataset?.testid || 'null'}

Nested containers

Outer

Content

Content

Content

Content

Content

{results.nestedScroll?.dataset?.testid || 'null'}

overflow: auto (no constraints)

Content

{results.overflowAutoNoConstraints?.tagName || 'null'}

Flex horizontal carousel

Item
Item
Item

{results.flexHorizontalCarousel?.dataset?.testid || 'null'}

overflow-x: scroll (overflow-y coerced)

Wide

{results.coercedAutoNoConstraint?.dataset?.testid || 'null'}

display: contents (skip parent)

Content

Content

Content

Content

{results.displayContents?.dataset?.testid || 'null'}

overflow: clip

Content

Content

{results.overflowClip?.tagName || 'null'}

overflow: overlay

Content

Content

Content

Content

{results.overflowOverlay?.dataset?.testid || 'null'}

overflow-x: auto + inline width

Wide

{results.inlineWidthStyle?.dataset?.testid || 'null'}

overflow: scroll (both)

Wide and tall

Content

Content

{results.bothScrollDirections?.dataset?.testid || 'null'}

overflow-y: auto + overflow-x: visible

Content

Content

Content

{results.overflowYAutoOverflowXVisible?.dataset?.testid || 'null'}

overflow-y: auto + overflow-x: clip

Content

Content

Content

{results.overflowYAutoOverflowXClip?.dataset?.testid || 'null'}

overflow-x: auto + overflow-y: visible

Wide content

{results.overflowXAutoOverflowYVisible?.dataset?.testid || 'null'}

overflow-x: auto + overflow-y: clip

Wide content

{results.overflowXAutoOverflowYClip?.dataset?.testid || 'null'}

overflow-y: auto + overflow-x: hidden (no height)

Content

{results.overflowYAutoOverflowXHidden?.tagName || 'null'}

overflow-x: auto + overflow-y: hidden (no width)

Content

{results.overflowXAutoOverflowYHidden?.tagName || 'null'}

================================================ FILE: packages/svelte/test-app/Pages/Svelte/PropsAndPageStore.svelte ================================================

foo prop is {foo}

$page.props.foo is {$page.props.foo}

pageProps.foo is {pageProps.foo}

$sveltePage.props.foo is {$sveltePage.props.foo}

Bar Baz Home
================================================ FILE: packages/svelte/test-app/Pages/TypeScriptCreateInertiaApp.ts ================================================ // This file is used for checking the TypeScript implementation; there is no Playwright test depending on it. import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte' declare module '@inertiajs/core' { export interface InertiaConfig { sharedPageProps: { auth: { user: { name: string } | null } } } } // createInertiaApp setup should include shared props without explicit generic createInertiaApp({ resolve: (name) => { const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, setup({ el, App, props }) { console.log(props.initialPage.props.auth.user?.name) // @ts-expect-error - 'email' does not exist on user console.log(props.initialPage.props.auth.user?.email) new App({ target: el!, props }) }, }) ================================================ FILE: packages/svelte/test-app/Pages/TypeScriptFlash.svelte ================================================ {#if $page.flash.toast} {$page.flash.toast.message} {/if} ================================================ FILE: packages/svelte/test-app/Pages/TypeScriptProps.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/ViewTransition/FormErrors.svelte ================================================

View Transition Form Errors Test

{#if $form.errors.name}

{$form.errors.name}

{/if}
================================================ FILE: packages/svelte/test-app/Pages/ViewTransition/PageA.svelte ================================================

Page A - View Transition Test

{ viewTransition.ready.then(() => console.log('ready')) viewTransition.updateCallbackDone.then(() => console.log('updateCallbackDone')) viewTransition.finished.then(() => console.log('finished')) }}>Link to Page B ================================================ FILE: packages/svelte/test-app/Pages/ViewTransition/PageB.svelte ================================================

Page B - View Transition Test

================================================ FILE: packages/svelte/test-app/Pages/Visits/AfterError.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Visits/AutomaticCancellation.svelte ================================================
This is the page that demonstrates that only one visit can be active at a time Link
================================================ FILE: packages/svelte/test-app/Pages/Visits/Data/AutoConverted.svelte ================================================
This is the page that demonstrates automatic conversion of plain objects to form-data using manual visits Visit Link POST Link PUT Link PATCH Link DELETE Link
================================================ FILE: packages/svelte/test-app/Pages/Visits/Data/FormData.svelte ================================================
This is the page that demonstrates manual visit data passing through FormData objects Visit Link POST Link PUT Link PATCH Link DELETE Link
================================================ FILE: packages/svelte/test-app/Pages/Visits/Data/Object.svelte ================================================
This is the page that demonstrates manual visit data passing through plain objects Visit Link GET Link POST Link PUT Link PATCH Link DELETE Link QSAF Defaults QSAF Indices QSAF Brackets Delete Query Param
================================================ FILE: packages/svelte/test-app/Pages/Visits/ErrorBags.svelte ================================================
This is the page that demonstrates error bags using manual visits Default visit Basic visit POST visit
================================================ FILE: packages/svelte/test-app/Pages/Visits/Headers.svelte ================================================
This is the page that demonstrates passing custom headers through manual visits Standard visit Link Specific visit Link GET Link POST Link PUT Link PATCH Link DELETE Link Overriden Link
================================================ FILE: packages/svelte/test-app/Pages/Visits/Location.svelte ================================================
This is the page that demonstrates location visits Location visit
================================================ FILE: packages/svelte/test-app/Pages/Visits/Method.svelte ================================================
This is the page that demonstrates manual visit methods Standard visit Link Specific visit Link GET Link POST Link PUT Link PATCH Link DELETE Link
================================================ FILE: packages/svelte/test-app/Pages/Visits/PartialReloads.svelte ================================================
This is the page that demonstrates partial reloads using manual visits Foo is now {foo} Bar is now {bar} Baz is now {baz}
{headers}
Update All (visit) 'Only' foo + bar (visit) 'Only' baz (visit) 'Except' foo + bar (visit) 'Except' baz (visit) Update All (GET) 'Only' foo + bar (GET) 'Only' baz (GET) 'Except' foo + bar (GET) 'Except' baz (GET)
================================================ FILE: packages/svelte/test-app/Pages/Visits/PreserveScroll.svelte ================================================
This is the page that demonstrates scroll preservation with scroll regions when using manual visits Foo is now {foo} Preserve Scroll Reset Scroll Preserve Scroll (Callback)
Reset Scroll (Callback) Preserve Scroll (GET) Reset Scroll (GET) Off-site link
================================================ FILE: packages/svelte/test-app/Pages/Visits/PreserveScrollFalse.svelte ================================================
This is the page that demonstrates scroll preservation without scroll regions when using manual visits Foo is now {foo} Preserve Scroll Reset Scroll Preserve Scroll (Callback)
Reset Scroll (Callback) Preserve Scroll (GET) Reset Scroll (GET) Off-site link
================================================ FILE: packages/svelte/test-app/Pages/Visits/PreserveState.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Visits/ReloadOnMount.svelte ================================================
Name is {name}
================================================ FILE: packages/svelte/test-app/Pages/Visits/Replace.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/Visits/UrlFragments.svelte ================================================
This is the page that demonstrates url fragment behaviour using manual visits
Document scroll position is {documentScrollLeft} & {documentScrollTop}
Basic visit Fragment visit Non-existent fragment visit Basic GET visit Fragment GET visit Non-existent fragment GET visit
This is the element with id 'target'
================================================ FILE: packages/svelte/test-app/Pages/Visits/Wayfinder.svelte ================================================ ================================================ FILE: packages/svelte/test-app/Pages/WhenVisible.svelte ================================================
Loading first one...
First one is visible!
Loading second one...
Second one is visible!
Loading third one...
Third one is visible!
Loading fourth one...
Loading fifth one...
Count is now {count}
================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleArrayReload.svelte ================================================

WhenVisible + Array Props + Reload

Loading array data...

{firstData?.text}

{secondData?.text}

================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleBackButton.svelte ================================================

WhenVisible + Back Button

Navigate Away

Loading lazy data...

{lazyData?.text}

Loading always data...

Always: {lazyData?.text}

================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleFetching.svelte ================================================
Lazy data loaded!
{#if fetching}
Fetching in background...
{/if}
Loading lazy data...
================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleMergeParams.svelte ================================================
Loading data only...
Data only loaded: {dataOnlyProp?.text}
Loading merged...
Merged loaded: {mergedProp?.text}
Loading merged with callback...
Merged with callback loaded: {mergedWithCallbackProp?.text}
================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleParamsUpdate.svelte ================================================

Current param: {paramValue}

Data loaded: {lazyData?.text}

Loading lazy data...

================================================ FILE: packages/svelte/test-app/Pages/WhenVisibleReload.svelte ================================================

WhenVisible + Reload

Loading lazy data...

{lazyData?.text}
================================================ FILE: packages/svelte/test-app/app.ts ================================================ import type { VisitOptions } from '@inertiajs/core' import { createInertiaApp, type ResolvedComponent, router } from '@inertiajs/svelte' window.testing = { Inertia: router } const withAppDefaults = new URLSearchParams(window.location.search).get('withAppDefaults') createInertiaApp({ page: window.initialPage, resolve: async (name) => { const pages = import.meta.glob('./Pages/**/*.svelte', { eager: true }) if (name === 'DeferredProps/InstantReload') { // Add small delay to ensure the component is loaded after the initial page load // This is for projects that don't use { eager: true } in import.meta.glob await new Promise((resolve) => setTimeout(resolve, 50)) } return pages[`./Pages/${name}.svelte`] }, setup({ el, App, props }) { const hydrate = el?.hasAttribute('data-server-rendered') new App({ target: el!, props, hydrate }) }, ...(withAppDefaults && { defaults: { visitOptions: (href: string, options: VisitOptions) => { return { headers: { ...options.headers, 'X-From-App-Defaults': 'test' } } }, }, }), }) ================================================ FILE: packages/svelte/test-app/eslint.config.js ================================================ import js from '@eslint/js' import svelte from 'eslint-plugin-svelte' import globals from 'globals' import ts from 'typescript-eslint' import svelteConfig from './svelte.config.js' export default ts.config( { files: ['**/*.js', '**/*.ts', '**/*.svelte'], }, { ignores: ['node_modules', 'dist/**/*', '*.config.js', '**/*.d.ts', '*.timestamp-*'], }, js.configs.recommended, ...ts.configs.recommended, ...svelte.configs.recommended, { languageOptions: { ecmaVersion: 2020, sourceType: 'module', globals: { ...globals.browser, ...globals.es2020, }, }, }, { files: ['**/*.ts', '**/*.svelte'], // See more details at: https://typescript-eslint.io/packages/parser/ languageOptions: { parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], // Add support for additional file extensions, such as .svelte parser: ts.parser, // Specify a parser for each language, if needed: // parser: { // ts: ts.parser, // js: espree, // Use espree for .js files (add: import espree from 'espree') // typescript: ts.parser // }, // We recommend importing and specifying svelte.config.js. // By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly. // While certain Svelte settings may be statically loaded from svelte.config.js even if you don’t specify it, // explicitly specifying it ensures better compatibility and functionality. // // If non-serializable properties are included, running ESLint with the --cache flag will fail. // In that case, please remove the non-serializable properties. (e.g. `svelteConfig: { ...svelteConfig, kit: { ...svelteConfig.kit, typescript: undefined }}`) svelteConfig, }, }, }, { rules: { 'svelte/no-navigation-without-resolve': 'off', 'svelte/no-useless-mustaches': 'off', }, }, { files: ['**/*.ts', '**/*.svelte'], rules: { '@typescript-eslint/unbound-method': 'error', }, }, ) ================================================ FILE: packages/svelte/test-app/index.html ================================================ Inertia Svelte - Testing Environment
================================================ FILE: packages/svelte/test-app/package.json ================================================ { "type": "module", "scripts": { "build": "vite build .", "build:ssr": "vite build --ssr ssr.ts --outDir dist", "dev": "nodemon --watch . --watch ../../core/dist --watch ../../svelte/dist --ext js,ts,svelte,html,json --ignore dist/ --exec 'vite build .'", "lint": "eslint .", "type-check": "svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { "@eslint/js": "^9.39.3", "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.8", "eslint": "^9.39.3", "eslint-plugin-svelte": "^3.15.0", "globals": "^17.3.0", "nodemon": "^3.1.14", "svelte": "^4.2.20", "svelte-check": "^4.4.4", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^5.4.21" }, "dependencies": { "@inertiajs/core": "workspace:*", "@inertiajs/svelte": "workspace:*" } } ================================================ FILE: packages/svelte/test-app/ssr.ts ================================================ import { createInertiaApp, type ResolvedComponent } from '@inertiajs/svelte' import createServer from '@inertiajs/svelte/server' createServer((page) => createInertiaApp({ page, resolve: (name) => { const pages = import.meta.glob('./Pages/SSR/**/*.svelte', { eager: true }) return pages[`./Pages/${name}.svelte`] }, setup({ App, props }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (App as any).render(props) }, defaults: { future: { useScriptElementForInitialPage: page.component === 'SSR/PageWithScriptElement', }, }, }), ) ================================================ FILE: packages/svelte/test-app/svelte-html.d.ts ================================================ declare module 'svelte/elements' { export interface HTMLAttributes { 'scroll-region'?: boolean | '' | undefined } } export {} ================================================ FILE: packages/svelte/test-app/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors // for more information about preprocessors preprocess: vitePreprocess(), } export default config ================================================ FILE: packages/svelte/test-app/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "module": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "baseUrl": ".", "paths": { "@/*": ["./*"] } }, "include": ["**/*.ts", "**/*.d.ts", "**/*.svelte"] } ================================================ FILE: packages/svelte/test-app/types.d.ts ================================================ import type { Method, Page, PageProps, Router } from '@inertiajs/core' declare global { interface Window { testing: { Inertia: Router } initialPage: Page _inertia_request_dump: { headers: Record method: Method form: Record | undefined files: MulterFile[] | object query: Record url: string $page: Page } _inertia_page_key: string | undefined _inertia_props: PageProps _inertia_layout_id: number | string | undefined _inertia_site_layout_props: PageProps _inertia_nested_layout_id: number | string | undefined _inertia_nested_layout_props: PageProps _inertia_page_props: PageProps _plugin_global_props: object } interface ImportMeta { readonly glob: (pattern: string, options: { eager: true }) => Record } } export type MulterFile = Express.Multer.File ================================================ FILE: packages/svelte/test-app/vite-env.d.ts ================================================ /// /// ================================================ FILE: packages/svelte/test-app/vite.config.js ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' const isSSR = process.argv.includes('--ssr') export default defineConfig({ build: { sourcemap: 'inline', emptyOutDir: !isSSR, }, resolve: { alias: { '@': __dirname, }, }, plugins: [ svelte({ compilerOptions: { hydratable: true, }, }), ], }) ================================================ FILE: packages/svelte/tsconfig.json ================================================ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "declaration": true, "declarationDir": "dist", "module": "ES2020", "moduleResolution": "Node", "allowSyntheticDefaultImports": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, "removeComments": true } } ================================================ FILE: packages/svelte/vite-with-deps.config.js ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' export default defineConfig({ plugins: [svelte()], build: { minify: false, lib: { entry: './src/index.ts', formats: ['es'], fileName: 'index', }, rollupOptions: { // Only externalize Svelte (peer dependency) - bundle everything else external: ['svelte', 'svelte/internal', 'svelte/store'], }, }, }) ================================================ FILE: packages/svelte/vite.config.js ================================================ import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' export default defineConfig({ build: { minify: false, }, plugins: [sveltekit()], }) ================================================ FILE: packages/vue3/.gitignore ================================================ dist types node_modules package-lock.json yarn.lock /test-results/ /blob-report/ /playwright/.cache/ ================================================ FILE: packages/vue3/LICENSE ================================================ MIT License Copyright (c) Jonathan Reinink Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/vue3/build.js ================================================ #!/usr/bin/env node import esbuild from 'esbuild' import { nodeExternalsPlugin } from 'esbuild-node-externals' import { readFileSync } from 'fs' const watch = process.argv.slice(1).includes('--watch') const withDeps = process.argv.slice(1).includes('--with-deps') // For regular builds, externalize all dependencies to keep the bundle size small (using nodeExternalsPlugin). // For builds with dependencies, only externalize peer dependencies and bundle everything // else so we can check ES2020 compatibility without checking framework code. let externalDependencies = undefined if (withDeps) { const pkg = JSON.parse(readFileSync('./package.json', 'utf8')) externalDependencies = Object.keys(pkg.peerDependencies || {}) } const config = { bundle: true, minify: false, sourcemap: withDeps ? false : true, target: 'es2020', external: externalDependencies, plugins: [ ...(withDeps ? [] : [nodeExternalsPlugin()]), { name: 'inertia', setup(build) { let count = 0 build.onEnd((result) => { if (count++ !== 0) { console.log(`Rebuilding ${build.initialOptions.entryPoints} (${build.initialOptions.format})…`) } }) }, }, ], } const builds = [ { entryPoints: ['src/index.ts'], format: 'esm', outfile: 'dist/index.esm.js', platform: 'browser' }, { entryPoints: ['src/index.ts'], format: 'cjs', outfile: 'dist/index.js', platform: 'browser' }, { entryPoints: ['src/server.ts'], format: 'esm', outfile: 'dist/server.esm.js', platform: 'node' }, { entryPoints: ['src/server.ts'], format: 'cjs', outfile: 'dist/server.js', platform: 'node' }, ] builds.forEach(async (build) => { const context = await esbuild.context({ ...config, ...build }) if (watch) { console.log(`Watching ${build.entryPoints} (${build.format})…`) await context.watch() } else { await context.rebuild() context.dispose() console.log(`Built ${build.entryPoints} (${build.format}) ${withDeps ? '(with-deps)' : ''}…`) } }) ================================================ FILE: packages/vue3/package.json ================================================ { "name": "@inertiajs/vue3", "version": "2.3.18", "license": "MIT", "description": "The Vue 3 adapter for Inertia.js", "contributors": [ "Jonathan Reinink " ], "homepage": "https://inertiajs.com/", "repository": { "type": "git", "url": "https://github.com/inertiajs/inertia.git", "directory": "packages/vue3" }, "bugs": { "url": "https://github.com/inertiajs/inertia/issues" }, "files": [ "dist", "types", "resources" ], "type": "module", "main": "dist/index.js", "types": "types/index.d.ts", "exports": { ".": { "types": "./types/index.d.ts", "import": "./dist/index.esm.js", "require": "./dist/index.js" }, "./server": { "types": "./types/server.d.ts", "import": "./dist/server.esm.js", "require": "./dist/server.js" } }, "typesVersions": { "*": { "server": [ "types/server.d.ts" ] } }, "scripts": { "build": "pnpm clean && ./build.js && tsc", "build:with-deps": "./build.js --with-deps", "clean": "rm -rf types && rm -rf dist", "dev": "pnpx concurrently -c \"#ffcf00,#3178c6\" \"pnpm dev:build\" \"pnpm dev:types\" --names build,types", "dev:build": "./build.js --watch", "dev:types": "tsc --watch --preserveWatchOutput", "es2020-check": "pnpm build:with-deps && es-check es2020 \"dist/index.esm.js\" --checkFeatures --module --noCache --verbose" }, "devDependencies": { "axios": "^1.13.5", "es-check": "^9.6.1", "esbuild": "^0.27.3", "esbuild-node-externals": "^1.20.1", "typescript": "^5.9.3", "vue": "^3.5.29" }, "peerDependencies": { "vue": "^3.0.0" }, "dependencies": { "@inertiajs/core": "workspace:*", "@types/lodash-es": "^4.17.12", "laravel-precognition": "^1.0.2", "lodash-es": "^4.17.23" } } ================================================ FILE: packages/vue3/readme.md ================================================ # Inertia.js Vue 3 Adapter Visit [inertiajs.com](https://inertiajs.com/) to learn more. ================================================ FILE: packages/vue3/resources/boost/guidelines/core.blade.php ================================================ # Inertia + Vue Vue components must have a single root element. - IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns. ================================================ FILE: packages/vue3/resources/boost/skills/inertia-vue-development/SKILL.blade.php ================================================ --- name: inertia-vue-development description: "Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using ,
, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation." license: MIT metadata: author: laravel --- @php /** @var \Laravel\Boost\Install\GuidelineAssist $assist */ @endphp # Inertia Vue Development ## When to Apply Activate this skill when: - Creating or modifying Vue page components for Inertia - Working with forms in Vue (using `` or `useForm`) - Implementing client-side navigation with `` or `router` - Using v2 features: deferred props, prefetching, WhenVisible, InfiniteScroll, once props, flash data, or polling - Building Vue-specific features with the Inertia protocol ## Documentation Use `search-docs` for detailed Inertia v2 Vue patterns and documentation. ## Basic Usage ### Page Components Location Vue page components should be placed in the `{{ $assist->inertia()->pagesDirectory() }}` directory. ### Page Component Structure @verbatim @boostsnippet("Basic Vue Page Component", "vue") @endboostsnippet @endverbatim ## Client-Side Navigation ### Basic Link Component Use `` for client-side navigation instead of traditional `` tags: @boostsnippet("Inertia Vue Navigation", "vue") @endboostsnippet ### Link with Method @boostsnippet("Link with POST Method", "vue") @endboostsnippet ### Prefetching Prefetch pages to improve perceived performance: @boostsnippet("Prefetch on Hover", "vue") @endboostsnippet ### Programmatic Navigation @boostsnippet("Router Visit", "vue") @endboostsnippet ## Form Handling @if($assist->inertia()->hasFormComponent()) ### Form Component (Recommended) The recommended way to build forms is with the `` component: @verbatim @boostsnippet("Form Component Example", "vue") @endboostsnippet @endverbatim ### Form Component With All Props @verbatim @boostsnippet("Form Component Full Example", "vue") @endboostsnippet @endverbatim @if($assist->inertia()->hasFormComponentResets()) ### Form Component Reset Props The `` component supports automatic resetting: - `resetOnError` - Reset form data when the request fails - `resetOnSuccess` - Reset form data when the request succeeds - `setDefaultsOnSuccess` - Update default values on success Use the `search-docs` tool with a query of `form component resetting` for detailed guidance. @verbatim @boostsnippet("Form with Reset Props", "vue") @endboostsnippet @endverbatim @else Note: This version of Inertia does not support `resetOnError`, `resetOnSuccess`, or `setDefaultsOnSuccess` on the `` component. Using these props will cause errors. Upgrade to Inertia v2.2.0+ to use these features. @endif Forms can also be built using the `useForm` composable for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance. @endif ### `useForm` Composable @if($assist->inertia()->hasFormComponent() === false) For Inertia v2.0.x: Build forms using the `useForm` composable as the `` component is not available until v2.1.0+. @else For more programmatic control or to follow existing conventions, use the `useForm` composable: @endif @verbatim @boostsnippet("useForm Composable Example", "vue") @endboostsnippet @endverbatim ## Inertia v2 Features ### Deferred Props Use deferred props to load data after initial page render: @verbatim @boostsnippet("Deferred Props with Empty State", "vue") @endboostsnippet @endverbatim ### Polling Automatically refresh data at intervals: @verbatim @boostsnippet("Polling Example", "vue") @endboostsnippet @endverbatim ### WhenVisible Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold: @verbatim @boostsnippet("WhenVisible Example", "vue") @endboostsnippet @endverbatim ## Server-Side Patterns Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines. ## Common Pitfalls - Using traditional `` links instead of Inertia's `` component (breaks SPA behavior) - Forgetting that Vue components must have a single root element - Forgetting to add loading states (skeleton screens) when using deferred props - Not handling the `undefined` state of deferred props before data loads - Using `` without preventing default submission (use `` component or `@submit.prevent`) - Forgetting to check if `` component is available in your Inertia version ================================================ FILE: packages/vue3/src/app.ts ================================================ import { createHeadManager, HeadManager, HeadManagerOnUpdateCallback, HeadManagerTitleCallback, Page, PageProps, router, SharedPageProps, } from '@inertiajs/core' import { computed, DefineComponent, defineComponent, h, markRaw, Plugin, PropType, reactive, ref, shallowRef, } from 'vue' import remember from './remember' import { VuePageHandlerArgs } from './types' import useForm from './useForm' export interface InertiaAppProps { initialPage: Page initialComponent?: DefineComponent resolveComponent?: (name: string) => DefineComponent | Promise titleCallback?: HeadManagerTitleCallback onHeadUpdate?: HeadManagerOnUpdateCallback } export type InertiaApp = DefineComponent const component = ref(undefined) const page = ref() const layout = shallowRef(null) const key = ref(undefined) let headManager: HeadManager const App: InertiaApp = defineComponent({ name: 'Inertia', props: { initialPage: { type: Object as PropType, required: true, }, initialComponent: { type: Object as PropType, required: false, }, resolveComponent: { type: Function as PropType<(name: string) => DefineComponent | Promise>, required: false, }, titleCallback: { type: Function as PropType, required: false, default: (title: string) => title, }, onHeadUpdate: { type: Function as PropType, required: false, default: () => () => {}, }, }, setup({ initialPage, initialComponent, resolveComponent, titleCallback, onHeadUpdate }: InertiaAppProps) { component.value = initialComponent ? markRaw(initialComponent) : undefined page.value = { ...initialPage, flash: initialPage.flash ?? {} } key.value = undefined const isServer = typeof window === 'undefined' headManager = createHeadManager(isServer, titleCallback || ((title: string) => title), onHeadUpdate || (() => {})) if (!isServer) { router.init({ initialPage, resolveComponent: resolveComponent!, swapComponent: async (options: VuePageHandlerArgs) => { component.value = markRaw(options.component) page.value = options.page key.value = options.preserveState ? key.value : Date.now() }, onFlash: (flash) => { page.value = { ...page.value!, flash } }, }) router.on('navigate', () => headManager.forceUpdate()) } return () => { if (component.value) { component.value.inheritAttrs = !!component.value.inheritAttrs const child = h(component.value, { ...page.value!.props, key: key.value, }) if (layout.value) { component.value.layout = layout.value layout.value = null } if (component.value.layout) { if (typeof component.value.layout === 'function') { return component.value.layout(h, child) } return (Array.isArray(component.value.layout) ? component.value.layout : [component.value.layout]) .concat(child) .reverse() .reduce((child, layout) => { layout.inheritAttrs = !!layout.inheritAttrs return h(layout, { ...page.value!.props }, () => child) }) } return child } } }, }) export default App export const plugin: Plugin = { install(app) { router.form = useForm Object.defineProperty(app.config.globalProperties, '$inertia', { get: () => router }) Object.defineProperty(app.config.globalProperties, '$page', { get: () => page.value }) Object.defineProperty(app.config.globalProperties, '$headManager', { get: () => headManager }) app.mixin(remember) }, } export function usePage(): Page { return reactive({ props: computed(() => page.value?.props), url: computed(() => page.value?.url), component: computed(() => page.value?.component), version: computed(() => page.value?.version), clearHistory: computed(() => page.value?.clearHistory), deferredProps: computed(() => page.value?.deferredProps), mergeProps: computed(() => page.value?.mergeProps), prependProps: computed(() => page.value?.prependProps), deepMergeProps: computed(() => page.value?.deepMergeProps), matchPropsOn: computed(() => page.value?.matchPropsOn), rememberedState: computed(() => page.value?.rememberedState), encryptHistory: computed(() => page.value?.encryptHistory), flash: computed(() => page.value?.flash), }) as Page } ================================================ FILE: packages/vue3/src/createInertiaApp.ts ================================================ import { CreateInertiaAppOptionsForCSR, CreateInertiaAppOptionsForSSR, getInitialPageFromDOM, InertiaAppResponse, InertiaAppSSRResponse, Page, PageProps, router, setupProgress, SharedPageProps, } from '@inertiajs/core' import { createSSRApp, DefineComponent, h, Plugin, App as VueApp } from 'vue' import App, { InertiaApp, InertiaAppProps, plugin } from './app' import { config } from './index' import { VueInertiaAppConfig } from './types' type ComponentResolver = (name: string) => DefineComponent | Promise | { default: DefineComponent } type SetupOptions = { el: ElementType App: InertiaApp props: InertiaAppProps plugin: Plugin } type InertiaAppOptionsForCSR = CreateInertiaAppOptionsForCSR< SharedProps, ComponentResolver, SetupOptions, void, VueInertiaAppConfig > type InertiaAppOptionsForSSR = CreateInertiaAppOptionsForSSR< SharedProps, ComponentResolver, SetupOptions, VueApp, VueInertiaAppConfig > & { render: (app: VueApp) => Promise } export default async function createInertiaApp( options: InertiaAppOptionsForCSR, ): Promise export default async function createInertiaApp( options: InertiaAppOptionsForSSR, ): Promise export default async function createInertiaApp({ id = 'app', resolve, setup, title, progress = {}, page, render, defaults = {}, }: InertiaAppOptionsForCSR | InertiaAppOptionsForSSR): InertiaAppResponse { config.replace(defaults) const isServer = typeof window === 'undefined' const useScriptElementForInitialPage = config.get('future.useScriptElementForInitialPage') const initialPage = page || getInitialPageFromDOM>(id, useScriptElementForInitialPage)! const resolveComponent = (name: string) => Promise.resolve(resolve(name)).then((module) => module.default || module) let head: string[] = [] const vueApp = await Promise.all([ resolveComponent(initialPage.component), router.decryptHistory().catch(() => {}), ]).then(([initialComponent]) => { const props = { initialPage, initialComponent, resolveComponent, titleCallback: title, } if (isServer) { const ssrSetup = setup as (options: SetupOptions) => VueApp return ssrSetup({ el: null, App, props: { ...props, onHeadUpdate: (elements: string[]) => (head = elements) }, plugin, }) } const csrSetup = setup as (options: SetupOptions) => void return csrSetup({ el: document.getElementById(id)!, App, props, plugin, }) }) if (!isServer && progress) { setupProgress(progress) } if (isServer && render) { const element = () => { if (!useScriptElementForInitialPage) { return h('div', { id, 'data-page': JSON.stringify(initialPage), innerHTML: vueApp ? render(vueApp) : '', }) } return [ h('script', { 'data-page': id, type: 'application/json', innerHTML: JSON.stringify(initialPage).replace(/\//g, '\\/'), }), h('div', { id, innerHTML: vueApp ? render(vueApp) : '', }), ] } const body = await render( createSSRApp({ render: () => element(), }), ) return { head, body } } } ================================================ FILE: packages/vue3/src/deferred.ts ================================================ import { defineComponent } from 'vue' export default defineComponent({ name: 'Deferred', props: { data: { type: [String, Array], required: true, }, }, render() { const keys = (Array.isArray(this.$props.data) ? this.$props.data : [this.$props.data]) as string[] if (!this.$slots.fallback) { throw new Error('`` requires a ` ================================================ FILE: playgrounds/vue3/resources/js/Pages/Article.vue ================================================ ================================================ FILE: playgrounds/vue3/resources/js/Pages/Async.vue ================================================ ================================================ FILE: playgrounds/vue3/resources/js/Pages/Chat.vue ================================================