@@ -11,17 +11,26 @@ import { BaseSerializer } from '@adonisjs/core/transformers'
1111import { type AsyncOrSync } from '@adonisjs/core/types/common'
1212import { type JSONDataTypes } from '@adonisjs/core/types/transformers'
1313
14- import { ALWAYS_PROP , DEEP_MERGE , DEFERRED_PROP , OPTIONAL_PROP , TO_BE_MERGED } from './symbols.ts'
14+ import { type ContainerResolver } from '@adonisjs/core/container'
15+ import {
16+ ALWAYS_PROP ,
17+ DEEP_MERGE ,
18+ DEFERRED_PROP ,
19+ OPTIONAL_PROP ,
20+ SCROLL_PROP ,
21+ TO_BE_MERGED ,
22+ } from './symbols.ts'
1523import {
16- type DeferProp ,
17- type PageProps ,
1824 type AlwaysProp ,
19- type OptionalProp ,
20- type MergeableProp ,
2125 type ComponentProps ,
26+ type DeferProp ,
27+ type MergeableProp ,
28+ type OptionalProp ,
29+ type PageProps ,
30+ type ScrollMetadata ,
31+ type ScrollProp ,
2232 type UnPackedPageProps ,
2333} from './types.ts'
24- import { type ContainerResolver } from '@adonisjs/core/container'
2534
2635class InertiaSerializer extends BaseSerializer {
2736 wrap : undefined = undefined
@@ -215,6 +224,39 @@ export function deepMerge<T extends UnPackedPageProps | DeferProp<UnPackedPagePr
215224 }
216225}
217226
227+ /**
228+ * Wraps a paginated value for infinite scrolling.
229+ *
230+ * Pagination metadata is automatically extracted and emitted in scrollProps.
231+ * Merge/prepend behavior is wired based on the X-Inertia-Infinite-Scroll-Merge-Intent
232+ * request header: "append" adds propKey.data to mergeProps, "prepend" to prependProps,
233+ * absent means initial load and no merge is declared.
234+ *
235+ * @example
236+ * ```js
237+ * { posts: inertia.scroll(() => PostTransformer.paginate(posts.all(), posts.getMeta())) }
238+ * ```
239+ *
240+ * @example Custom page query parameter
241+ * ```js
242+ * {
243+ * users: inertia.scroll(() => UserTransformer.paginate(users.all(), users.getMeta()), { pageName: 'usersPage' }),
244+ * orders: inertia.scroll(() => OrderTransformer.paginate(orders.all(), orders.getMeta()), { pageName: 'ordersPage' }),
245+ * }
246+ * ```
247+ */
248+ export function scroll < T extends UnPackedPageProps > (
249+ value : T | ( ( ) => AsyncOrSync < T > ) ,
250+ options : { pageName ?: string ; wrapper ?: string } = { }
251+ ) : ScrollProp < T > {
252+ return {
253+ value,
254+ pageName : options . pageName ?? 'page' ,
255+ wrapper : options . wrapper ?? 'data' ,
256+ [ SCROLL_PROP ] : true ,
257+ }
258+ }
259+
218260/**
219261 * Type guard that checks if a prop value is a deferred prop.
220262 *
@@ -315,6 +357,50 @@ export function isOptionalProp<T extends UnPackedPageProps>(
315357 return OPTIONAL_PROP in propValue
316358}
317359
360+ /**
361+ * Type guard that checks if a prop value is a scroll prop.
362+ *
363+ * Scroll props contain the SCROLL_PROP symbol and wrap a paginated value
364+ * for the infinite scroll.
365+ *
366+ * @param propValue - The object to check for scroll prop characteristics
367+ * @returns True if the prop value is a scroll prop
368+ *
369+ * @example
370+ * ```js
371+ * const prop = scroll(() => PostTransformer.paginate(posts.all(), posts.getMeta()))
372+ *
373+ * if (isScrollProp(prop)) {
374+ * // prop is now typed as ScrollProp<T>
375+ * const result = await prop.compute()
376+ * }
377+ * ```
378+ */
379+ export function isScrollProp < T extends UnPackedPageProps > (
380+ propValue : Object
381+ ) : propValue is ScrollProp < T > {
382+ return SCROLL_PROP in propValue
383+ }
384+
385+ /**
386+ * Extracts scroll metadata from a serialized paginator value.
387+ * Reads from the "metadata" key produced by AdonisJS transformers
388+ * rather than inspecting the raw paginator instance.
389+ */
390+ function resolveScrollMetadata ( serialized : any , pageName : string , wrapper : string ) : ScrollMetadata {
391+ const meta = serialized ?. metadata ?? { }
392+ const currentPage : number | null = meta . currentPage ?? null
393+ const lastPage : number | null = meta . lastPage ?? null
394+ return {
395+ pageName,
396+ wrapper,
397+ currentPage,
398+ nextPage :
399+ currentPage !== null && lastPage !== null && currentPage < lastPage ? currentPage + 1 : null ,
400+ previousPage : currentPage !== null && currentPage > 1 ? currentPage - 1 : null ,
401+ }
402+ }
403+
318404/**
319405 * Helper function to unpack prop values using the transformer serialize function.
320406 *
@@ -361,6 +447,7 @@ export async function buildStandardVisitProps(
361447 const deepMergeProps : string [ ] = [ ]
362448 const newProps : ComponentProps = { }
363449 const deferredProps : { [ group : string ] : string [ ] } = { }
450+ const scrollProps : { [ key : string ] : ScrollMetadata } = { }
364451 const unpackedValues : Array < {
365452 key : string
366453 value : UnPackedPageProps | ( ( ) => AsyncOrSync < UnPackedPageProps > )
@@ -393,6 +480,15 @@ export async function buildStandardVisitProps(
393480 continue
394481 }
395482
483+ /**
484+ * Scroll props are serialized like standard props. The pageName is tracked
485+ * so that scrollProps metadata can be derived after serialization.
486+ */
487+ if ( isScrollProp ( value ) ) {
488+ unpackedValues . push ( { key, value : value . value } )
489+ continue
490+ }
491+
396492 /**
397493 * Inform the client about the mergeable prop and use its
398494 * value
@@ -464,11 +560,18 @@ export async function buildStandardVisitProps(
464560 } )
465561 )
466562
563+ for ( const [ key , value ] of Object . entries ( pageProps ) ) {
564+ if ( isObject ( value ) && isScrollProp ( value ) ) {
565+ scrollProps [ key ] = resolveScrollMetadata ( newProps [ key ] , value . pageName , value . wrapper )
566+ }
567+ }
568+
467569 return {
468570 props : newProps ,
469571 mergeProps,
470572 deepMergeProps,
471573 deferredProps,
574+ scrollProps,
472575 }
473576}
474577
@@ -503,6 +606,7 @@ export async function buildPartialRequestProps(
503606) {
504607 const mergeProps : string [ ] = [ ]
505608 const deepMergeProps : string [ ] = [ ]
609+ const scrollProps : { [ key : string ] : ScrollMetadata } = { }
506610 const newProps : ComponentProps = { }
507611 const unpackedValues : Array < {
508612 key : string
@@ -543,6 +647,14 @@ export async function buildPartialRequestProps(
543647 continue
544648 }
545649
650+ /**
651+ * Unpack scroll prop value and track pageName for metadata derivation
652+ */
653+ if ( isScrollProp ( value ) ) {
654+ unpackedValues . push ( { key, value : value . value } )
655+ continue
656+ }
657+
546658 /**
547659 * Inform the client about the mergeable prop
548660 */
@@ -605,10 +717,17 @@ export async function buildPartialRequestProps(
605717 } )
606718 )
607719
720+ for ( const [ key , value ] of Object . entries ( pageProps ) ) {
721+ if ( isObject ( value ) && isScrollProp ( value ) ) {
722+ scrollProps [ key ] = resolveScrollMetadata ( newProps [ key ] , value . pageName , value . wrapper )
723+ }
724+ }
725+
608726 return {
609727 props : newProps ,
610728 mergeProps,
611729 deepMergeProps,
612730 deferredProps : { } ,
731+ scrollProps,
613732 }
614733}
0 commit comments