Skip to content

Commit f97d4a7

Browse files
authored
Add Cascade attribute for Cascade data autoloading (#17)
1 parent 93505bf commit f97d4a7

File tree

6 files changed

+324
-6
lines changed

6 files changed

+324
-6
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ A third-party [Laravel Livewire](https://laravel-livewire.com/) integration for
1717
+ [Static caching](#static-caching)
1818
+ [`@script` and `@assets`](#--script--and---assets-)
1919
+ [Computed Properties](#computed-properties)
20+
+ [Cascade](#cascade)
2021
+ [Multi-Site / Localization](#multi-site---localization)
2122
+ [Lazy Components](#lazy-components)
2223
+ [Paginating Data](#paginating-data)
@@ -192,6 +193,40 @@ public function entries() {
192193
{{ /entries }}
193194
```
194195
196+
### Cascade
197+
Normally all the variables in the Cascade are only available on initial render and get lost between Livewire requests. This means you'd need pass in the required ones into the component yourself.
198+
To make our lives a bit easier, you can add the `#[Cascade]` attribute to your component. <br> This is only needed for Antlers views and mirrors the logic of Blade's [`@cascade`](https://statamic.dev/blade#cascade-directive) directive.
199+
200+
```php
201+
use Livewire\Component;
202+
use MarcoRieser\Livewire\Attributes\Cascade;
203+
204+
#[Cascade]
205+
class ShowArticle extends Component
206+
{
207+
...
208+
}
209+
```
210+
211+
Now you can access the variables from the Cascade directly in your Antlers view, even on subsequent renders:
212+
213+
```antlers
214+
<h1>{{ title }}</h1>
215+
{{ seo_title }}
216+
```
217+
218+
You can also limit which cascade keys are exposed (and provide defaults):
219+
220+
```php
221+
#[Cascade([
222+
'title',
223+
'seo_title' => 'Fallback title',
224+
])]
225+
class ShowArticle extends Component {}
226+
```
227+
228+
For subsequent requests, the addon restores the Cascade using the original Livewire URL, so site, request, and content data resolve as expected.
229+
195230
### Multi-Site / Localization
196231
By default, your current site is persisted between Livewire requests automatically.
197232
In case you want to implement your own logic, you can disable `localization` in your published `config/statamic-livewire.php` config.

src/Attributes/Cascade.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace MarcoRieser\Livewire\Attributes;
4+
5+
use Illuminate\Support\Arr;
6+
use Livewire\Features\SupportAttributes\Attribute as LivewireAttribute;
7+
use Statamic\Exceptions\CascadeDataNotFoundException;
8+
use Statamic\Facades\Cascade as CascadeFacade;
9+
10+
#[\Attribute]
11+
class Cascade extends LivewireAttribute
12+
{
13+
public function __construct(public array $keys = []) {}
14+
15+
public function getCascadeData(): array
16+
{
17+
if (! $data = CascadeFacade::toArray()) {
18+
$data = CascadeFacade::hydrate()->toArray();
19+
}
20+
21+
if (! $this->keys) {
22+
return $data;
23+
}
24+
25+
return collect($this->keys)
26+
->mapWithKeys(function ($default, $key) use ($data) {
27+
if (is_numeric($key)) {
28+
$key = $default;
29+
$default = null;
30+
31+
if (! array_key_exists($key, $data)) {
32+
throw new CascadeDataNotFoundException($key);
33+
}
34+
}
35+
36+
return [$key => Arr::get($data, $key, $default)];
37+
})
38+
->all();
39+
}
40+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace MarcoRieser\Livewire\Hooks;
4+
5+
use Illuminate\View\View;
6+
use Livewire\Component;
7+
use Livewire\ComponentHook;
8+
use Livewire\Livewire;
9+
use MarcoRieser\Livewire\Attributes\Cascade as CascadeAttribute;
10+
use Statamic\View\Antlers\Engine as AntlersEngine;
11+
12+
class CascadeVariablesAutoloader extends ComponentHook
13+
{
14+
public function render($view, $data): void
15+
{
16+
/** @var Component $component */
17+
if (! ($component = Livewire::current())) {
18+
return;
19+
}
20+
21+
if (! $this->isUsingAntlers($view)) {
22+
return;
23+
}
24+
25+
/** @var ?CascadeAttribute $attribute */
26+
$attribute = $component
27+
->getAttributes()
28+
->whereInstanceOf(CascadeAttribute::class)
29+
->first();
30+
31+
if (! $attribute) {
32+
return;
33+
}
34+
35+
$cascade = $attribute->getCascadeData();
36+
37+
$view->with(array_merge($cascade, $data));
38+
}
39+
40+
protected function isUsingAntlers(View $view): bool
41+
{
42+
return $view->getEngine() instanceof AntlersEngine;
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace MarcoRieser\Livewire\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Request as RequestFacade;
8+
use Livewire\Livewire;
9+
use Statamic\Facades\Cascade;
10+
use Statamic\Facades\Data;
11+
use Statamic\Facades\Site;
12+
use Symfony\Component\HttpFoundation\Response;
13+
14+
class HydrateCascadeByLivewireUrl
15+
{
16+
/**
17+
* Handle an incoming request.
18+
*
19+
* @param Closure(Request): (Response) $next
20+
*/
21+
public function handle(Request $request, Closure $next): Response
22+
{
23+
$this->hydrateSite();
24+
$this->hydrateRequest();
25+
$this->hydrateContent();
26+
27+
return $next($request);
28+
}
29+
30+
protected function hydrateSite(): void
31+
{
32+
Cascade::withSite(Site::current());
33+
}
34+
35+
protected function hydrateRequest(): void
36+
{
37+
Cascade::withRequest(RequestFacade::create(uri: Livewire::originalUrl(), method: Livewire::originalMethod()));
38+
}
39+
40+
protected function hydrateContent(): void
41+
{
42+
Cascade::withContent(Data::findByRequestUrl(Livewire::originalUrl()));
43+
}
44+
}

src/ServiceProvider.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
use Illuminate\Routing\Router;
77
use Livewire\Livewire;
88
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
9+
use MarcoRieser\Livewire\Hooks\CascadeVariablesAutoloader;
910
use MarcoRieser\Livewire\Hooks\ComputedPropertiesAutoloader;
1011
use MarcoRieser\Livewire\Hooks\SynthesizerAugmentor;
12+
use MarcoRieser\Livewire\Http\Middleware\HydrateCascadeByLivewireUrl;
1113
use MarcoRieser\Livewire\Http\Middleware\ResolveCurrentSiteByLivewireUrl;
1214
use Statamic\Http\Middleware\Localize;
1315
use Statamic\Providers\AddonServiceProvider;
1416

1517
class ServiceProvider extends AddonServiceProvider
1618
{
19+
protected array $middlewares = [];
20+
1721
protected $tags = [
1822
'MarcoRieser\Livewire\Tags\Livewire',
1923
];
@@ -24,13 +28,16 @@ public function register(): void
2428

2529
$this->registerSynthesizerAugmentation();
2630
$this->registerComputedPropertiesAutoloader();
31+
$this->registerCascadeVariablesAutoloader();
2732
}
2833

2934
public function bootAddon(): void
3035
{
3136
$this->bootLocalization();
37+
$this->bootCascadeRestoration();
3238
$this->bootReplacers();
3339
$this->bootSynthesizers();
40+
$this->bootMiddlewares();
3441
}
3542

3643
protected function bootLocalization(): void
@@ -39,12 +46,13 @@ protected function bootLocalization(): void
3946
return;
4047
}
4148

42-
collect($this->app->make(Router::class)->getRoutes()->getRoutes())
43-
->filter(fn (Route $route) => $route->named('*livewire.update'))
44-
->each(fn (Route $route) => $route->middleware([
45-
ResolveCurrentSiteByLivewireUrl::class,
46-
Localize::class,
47-
]));
49+
$this->middlewares[] = ResolveCurrentSiteByLivewireUrl::class;
50+
$this->middlewares[] = Localize::class;
51+
}
52+
53+
protected function bootCascadeRestoration(): void
54+
{
55+
$this->middlewares[] = HydrateCascadeByLivewireUrl::class;
4856
}
4957

5058
protected function bootReplacers(): void
@@ -75,4 +83,16 @@ protected function registerComputedPropertiesAutoloader(): void
7583
{
7684
Livewire::componentHook(ComputedPropertiesAutoloader::class);
7785
}
86+
87+
protected function registerCascadeVariablesAutoloader(): void
88+
{
89+
Livewire::componentHook(CascadeVariablesAutoloader::class);
90+
}
91+
92+
protected function bootMiddlewares(): void
93+
{
94+
collect($this->app->make(Router::class)->getRoutes()->getRoutes())
95+
->filter(fn (Route $route) => $route->named('*livewire.update'))
96+
->each(fn (Route $route) => $route->middleware($this->middlewares));
97+
}
7898
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace MarcoRieser\Livewire\Tests\Hooks;
4+
5+
use Illuminate\View\ViewException;
6+
use Livewire\Component;
7+
use Livewire\Livewire;
8+
use MarcoRieser\Livewire\Attributes\Cascade;
9+
use MarcoRieser\Livewire\Tests\TestCase;
10+
use PHPUnit\Framework\Attributes\Test;
11+
use Statamic\Testing\Concerns\PreventsSavingStacheItemsToDisk;
12+
13+
class CascadeVariablesAutoloaderTest extends TestCase
14+
{
15+
use PreventsSavingStacheItemsToDisk;
16+
17+
#[Test]
18+
public function cascade_variables_are_autoloaded_in_antlers()
19+
{
20+
$component = $this->getAntlersLivewireComponent();
21+
22+
$testable = Livewire::test($component);
23+
24+
$testable->assertViewHas('homepage', '/');
25+
$testable->assertViewHas('environment', 'testing');
26+
}
27+
28+
#[Test]
29+
public function cascade_variables_are_not_autoloaded_in_blade()
30+
{
31+
$component = $this->getBladeLivewireComponent();
32+
33+
$testable = Livewire::test($component);
34+
35+
$testable->assertViewMissing('homepage');
36+
$testable->assertViewMissing('environment');
37+
}
38+
39+
#[Test]
40+
public function cascade_variables_are_autoloaded_selectively()
41+
{
42+
$component = $this->getSelectedLivewireComponent();
43+
44+
$testable = Livewire::test($component);
45+
46+
$testable->assertViewHas('homepage', '/');
47+
$testable->assertViewHas('my_global', true);
48+
$testable->assertViewMissing('environment');
49+
}
50+
51+
#[Test]
52+
public function cascade_variables_throw_exception_when_invalid()
53+
{
54+
$this->expectException(ViewException::class);
55+
$this->expectExceptionMessage('Cascade data [my_invalid] not found');
56+
57+
$component = $this->getInvalidLivewireComponent();
58+
59+
Livewire::test($component);
60+
}
61+
62+
#[Test]
63+
public function cascade_variables_are_not_autoloaded_when_attribute_excluded()
64+
{
65+
$component = $this->getExcludedLivewireComponent();
66+
67+
$testable = Livewire::test($component);
68+
69+
$testable->assertViewMissing('homepage');
70+
$testable->assertViewMissing('environment');
71+
}
72+
73+
protected function getAntlersLivewireComponent(): Component
74+
{
75+
return new
76+
#[Cascade]
77+
class extends Component
78+
{
79+
public function render()
80+
{
81+
return view('antlers');
82+
}
83+
};
84+
}
85+
86+
protected function getBladeLivewireComponent(): Component
87+
{
88+
return new
89+
#[Cascade]
90+
class extends Component
91+
{
92+
public function render()
93+
{
94+
return view('blade');
95+
}
96+
};
97+
}
98+
99+
protected function getSelectedLivewireComponent(): Component
100+
{
101+
return new
102+
#[Cascade(['homepage', 'my_global' => true])]
103+
class extends Component
104+
{
105+
public function render()
106+
{
107+
return view('antlers');
108+
}
109+
};
110+
}
111+
112+
protected function getInvalidLivewireComponent(): Component
113+
{
114+
return new
115+
#[Cascade(['my_invalid'])]
116+
class extends Component
117+
{
118+
public function render()
119+
{
120+
return view('antlers');
121+
}
122+
};
123+
}
124+
125+
protected function getExcludedLivewireComponent(): Component
126+
{
127+
return new class extends Component
128+
{
129+
public function render()
130+
{
131+
return view('antlers');
132+
}
133+
};
134+
}
135+
}

0 commit comments

Comments
 (0)