Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"require": {
"php": ">=8.0",
"symfony/ux-twig-component": "^2.0"
"symfony/ux-twig-component": "^2.1"
},
"require-dev": {
"symfony/framework-bundle": "^4.4|^5.0|^6.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\ComponentValidator;
use Symfony\UX\LiveComponent\ComponentValidatorInterface;
use Symfony\UX\LiveComponent\EventListener\AddLiveAttributesSubscriber;
use Symfony\UX\LiveComponent\EventListener\LiveComponentSubscriber;
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\LiveComponent\PropertyHydratorInterface;
Expand All @@ -47,6 +48,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
'key' => $attribute->name,
'template' => $attribute->template,
'default_action' => $attribute->defaultAction,
'live' => true,
]))
->addTag('controller.service_arguments')
;
Expand Down Expand Up @@ -80,6 +82,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {

$container->register('ux.live_component.twig.component_runtime', LiveComponentRuntime::class)
->setArguments([
new Reference('twig'),
new Reference('ux.live_component.component_hydrator'),
new Reference('ux.twig_component.component_factory'),
new Reference(UrlGeneratorInterface::class),
Expand All @@ -92,6 +95,11 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
->addTag('container.service_subscriber', ['key' => 'validator', 'id' => 'validator'])
;

$container->register('ux.live_component.add_attributes_subscriber', AddLiveAttributesSubscriber::class)
->setArguments([new Reference('ux.live_component.twig.component_runtime')])
->addTag('kernel.event_subscriber')
;

$container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Symfony\UX\LiveComponent\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;

/**
* @author Kevin Bond <[email protected]>
*/
final class AddLiveAttributesSubscriber implements EventSubscriberInterface
{
public function __construct(private LiveComponentRuntime $runtime)
{
}

public function onPreRender(PreRenderEvent $event): void
{
if (!isset($event->mountedComponent->config()['live'])) {
// not a live component, skip
return;
}

$event->mountedComponent->attributes = $event->mountedComponent->attributes->merge(
$this->runtime->getLiveAttributes($event->mountedComponent)->all()
);
}

public static function getSubscribedEvents(): array
{
return [PreRenderEvent::class => 'onPreRender'];
}
}
28 changes: 17 additions & 11 deletions src/LiveComponent/src/EventListener/LiveComponentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Symfony\UX\LiveComponent\LiveComponentHydrator;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentRenderer;
use Symfony\UX\TwigComponent\MountedComponent;

/**
* @author Kevin Bond <[email protected]>
Expand Down Expand Up @@ -77,7 +78,11 @@ public function onKernelRequest(RequestEvent $event): void
throw new NotFoundHttpException(sprintf('Component "%s" not found.', $componentName), $e);
}

$request->attributes->set('_component_template', $config['template']);
if (!isset($config['live'])) {
throw new NotFoundHttpException(sprintf('"%s" is not a Live Component.', $config['class']));
}

$request->attributes->set('_component_config', $config);

if ('get' === $action) {
$defaultAction = trim($config['default_action'] ?? '__invoke', '()');
Expand Down Expand Up @@ -135,13 +140,15 @@ public function onKernelController(ControllerEvent $event): void
throw new NotFoundHttpException(sprintf('The action "%s" either doesn\'t exist or is not allowed in "%s". Make sure it exist and has the LiveAction attribute above it.', $action, \get_class($component)));
}

$this->container->get(LiveComponentHydrator::class)->hydrate($component, $data);
$mountedComponent = $this->container->get(LiveComponentHydrator::class)
->hydrate($component, $data, $request->attributes->get('_component_config'))
;

// extra variables to be made available to the controller
// (for "actions" only)
parse_str($request->query->get('values'), $values);
$request->attributes->add($values);
$request->attributes->set('_component', $component);
$request->attributes->set('_mounted_component', $mountedComponent);
}

public function onKernelView(ViewEvent $event): void
Expand All @@ -151,7 +158,7 @@ public function onKernelView(ViewEvent $event): void
return;
}

$response = $this->createResponse($request->attributes->get('_component'), $request);
$response = $this->createResponse($request->attributes->get('_mounted_component'));

$event->setResponse($response);
}
Expand All @@ -168,14 +175,14 @@ public function onKernelException(ExceptionEvent $event): void
return;
}

$component = $request->attributes->get('_component');
$component = $request->attributes->get('_mounted_component');

// in case the exception was too early somehow
if (!$component) {
return;
}

$response = $this->createResponse($component, $request);
$response = $this->createResponse($component);
$event->setResponse($response);
}

Expand Down Expand Up @@ -213,16 +220,15 @@ public static function getSubscribedEvents(): array
];
}

private function createResponse(object $component, Request $request): Response
private function createResponse(MountedComponent $mountedComponent): Response
{
$component = $mountedComponent->component;

foreach (AsLiveComponent::beforeReRenderMethods($component) as $method) {
$component->{$method->name}();
}

$html = $this->container->get(ComponentRenderer::class)->render(
$component,
$request->attributes->get('_component_template')
);
$html = $this->container->get(ComponentRenderer::class)->render($mountedComponent);

return new Response($html);
}
Expand Down
24 changes: 21 additions & 3 deletions src/LiveComponent/src/LiveComponentHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LivePropContext;
use Symfony\UX\LiveComponent\Exception\UnsupportedHydrationException;
use Symfony\UX\TwigComponent\ComponentAttributes;
use Symfony\UX\TwigComponent\MountedComponent;

/**
* @author Kevin Bond <[email protected]>
Expand All @@ -29,6 +31,7 @@ final class LiveComponentHydrator
{
private const CHECKSUM_KEY = '_checksum';
private const EXPOSED_PROP_KEY = '_id';
private const ATTRIBUTES_KEY = '_attributes';

/** @var PropertyHydratorInterface[] */
private iterable $propertyHydrators;
Expand All @@ -45,8 +48,10 @@ public function __construct(iterable $propertyHydrators, PropertyAccessorInterfa
$this->secret = $secret;
}

public function dehydrate(object $component): array
public function dehydrate(MountedComponent $mountedComponent): array
{
$component = $mountedComponent->component;

foreach (AsLiveComponent::preDehydrateMethods($component) as $method) {
$component->{$method->name}();
}
Expand Down Expand Up @@ -100,15 +105,24 @@ public function dehydrate(object $component): array
}
}

if ($mountedComponent->attributes->count()) {
$data[self::ATTRIBUTES_KEY] = $mountedComponent->attributes->all();
$readonlyProperties[] = self::ATTRIBUTES_KEY;
}

$data[self::CHECKSUM_KEY] = $this->computeChecksum($data, $readonlyProperties);

return $data;
}

public function hydrate(object $component, array $data): void
public function hydrate(object $component, array $data, array $config): MountedComponent
{
$readonlyProperties = [];

if (isset($data[self::ATTRIBUTES_KEY])) {
$readonlyProperties[] = self::ATTRIBUTES_KEY;
}

/** @var LivePropContext[] $propertyContexts */
$propertyContexts = iterator_to_array(AsLiveComponent::liveProps($component));

Expand All @@ -129,7 +143,9 @@ public function hydrate(object $component, array $data): void

$this->verifyChecksum($data, $readonlyProperties);

unset($data[self::CHECKSUM_KEY]);
$attributes = new ComponentAttributes($data[self::ATTRIBUTES_KEY] ?? []);

unset($data[self::CHECKSUM_KEY], $data[self::ATTRIBUTES_KEY]);

foreach ($propertyContexts as $context) {
$property = $context->reflectionProperty();
Expand Down Expand Up @@ -187,6 +203,8 @@ public function hydrate(object $component, array $data): void
foreach (AsLiveComponent::postHydrateMethods($component) as $method) {
$component->{$method->name}();
}

return new MountedComponent($component, $attributes, $config);
}

private function computeChecksum(array $data, array $readonlyProperties): string
Expand Down
28 changes: 14 additions & 14 deletions src/LiveComponent/src/Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ A real-time product search component might look like this::
.. code-block:: twig

{# templates/components/product_search.html.twig #}
<div {{ init_live_component(this) }}>
<div {{ init_live_component() }}>
<input
type="search"
name="query"
Expand Down Expand Up @@ -165,7 +165,7 @@ initialize the Stimulus controller:
.. code-block:: diff

- <div>
+ <div {{ init_live_component(this) }}>
+ <div {{ init_live_component() }}>
<strong>{{ this.randomNumber }}</strong>
</div>

Expand All @@ -176,7 +176,7 @@ and give the user a new random number:

.. code-block:: twig

<div {{ init_live_component(this) }}>
<div {{ init_live_component() }}>
<strong>{{ this.randomNumber }}</strong>

<button
Expand Down Expand Up @@ -251,7 +251,7 @@ Let's add two inputs to our template:
.. code-block:: twig

{# templates/components/random_number.html.twig #}
<div {{ init_live_component(this) }}>
<div {{ init_live_component() }}>
<input
type="number"
value="{{ min }}"
Expand Down Expand Up @@ -368,7 +368,7 @@ property. The following code works identically to the previous example:

.. code-block:: diff

<div {{ init_live_component(this)>
<div {{ init_live_component()>
<input
type="number"
value="{{ min }}"
Expand Down Expand Up @@ -755,7 +755,7 @@ as ``this.form`` thanks to the trait:

{# templates/components/post_form.html.twig #}
<div
{{ init_live_component(this) }}
{{ init_live_component() }}
{#
Automatically catch all "change" events from the fields
below and re-render the component.
Expand All @@ -779,7 +779,7 @@ as ``this.form`` thanks to the trait:
</div>

Mostly, this is a pretty boring template! It includes the normal
``init_live_component(this)`` and then you render the form however you
``init_live_component()`` and then you render the form however you
want.

But the result is incredible! As you finish changing each field, the
Expand Down Expand Up @@ -988,7 +988,7 @@ section above) is to add:
.. code-block:: diff

<div
{{ init_live_component(this) }}
{{ init_live_component() }}
+ data-action="change->live#update"
>

Expand Down Expand Up @@ -1020,7 +1020,7 @@ rendered the ``content`` through a Markdown filter from the

.. code-block:: twig

<div {{init_live_component(this)}}>
<div {{init_live_component()}}>
<input
type="text"
value="{{ post.title }}"
Expand Down Expand Up @@ -1185,7 +1185,7 @@ You can also use “polling” to continually refresh a component. On the
.. code-block:: diff

<div
{{ init_live_component(this) }}
{{ init_live_component() }}
+ data-poll
>

Expand All @@ -1197,7 +1197,7 @@ delay for 500ms:
.. code-block:: twig

<div
{{ init_live_component(this) }}
{{ init_live_component() }}
data-poll="delay(500)|$render"
>

Expand All @@ -1206,7 +1206,7 @@ You can also trigger a specific “action” instead of a normal re-render:
.. code-block:: twig

<div
{{ init_live_component(this) }}
{{ init_live_component() }}

data-poll="save"
{#
Expand Down Expand Up @@ -1401,7 +1401,7 @@ In the ``EditPostComponent`` template, you render the
.. code-block:: twig

{# templates/components/edit_post.html.twig #}
<div {{ init_live_component(this) }}>
<div {{ init_live_component() }}>
<input
type="text"
name="post[title]"
Expand All @@ -1423,7 +1423,7 @@ In the ``EditPostComponent`` template, you render the

.. code-block:: twig

<div {{ init_live_component(this) }} class="mb-3">
<div {{ init_live_component() }} class="mb-3">
<textarea
name="{{ name }}"
data-model="value"
Expand Down
2 changes: 1 addition & 1 deletion src/LiveComponent/src/Twig/LiveComponentExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class LiveComponentExtension extends AbstractExtension
public function getFunctions(): array
{
return [
new TwigFunction('init_live_component', [LiveComponentRuntime::class, 'renderLiveAttributes'], ['needs_environment' => true, 'is_safe' => ['html_attr']]),
new TwigFunction('init_live_component', [LiveComponentRuntime::class, 'renderLiveAttributes'], ['needs_context' => true, 'is_safe' => ['html_attr']]),
new TwigFunction('component_url', [LiveComponentRuntime::class, 'getComponentUrl']),
];
}
Expand Down
Loading