Skip to content

Commit e2ab960

Browse files
authored
More fine-grained control for Inertia::merge() and a new Inertia::scroll() (#774)
1 parent 43c44df commit e2ab960

17 files changed

+1145
-24
lines changed

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
"suggest": {
4646
"ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command."
4747
},
48+
"scripts": {
49+
"test": "phpunit",
50+
"lint": "pint",
51+
"analyze": "phpstan analyze --memory-limit=256M"
52+
},
4853
"extra": {
4954
"laravel": {
5055
"providers": [

src/Mergeable.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,32 @@ public function shouldDeepMerge();
3131
* @return array<int, string>
3232
*/
3333
public function matchesOn();
34+
35+
/**
36+
* Determine if the property should be appended at the root level.
37+
*
38+
* @return bool
39+
*/
40+
public function appendsAtRoot();
41+
42+
/**
43+
* Determine if the property should be prepended at the root level.
44+
*
45+
* @return bool
46+
*/
47+
public function prependsAtRoot();
48+
49+
/**
50+
* Get the paths to append when merging.
51+
*
52+
* @return array<int, string>
53+
*/
54+
public function appendsAtPaths(): array;
55+
56+
/**
57+
* Get the paths to prepend when merging.
58+
*
59+
* @return array<int, string>
60+
*/
61+
public function prependsAtPaths(): array;
3462
}

src/MergesProps.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ trait MergesProps
2323
*/
2424
protected array $matchOn = [];
2525

26+
/**
27+
* Indicates if the property values should be appended or prepended.
28+
*/
29+
protected bool $append = true;
30+
31+
/**
32+
* The paths to append.
33+
*
34+
* @var array<int, string>
35+
*/
36+
protected array $appendsAtPaths = [];
37+
38+
/**
39+
* The paths to prepend.
40+
*
41+
* @var array<int, string>
42+
*/
43+
protected array $prependsAtPaths = [];
44+
2645
/**
2746
* Mark the property for merging.
2847
*/
@@ -80,4 +99,96 @@ public function matchesOn(): array
8099
{
81100
return $this->matchOn;
82101
}
102+
103+
/**
104+
* Determine if the property should be appended at the root level.
105+
*
106+
* @return bool
107+
*/
108+
public function appendsAtRoot()
109+
{
110+
return $this->append && $this->mergesAtRoot();
111+
}
112+
113+
/**
114+
* Determine if the property should be prepended at the root level.
115+
*
116+
* @return bool
117+
*/
118+
public function prependsAtRoot()
119+
{
120+
return ! $this->append && $this->mergesAtRoot();
121+
}
122+
123+
/**
124+
* Determine if the property merges at the root level.
125+
*/
126+
protected function mergesAtRoot(): bool
127+
{
128+
return count($this->appendsAtPaths) === 0 && count($this->prependsAtPaths) === 0;
129+
}
130+
131+
/**
132+
* Specify that the value should be appended, optionally providing a key to append and a property to match on.
133+
*
134+
* @param bool|string|array<array-key, string> $path
135+
*/
136+
public function append(bool|string|array $path = true, ?string $matchOn = null): static
137+
{
138+
match (true) {
139+
is_bool($path) => $this->append = $path,
140+
is_string($path) => $this->appendsAtPaths[] = $path,
141+
is_array($path) => collect($path)->each(
142+
fn ($value, $key) => is_numeric($key) ? $this->append($value) : $this->append($key, $value)
143+
),
144+
};
145+
146+
if (is_string($path) && $matchOn) {
147+
$this->matchOn([...$this->matchOn, "{$path}.{$matchOn}"]);
148+
}
149+
150+
return $this;
151+
}
152+
153+
/**
154+
* Specify that the value should be prepended, optionally providing a key to prepend and a property to match on.
155+
*
156+
* @param bool|string|array<array-key, string> $path
157+
*/
158+
public function prepend(bool|string|array $path = true, ?string $matchOn = null): static
159+
{
160+
match (true) {
161+
is_bool($path) => $this->append = ! $path,
162+
is_string($path) => $this->prependsAtPaths[] = $path,
163+
is_array($path) => collect($path)->each(
164+
fn ($value, $key) => is_numeric($key) ? $this->prepend($value) : $this->prepend($key, $value)
165+
),
166+
};
167+
168+
if (is_string($path) && $matchOn) {
169+
$this->matchOn([...$this->matchOn, "{$path}.{$matchOn}"]);
170+
}
171+
172+
return $this;
173+
}
174+
175+
/**
176+
* Get the paths to append.
177+
*
178+
* @return array<int, string>
179+
*/
180+
public function appendsAtPaths(): array
181+
{
182+
return $this->appendsAtPaths;
183+
}
184+
185+
/**
186+
* Get the paths to prepend.
187+
*
188+
* @return array<int, string>
189+
*/
190+
public function prependsAtPaths(): array
191+
{
192+
return $this->prependsAtPaths;
193+
}
83194
}

src/ProvidesScrollMetadata.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Inertia;
4+
5+
interface ProvidesScrollMetadata
6+
{
7+
/**
8+
* Get the page name parameter.
9+
*/
10+
public function getPageName(): string;
11+
12+
/**
13+
* Get the previous page identifier.
14+
*/
15+
public function getPreviousPage(): int|string|null;
16+
17+
/**
18+
* Get the next page identifier.
19+
*/
20+
public function getNextPage(): int|string|null;
21+
22+
/**
23+
* Get the current page identifier.
24+
*/
25+
public function getCurrentPage(): int|string|null;
26+
}

src/Response.php

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Http\JsonResponse;
1111
use Illuminate\Http\Request;
1212
use Illuminate\Support\Arr;
13+
use Illuminate\Support\Collection;
1314
use Illuminate\Support\Facades\App;
1415
use Illuminate\Support\Facades\Response as ResponseFactory;
1516
use Illuminate\Support\Str;
@@ -188,6 +189,7 @@ public function toResponse($request)
188189
$this->resolveMergeProps($request),
189190
$this->resolveDeferredProps($request),
190191
$this->resolveCacheDirections($request),
192+
$this->resolveScrollProps($request),
191193
);
192194

193195
if ($request->header(Header::INERTIA)) {
@@ -371,13 +373,18 @@ public function resolveAlways(array $props): array
371373
public function resolvePropertyInstances(array $props, Request $request, ?string $parentKey = null): array
372374
{
373375
foreach ($props as $key => $value) {
376+
if ($value instanceof ScrollProp) {
377+
$value->configureMergeIntent($request);
378+
}
379+
374380
$resolveViaApp = collect([
375381
Closure::class,
376382
LazyProp::class,
377383
OptionalProp::class,
378384
DeferProp::class,
379385
AlwaysProp::class,
380386
MergeProp::class,
387+
ScrollProp::class,
381388
])->first(fn ($class) => $value instanceof $class);
382389

383390
if ($resolveViaApp) {
@@ -439,45 +446,112 @@ public function resolveCacheDirections(Request $request): array
439446
}
440447

441448
/**
442-
* Resolve merge props configuration for client-side prop merging.
449+
* Get the props that should be considered for merging based on the request headers.
443450
*
444-
* @return array<string, mixed>
451+
* @return \Illuminate\Support\Collection<string, \Inertia\Mergeable>
445452
*/
446-
public function resolveMergeProps(Request $request): array
453+
protected function getMergePropsForRequest(Request $request): Collection
447454
{
448455
$resetProps = array_filter(explode(',', $request->header(Header::RESET, '')));
449456
$onlyProps = array_filter(explode(',', $request->header(Header::PARTIAL_ONLY, '')));
450457
$exceptProps = array_filter(explode(',', $request->header(Header::PARTIAL_EXCEPT, '')));
451458

452-
$mergeProps = collect($this->props)
459+
return collect($this->props)
453460
->filter(fn ($prop) => $prop instanceof Mergeable)
454-
->filter(fn ($prop) => $prop->shouldMerge())
455-
->reject(fn ($_, $key) => in_array($key, $resetProps))
456-
->filter(fn ($_, $key) => count($onlyProps) === 0 || in_array($key, $onlyProps))
457-
->reject(fn ($_, $key) => in_array($key, $exceptProps));
461+
->filter(fn (Mergeable $prop) => $prop->shouldMerge())
462+
->reject(fn ($_, string $key) => in_array($key, $resetProps))
463+
->filter(fn ($_, string $key) => count($onlyProps) === 0 || in_array($key, $onlyProps))
464+
->reject(fn ($_, string $key) => in_array($key, $exceptProps));
465+
}
466+
467+
/**
468+
* Resolve merge props configuration for client-side prop merging.
469+
*
470+
* @return array<string, mixed>
471+
*/
472+
public function resolveMergeProps(Request $request): array
473+
{
474+
$mergeProps = $this->getMergePropsForRequest($request);
458475

459-
$deepMergeProps = $mergeProps
460-
->filter(fn ($prop) => $prop->shouldDeepMerge())
461-
->keys();
476+
return array_filter([
477+
'mergeProps' => $this->resolveAppendMergeProps($mergeProps),
478+
'prependProps' => $this->resolvePrependMergeProps($mergeProps),
479+
'deepMergeProps' => $this->resolveDeepMergeProps($mergeProps),
480+
'matchPropsOn' => $this->resolveMergeMatchingKeys($mergeProps),
481+
], fn ($prop) => count($prop) > 0);
482+
}
462483

463-
$matchPropsOn = $mergeProps
484+
/**
485+
* Resolve props that should be appended during merging.
486+
*
487+
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
488+
* @return array<int, string>
489+
*/
490+
protected function resolveAppendMergeProps(Collection $mergeProps): array
491+
{
492+
[$rootAppendProps, $nestedAppendProps] = $mergeProps
493+
->reject(fn (Mergeable $prop) => $prop->shouldDeepMerge())
494+
->partition(fn (Mergeable $prop) => $prop->appendsAtRoot());
495+
496+
return $nestedAppendProps
497+
->flatMap(fn (Mergeable $prop, string $key) => collect($prop->appendsAtPaths())->map(fn ($path) => $key.'.'.$path))
498+
->merge($rootAppendProps->keys())
499+
->unique()
500+
->values()
501+
->toArray();
502+
}
503+
504+
/**
505+
* Resolve props that should be prepended during merging.
506+
*
507+
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
508+
* @return array<int, string>
509+
*/
510+
protected function resolvePrependMergeProps(Collection $mergeProps): array
511+
{
512+
[$rootPrependProps, $nestedPrependProps] = $mergeProps
513+
->reject(fn (Mergeable $prop) => $prop->shouldDeepMerge())
514+
->partition(fn (Mergeable $prop) => $prop->prependsAtRoot());
515+
516+
return $nestedPrependProps
517+
->flatMap(fn (Mergeable $prop, string $key) => collect($prop->prependsAtPaths())->map(fn ($path) => $key.'.'.$path))
518+
->merge($rootPrependProps->keys())
519+
->unique()
520+
->values()
521+
->toArray();
522+
}
523+
524+
/**
525+
* Resolve props that should be deep merged.
526+
*
527+
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
528+
* @return array<int, string>
529+
*/
530+
protected function resolveDeepMergeProps(Collection $mergeProps): array
531+
{
532+
return $mergeProps
533+
->filter(fn (Mergeable $prop) => $prop->shouldDeepMerge())
534+
->keys()
535+
->toArray();
536+
}
537+
538+
/**
539+
* Resolve the matching keys for merge props.
540+
*
541+
* @param \Illuminate\Support\Collection<string, \Inertia\Mergeable> $mergeProps
542+
* @return array<int, string>
543+
*/
544+
protected function resolveMergeMatchingKeys(Collection $mergeProps): array
545+
{
546+
return $mergeProps
464547
->map(function (Mergeable $prop, $key) {
465548
return collect($prop->matchesOn())
466549
->map(fn ($strategy) => $key.'.'.$strategy)
467550
->toArray();
468551
})
469552
->flatten()
470-
->values();
471-
472-
$mergeProps = $mergeProps
473-
->filter(fn ($prop) => ! $prop->shouldDeepMerge())
474-
->keys();
475-
476-
return array_filter([
477-
'mergeProps' => $mergeProps->toArray(),
478-
'deepMergeProps' => $deepMergeProps->toArray(),
479-
'matchPropsOn' => $matchPropsOn->toArray(),
480-
], fn ($prop) => count($prop) > 0);
553+
->values()
554+
->toArray();
481555
}
482556

483557
/**
@@ -508,6 +582,20 @@ public function resolveDeferredProps(Request $request): array
508582
return $deferredProps->isNotEmpty() ? ['deferredProps' => $deferredProps->toArray()] : [];
509583
}
510584

585+
/**
586+
* Resolve scroll props configuration for client-side infinite scrolling.
587+
*
588+
* @return array<string, mixed>
589+
*/
590+
public function resolveScrollProps(Request $request): array
591+
{
592+
$scrollProps = $this->getMergePropsForRequest($request)
593+
->filter(fn (Mergeable $prop) => $prop instanceof ScrollProp)
594+
->mapWithKeys(fn (ScrollProp $prop, string $key) => [$key => $prop->metadata()]);
595+
596+
return $scrollProps->isNotEmpty() ? ['scrollProps' => $scrollProps->toArray()] : [];
597+
}
598+
511599
/**
512600
* Determine if the request is a partial request.
513601
*/

0 commit comments

Comments
 (0)