1
1
import { AxiosResponse } from 'axios'
2
+ import { get , set } from 'lodash-es'
2
3
import { router } from '.'
3
4
import { fireErrorEvent , fireInvalidEvent , firePrefetchedEvent , fireSuccessEvent } from './events'
4
5
import { history } from './history'
@@ -221,50 +222,60 @@ export class Response {
221
222
return
222
223
}
223
224
224
- const propsToMerge = pageResponse . mergeProps || [ ]
225
+ const propsToAppend = pageResponse . mergeProps || [ ]
226
+ const propsToPrepend = pageResponse . prependProps || [ ]
225
227
const propsToDeepMerge = pageResponse . deepMergeProps || [ ]
226
228
const matchPropsOn = pageResponse . matchPropsOn || [ ]
227
229
228
- propsToMerge . forEach ( ( prop ) => {
229
- const incomingProp = pageResponse . props [ prop ]
230
+ const mergeProp = ( prop : string , shouldAppend : boolean ) => {
231
+ const currentProp = get ( currentPage . get ( ) . props , prop )
232
+ const incomingProp = get ( pageResponse . props , prop )
230
233
231
234
if ( Array . isArray ( incomingProp ) ) {
232
- pageResponse . props [ prop ] = this . mergeOrMatchItems (
233
- ( currentPage . get ( ) . props [ prop ] || [ ] ) as any [ ] ,
235
+ const newArray = this . mergeOrMatchItems (
236
+ ( currentProp || [ ] ) as any [ ] ,
234
237
incomingProp ,
235
238
prop ,
236
239
matchPropsOn ,
240
+ shouldAppend ,
237
241
)
242
+
243
+ set ( pageResponse . props , prop , newArray )
238
244
} else if ( typeof incomingProp === 'object' && incomingProp !== null ) {
239
- pageResponse . props [ prop ] = {
240
- ...( ( currentPage . get ( ) . props [ prop ] || [ ] ) as Record < string , any > ) ,
245
+ const newObject = {
246
+ ...( currentProp || { } ) ,
241
247
...incomingProp ,
242
248
}
249
+
250
+ set ( pageResponse . props , prop , newObject )
243
251
}
244
- } )
252
+ }
253
+
254
+ propsToAppend . forEach ( ( prop ) => mergeProp ( prop , true ) )
255
+ propsToPrepend . forEach ( ( prop ) => mergeProp ( prop , false ) )
245
256
246
257
propsToDeepMerge . forEach ( ( prop ) => {
247
- const incomingProp = pageResponse . props [ prop ]
248
258
const currentProp = currentPage . get ( ) . props [ prop ]
259
+ const incomingProp = pageResponse . props [ prop ]
249
260
250
261
// Function to recursively merge objects and arrays
251
- const deepMerge = ( target : any , source : any , currentKey : string ) => {
262
+ const deepMerge = ( target : any , source : any , matchProp : string ) => {
252
263
if ( Array . isArray ( source ) ) {
253
- return this . mergeOrMatchItems ( target , source , currentKey , matchPropsOn )
264
+ return this . mergeOrMatchItems ( target , source , matchProp , matchPropsOn )
254
265
}
255
266
256
267
if ( typeof source === 'object' && source !== null ) {
257
268
// Merge objects by iterating over keys
258
269
return Object . keys ( source ) . reduce (
259
270
( acc , key ) => {
260
- acc [ key ] = deepMerge ( target ? target [ key ] : undefined , source [ key ] , `${ currentKey } .${ key } ` )
271
+ acc [ key ] = deepMerge ( target ? target [ key ] : undefined , source [ key ] , `${ matchProp } .${ key } ` )
261
272
return acc
262
273
} ,
263
274
{ ...target } ,
264
275
)
265
276
}
266
277
267
- // f the source is neither an array nor an object, simply return the it
278
+ // If the source is neither an array nor an object, simply return the it
268
279
return source
269
280
}
270
281
@@ -275,45 +286,94 @@ export class Response {
275
286
pageResponse . props = { ...currentPage . get ( ) . props , ...pageResponse . props }
276
287
}
277
288
278
- protected mergeOrMatchItems ( target : any [ ] , source : any [ ] , currentKey : string , matchPropsOn : string [ ] ) {
279
- // Determine if there's a specific key to match items.
280
- // E.g.: matchPropsOn = ['posts.data.id'] and currentKey = 'posts.data' will match.
281
- const matchOn = matchPropsOn . find ( ( key ) => {
282
- const path = key . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' )
283
- return path === currentKey
289
+ protected mergeOrMatchItems (
290
+ existingItems : any [ ] ,
291
+ newItems : any [ ] ,
292
+ matchProp : string ,
293
+ matchPropsOn : string [ ] ,
294
+ shouldAppend = true ,
295
+ ) {
296
+ const items = Array . isArray ( existingItems ) ? existingItems : [ ]
297
+
298
+ // Find the matching key for this specific property path
299
+ const matchingKey = matchPropsOn . find ( ( key ) => {
300
+ const keyPath = key . split ( '.' ) . slice ( 0 , - 1 ) . join ( '.' )
301
+
302
+ return keyPath === matchProp
284
303
} )
285
304
286
- if ( ! matchOn ) {
287
- // No key found to match on, just concatenate the arrays
288
- return [ ...( Array . isArray ( target ) ? target : [ ] ) , ...source ]
305
+ // If no matching key is configured, simply concatenate the arrays
306
+ if ( ! matchingKey ) {
307
+ return shouldAppend ? [ ...items , ... newItems ] : [ ... newItems , ...items ]
289
308
}
290
309
291
- // Extract the unique property name to match items (e.g., 'id' from 'posts.data.id').
292
- const uniqueProperty = matchOn . split ( '.' ) . pop ( ) || ''
293
- const targetArray = Array . isArray ( target ) ? target : [ ]
294
- const map = new Map < any , any > ( )
310
+ // Extract the property name we'll use to match items (e.g., 'id' from 'users.data.id')
311
+ const uniqueProperty = matchingKey . split ( '.' ) . pop ( ) || ''
295
312
296
- // Populate the map with items from the target array, using the unique property as the key.
297
- // If an item doesn't have the unique property or isn't an object, a unique Symbol is used as the key.
298
- targetArray . forEach ( ( item ) => {
299
- if ( item && typeof item === 'object' && uniqueProperty in item ) {
300
- map . set ( item [ uniqueProperty ] , item )
301
- } else {
302
- map . set ( Symbol ( ) , item )
313
+ // Create a map of new items by their unique property lookups
314
+ const newItemsMap = new Map ( )
315
+
316
+ newItems . forEach ( ( item ) => {
317
+ if ( this . hasUniqueProperty ( item , uniqueProperty ) ) {
318
+ newItemsMap . set ( item [ uniqueProperty ] , item )
303
319
}
304
320
} )
305
321
306
- // Iterate through the source array. If an item's unique property matches an existing key in the map,
307
- // update the item. Otherwise, add the new item to the map.
308
- source . forEach ( ( item ) => {
309
- if ( item && typeof item === 'object' && uniqueProperty in item ) {
310
- map . set ( item [ uniqueProperty ] , item )
311
- } else {
312
- map . set ( Symbol ( ) , item )
322
+ return shouldAppend
323
+ ? this . appendWithMatching ( items , newItems , newItemsMap , uniqueProperty )
324
+ : this . prependWithMatching ( items , newItems , newItemsMap , uniqueProperty )
325
+ }
326
+
327
+ protected appendWithMatching (
328
+ existingItems : any [ ] ,
329
+ newItems : any [ ] ,
330
+ newItemsMap : Map < any , any > ,
331
+ uniqueProperty : string ,
332
+ ) : any [ ] {
333
+ // Update existing items with new values, keep non-matching items
334
+ const updatedExisting = existingItems . map ( ( item ) => {
335
+ if ( this . hasUniqueProperty ( item , uniqueProperty ) && newItemsMap . has ( item [ uniqueProperty ] ) ) {
336
+ return newItemsMap . get ( item [ uniqueProperty ] )
313
337
}
338
+
339
+ return item
340
+ } )
341
+
342
+ // Filter new items to only include those not already in existing items
343
+ const newItemsToAdd = newItems . filter ( ( item ) => {
344
+ if ( ! this . hasUniqueProperty ( item , uniqueProperty ) ) {
345
+ return true // Always add items without unique property
346
+ }
347
+
348
+ return ! existingItems . some (
349
+ ( existing ) =>
350
+ this . hasUniqueProperty ( existing , uniqueProperty ) && existing [ uniqueProperty ] === item [ uniqueProperty ] ,
351
+ )
352
+ } )
353
+
354
+ return [ ...updatedExisting , ...newItemsToAdd ]
355
+ }
356
+
357
+ protected prependWithMatching (
358
+ existingItems : any [ ] ,
359
+ newItems : any [ ] ,
360
+ newItemsMap : Map < any , any > ,
361
+ uniqueProperty : string ,
362
+ ) : any [ ] {
363
+ // Filter existing items, keeping only those not being updated
364
+ const untouchedExisting = existingItems . filter ( ( item ) => {
365
+ if ( this . hasUniqueProperty ( item , uniqueProperty ) ) {
366
+ return ! newItemsMap . has ( item [ uniqueProperty ] )
367
+ }
368
+
369
+ return true
314
370
} )
315
371
316
- return Array . from ( map . values ( ) )
372
+ return [ ...newItems , ...untouchedExisting ]
373
+ }
374
+
375
+ protected hasUniqueProperty ( item : any , property : string ) : boolean {
376
+ return item && typeof item === 'object' && property in item
317
377
}
318
378
319
379
protected async setRememberedState ( pageResponse : Page ) : Promise < void > {
0 commit comments