Skip to content

Commit 2c06bf9

Browse files
committed
feature #1143 Add deferred live components (jakubtobiasz)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- Add deferred live components | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | Fix #994 | License | MIT Initially this feature was called `lazy loading`, but this name could be misleading, as my solution doesn't provide **real** lazy loading. So, I named it `deferred live components`, I guess this name fits better. How to use it? ```twig <twig:MyComponent defer /> ``` In such case we'll get an empty div. Once `live:connect` event called, it'll load the component in the background. We can also define a template to be rendered while loading (e.g. a cool spinner or some text). ```twig <twig:MyComponent defer defer-loading-template="my_cool_spinner.html.twig" /> ``` I'm open for any suggestions 🙌🏼! Commits ------- d93cc29741 Add deferred live components
2 parents 05bf326 + 51ae346 commit 2c06bf9

12 files changed

+222
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.13.0
4+
5+
- Add deferred rendering of Live Components
6+
37
## 2.12.0
48

59
- Add support for (de)hydrating DTO classes in `LiveProp`.

doc/index.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,6 +2220,32 @@ To validate only on "change", use the ``on(change)`` modifier:
22202220
class="{{ _errors.has('post.content') ? 'is-invalid' : '' }}"
22212221
>
22222222

2223+
Deferring the Loading
2224+
---------------------
2225+
2226+
Certain components might be heavy to load. You can defer the loading of these components
2227+
until after the rest of the page has loaded. To do this, use the ``defer`` attribute:
2228+
2229+
.. code-block:: twig
2230+
2231+
{{ component('SomeHeavyComponent', { defer: true }) }}
2232+
2233+
Doing so will render an empty "placeholder" tag with the live attributes. Once the ``live:connect`` event is triggered,
2234+
the component will be rendered asynchronously.
2235+
2236+
By default the rendered tag is a ``div``. You can change this by specifying the ``loading-tag`` attribute:
2237+
2238+
.. code-block:: twig
2239+
2240+
{{ component('SomeHeavyComponent', { defer: true, loading-tag: 'span' }) }}
2241+
2242+
If you need to signify that the component is loading, use the ``loading-template`` attribute.
2243+
This lets you provide a Twig template that will render inside the "placeholder" tag:
2244+
2245+
.. code-block:: twig
2246+
2247+
{{ component('SomeHeavyComponent', { defer: true, loading-template: 'spinning-wheel.html.twig' }) }}
2248+
22232249
Polling
22242250
-------
22252251

src/DependencyInjection/LiveComponentExtension.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\UX\LiveComponent\Controller\BatchActionController;
2626
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
2727
use Symfony\UX\LiveComponent\EventListener\DataModelPropsSubscriber;
28+
use Symfony\UX\LiveComponent\EventListener\DeferLiveComponentSubscriber;
2829
use Symfony\UX\LiveComponent\EventListener\InterceptChildComponentRenderSubscriber;
2930
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
3031
use Symfony\UX\LiveComponent\EventListener\ResetDeterministicIdSubscriber;
@@ -215,6 +216,14 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
215216
->addTag('container.service_subscriber', ['key' => LiveControllerAttributesCreator::class, 'id' => 'ux.live_component.live_controller_attributes_creator'])
216217
;
217218

219+
$container->register('ux.live_component.defer_live_component_subscriber', DeferLiveComponentSubscriber::class)
220+
->setArguments([
221+
new Reference('ux.twig_component.component_stack'),
222+
new Reference('ux.live_component.live_controller_attributes_creator'),
223+
])
224+
->addTag('kernel.event_subscriber')
225+
;
226+
218227
$container->register('ux.live_component.deterministic_id_calculator', DeterministicTwigIdCalculator::class);
219228
$container->register('ux.live_component.fingerprint_calculator', FingerprintCalculator::class)
220229
->setArguments(['%kernel.secret%']);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\UX\LiveComponent\EventListener;
6+
7+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8+
use Symfony\UX\TwigComponent\Event\PostMountEvent;
9+
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
10+
11+
final class DeferLiveComponentSubscriber implements EventSubscriberInterface
12+
{
13+
private const DEFAULT_LOADING_TAG = 'div';
14+
15+
private const DEFAULT_LOADING_TEMPLATE = null;
16+
17+
public function onPostMount(PostMountEvent $event): void
18+
{
19+
$data = $event->getData();
20+
if (\array_key_exists('defer', $data)) {
21+
$event->addExtraMetadata('defer', true);
22+
unset($event->getData()['defer']);
23+
}
24+
25+
if (\array_key_exists('loading-template', $data)) {
26+
$event->addExtraMetadata('loading-template', $data['loading-template']);
27+
unset($event->getData()['loading-template']);
28+
}
29+
30+
if (\array_key_exists('loading-tag', $data)) {
31+
$event->addExtraMetadata('loading-tag', $data['loading-tag']);
32+
unset($event->getData()['loading-tag']);
33+
}
34+
35+
$event->setData($data);
36+
}
37+
38+
public function onPreRender(PreRenderEvent $event): void
39+
{
40+
$mountedComponent = $event->getMountedComponent();
41+
42+
if (!$mountedComponent->hasExtraMetadata('defer')) {
43+
return;
44+
}
45+
46+
$event->setTemplate('@LiveComponent/deferred.html.twig');
47+
48+
$variables = $event->getVariables();
49+
$variables['loadingTemplate'] = self::DEFAULT_LOADING_TEMPLATE;
50+
$variables['loadingTag'] = self::DEFAULT_LOADING_TAG;
51+
52+
if ($mountedComponent->hasExtraMetadata('loading-template')) {
53+
$variables['loadingTemplate'] = $mountedComponent->getExtraMetadata('loading-template');
54+
}
55+
56+
if ($mountedComponent->hasExtraMetadata('loading-tag')) {
57+
$variables['loadingTag'] = $mountedComponent->getExtraMetadata('loading-tag');
58+
}
59+
60+
$event->setVariables($variables);
61+
}
62+
63+
public static function getSubscribedEvents(): array
64+
{
65+
return [
66+
PostMountEvent::class => ['onPostMount'],
67+
PreRenderEvent::class => ['onPreRender'],
68+
];
69+
}
70+
}

templates/deferred.html.twig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<{{ loadingTag }} {{ attributes }} data-action="live:connect->live#$render">
2+
{% block loadingContent %}
3+
{% if loadingTemplate != null %}
4+
{{ include(loadingTemplate) }}
5+
{% endif %}
6+
{% endblock %}
7+
</{{ loadingTag }}>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\UX\LiveComponent\Tests\Fixtures\Component;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
use Symfony\UX\LiveComponent\DefaultActionTrait;
9+
10+
#[AsLiveComponent('deferred_component')]
11+
final class DeferredComponent
12+
{
13+
use DefaultActionTrait;
14+
15+
public function getLongAwaitedData(): string
16+
{
17+
return 'Long awaited data';
18+
}
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div {{ attributes }}>{{ computed.longAwaitedData }}</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I'm loading a reaaaally slow live component
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<twig:deferred_component defer />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<twig:deferred_component defer loading-tag='li' />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<twig:deferred_component defer loading-template='dummy/loading.html.twig' />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symfony\UX\LiveComponent\Tests\Functional\EventListener;
6+
7+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
8+
use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
9+
use Zenstruck\Browser\Test\HasBrowser;
10+
11+
final class DeferLiveComponentSubscriberTest extends KernelTestCase
12+
{
13+
use HasBrowser;
14+
use LiveComponentTestHelper;
15+
16+
public function testItSetsDeferredTemplateIfLiveIdNotPassed(): void
17+
{
18+
$div = $this->browser()
19+
->visit('/render-template/render_deferred_component')
20+
->assertSuccessful()
21+
->crawler()
22+
->filter('div')
23+
;
24+
25+
$this->assertSame('', trim($div->html()));
26+
$this->assertSame('live:connect->live#$render', $div->attr('data-action'));
27+
28+
$component = $this->mountComponent('deferred_component', [
29+
'data-live-id' => $div->attr('data-live-id'),
30+
]);
31+
32+
$dehydrated = $this->dehydrateComponent($component);
33+
34+
$div = $this->browser()
35+
->visit('/_components/deferred_component?props='.urlencode(json_encode($dehydrated->getProps())))
36+
->assertSuccessful()
37+
->crawler()
38+
->filter('div')
39+
;
40+
41+
$this->assertSame('Long awaited data', $div->html());
42+
}
43+
44+
public function testItIncludesGivenTemplateWhileLoadingDeferredComponent(): void
45+
{
46+
$div = $this->browser()
47+
->visit('/render-template/render_deferred_component_with_template')
48+
->assertSuccessful()
49+
->crawler()
50+
->filter('div')
51+
;
52+
53+
$this->assertSame('I\'m loading a reaaaally slow live component', trim($div->html()));
54+
55+
$component = $this->mountComponent('deferred_component', [
56+
'data-live-id' => $div->attr('data-live-id'),
57+
]);
58+
59+
$dehydrated = $this->dehydrateComponent($component);
60+
61+
$div = $this->browser()
62+
->visit('/_components/deferred_component?props='.urlencode(json_encode($dehydrated->getProps())))
63+
->assertSuccessful()
64+
->crawler()
65+
->filter('div')
66+
;
67+
68+
$this->assertStringContainsString('Long awaited data', $div->html());
69+
}
70+
71+
public function testItAllowsToSetCustomLoadingHtmlTag(): void
72+
{
73+
$crawler = $this->browser()
74+
->visit('/render-template/render_deferred_component_with_li_tag')
75+
->assertSuccessful()
76+
->crawler()
77+
;
78+
79+
$this->assertSame(0, $crawler->filter('div')->count());
80+
$this->assertSame(1, $crawler->filter('li')->count());
81+
}
82+
}

0 commit comments

Comments
 (0)