symfony/ux-live-component
Build interactive UIs in Symfony with Live Components: stateful Twig components that update via Ajax without writing custom JavaScript. Handle actions, validation, and form binding, with predictable server-side rendering and smooth partial updates.
loading attribute on a deferred componentaria-busy attribute during component re-renderUnprocessableEntityHttpException thrown by submitForm() when validation failsX-Requested-With: XMLHttpRequest request header on LiveComponent check.
See section 2.36 below for details.csrf argument from AsLiveComponent in favor of same-origin/CORSLegacyLivePropMetadatacsrf argument from AsLiveComponent in favor of same-origin/CORSLegacyLivePropMetadataLiveComponentSubscriber ControllerEvent from
0 to 10 to fix incompatibility with SensioFrameworkExtraBundle.exposed option was changed to writable in LiveProp:-#[LiveProp(exposed: ['email', 'plainPassword'])]
+#[LiveProp(writable: ['email', 'plainPassword'])]
public User $user;
[BC BREAK]: LiveProp values are no longer automatically (de)hydrated
through Symfony's serializer. Use LiveProp(useSerializerForHydration: true)
to activate this. Also, a serializationContext option was added to
LiveProp.
[BC BREAK]: Child components are no longer automatically re-rendered when
a parent component re-renders and the value of one of the props passed to
the child has changed. Pass acceptUpdatesFromParent: true to any LiveProp
on the child component to re-enable this behavior.
Non-persisted entity objects can now be used with LiveProp: it will be
serialized using the serializer.
Better support for using arrays with LiveProp.
Smart rendering system! If you have JavaScript that makes changes to the DOM inside a live component, those changes will now be kept when the component is re-rendered. This has limitations - see the documentation.
You can now emit() events to communicate between components.
You can now dispatch DOM/browser events from components.
Boolean checkboxes are now supported. Of a checkbox does not have a
value attribute, then the associated LiveProp will be set to a boolean
when the input is checked/unchecked.
A format option was added to LiveProp to control how DateTime
properties are (de)hydrated.
Added support for setting writable to a property that is an object
(previously, only scalar values were supported). The object is passed
through the serializer.
Invalid data sent by the user is now handled in a robust way. Previously,
if the user sent invalid data (e.g. a string for a LiveProp that has
an int type), the component update would break. Now, if the new data
cannot be hydrated onto the object during a re-render, the last valid
value is used.
When using ValidatableComponentTrait, a new _errors variable is sent
to the template, which is easier to use!
Several bug fixes to parent - child components - see #700.
Fixed handling of boolean attributes to a component - see #710.
Fixed performance calculating component fingerprint for large components.
[BC BREAK]: The "key" used to load the controller in your assets/controllers.json
file changed from typed to live. Update your assets/controllers.json
file to change this key.
Add a strategy for adding a Stimulus controller to a Twig component - #589.
Added a new getCompontent() function in JavaScript as the best way to find
a Component object for a given element.
Fixed various bugs related to child component handling - #596
Added a new route parameter to AsLiveComponent, which allows to choose
another route for Ajax calls.
Add assets/src to .gitattributes to exclude source TypeScript files from
installing.
TypeScript types are now included.
Added new response:error JavaScript component hook for custom handling Ajax errors - #587.
live_component.xml changed and the import now
MUST have a prefix: you should update your route import accordingly (the
name of the route also changed to ux_live_component):# config/routes/ux_live_component.yaml
live_component:
- resource: '[@LiveComponentBundle](https://github.com/LiveComponentBundle)/Resources/config/routing/live_component.xml'
+ resource: '[@LiveComponentBundle](https://github.com/LiveComponentBundle)/config/routes.php'
+ prefix: /_components
Content-Type header when returning the empty response redirect.[BEHAVIOR CHANGE] Previously, Ajax calls could happen in parallel (if you changed a model then triggered an action before the model update Ajax call finished, the action Ajax call would being in parallel). Now, if an Ajax call is currently happening, any future requests will wait until it finishes. Then, all queued changes (potentially multiple model updates or actions) will be sent all at once on the next request.
[BEHAVIOR CHANGE] Fields with data-model will now have their value set
automatically when the component initially loads and re-renders. For example,
previously you needed to manually set the value in your component template:
<!-- BEFORE -->
<input data-model="firstName" value="{{ firstName }}">
This is no longer necessary: Live Components will now set the value on load, which allows you to simply have the following in your template:
<!-- AFTER -->
<input data-model="firstName">
[BEHAVIOR CHANGE] The way that child components re-render when a parent re-renders
has changed, but shouldn't be drastically different. Child components will now
avoid re-rendering if no "input" to the component changed and will maintain
any writable LiveProp values after the re-render. Also, the re-render happens
in a separate Ajax call after the parent has finished re-rendering.
[BEHAVIOR CHANGE] If a model is updated, but the new value is equal to the old one, a re-render will now be avoided.
[BEHAVIOR CHANGE] Priority of DoctrineObjectNormalizer changed from 100 to -100
so that any custom normalizers are used before trying DoctrineObjectNormalizer.
[BC BREAK] The live:update-model and live:render events are not longer
dispatched. You can now use the "hook" system directly on the Component object/
[BC BREAK] The LiveComponentHydrator::dehydrate() method now returns a
DehydratedComponent object.
Added a new JavaScript Component object, which is attached to the __component
property of all root component elements.
the ability to add data-loading behavior, which is only activated
when a specific action is triggered - e.g. <span data-loading="action(save)|show">Loading</span>.
Added the ability to add data-loading behavior, which is only activated
when a specific model has been updated - e.g. <span data-loading="model(firstName)|show">Loading</span>.
Unexpected Ajax errors are now displayed in a modal to ease debugging! #467.
Fixed bug where sometimes a live component was broken after hitting "Back: in your browser - #436.
[BC BREAK] Previously, the id attribute was used with morphdom as the
"node id" when updating the DOM after a render. This has changed to
data-live-id. This is useful when maintaining the correct order of a list
of elements.
[BC BREAK] If using LiveCollectionType, the name of the remove field changed
from button_delete_prototype to button_delete and the add field changed
from button_add_prototype to button_add. Additionally, the allow_add
and allow_delete default values were changed from false to true.
[BEHAVIOR CHANGE] If an action Ajax call is still processing and a
model update occurs, the component will no longer re-render. The
model will be updated internally, but not re-rendered (so, any
model updates would effectively have the |norender modifier). See #419.
Reject malicious child component tags during rendering to prevent crafted component names from being rendered (security fix).
Cap the number of actions allowed per _batch request to prevent abuse
(security fix).
Parse format-less date LiveProps strictly using RFC 3339 to avoid lenient
date parsing of attacker-controlled values (security fix).
Change how the Live Component request checksum is computed (security fix). BC note: users with a payload signed before the upgrade will need to reload their page.
Require the X-Requested-With: XMLHttpRequest request header on LiveComponent
requests, in addition to the existing Accept: application/vnd.live-component+html
check, to prevent CSRF. The Accept header alone is CORS-safelisted and offers
no protection against cross-origin requests crafted with fetch().
BC break (minor): clients calling LiveComponent endpoints cross-origin must
now add X-Requested-With to their CORS Access-Control-Allow-Headers allow-list.
The bundled Stimulus controller already sends this header, so standard usage
is unaffected.
Add fetch_credentials option to configure the fetch API credentials mode for cross-origin requests.
This is useful when embedding a Live Component from a different domain that requires cookie-based authentication (e.g., JWT stored in cookies)
Global configuration in config/packages/live_component.yaml:
live_component:
fetch_credentials: 'include' # 'same-origin' (default), 'include', or 'omit'
Per-component override via the #[AsLiveComponent] attribute:
#[AsLiveComponent(fetchCredentials: 'include')]
class MyComponent
{
// ...
}
Add support for dynamic template resolution with AsLiveComponent(template: FromMethod('customFunction'))
Add debug:live-component command
InteractsWithLiveComponents:$testComponent = $this->createLiveComponent(name: 'MyComponent');
$renderedComponent = $testComponent->render();
// Assert that the component did dispatch a browser event named 'browser:event'
$this->assertComponentDispatchBrowserEvent($render, 'browser:event')
// optionally, you can assert that the browser event was dispatched with specific data...
->withData(['arg1' => 'foo', 'arg2' => 'bar'])
// ... or only with a subset of data
->withDataSubset(['arg1' => 'foo']);
// Assert that the component did not dispatch a browser event named 'another-browser:event'
$this->assertComponentNotDispatchBrowserEvent($render, 'another-browser:event');
data-action="live#update" attribute must now be
removed from nearly all elements. This is because LiveComponents
now automatically listens to the input event on all elements
with a data-model attribute and updates the data. If you previously
used data-action="change->live#update" to list on the change
event, now you should use the on(change) modifier inside data-model.<!-- BEFORE -->
<input
data-model="max"
data-action="change->live#update"
>
<!-- AFTER -->
<input
data-model="on(change)|max"
>
live#updateDefer action was removed entirely.
Now, to update a model without triggering a re-render, use the
norender modifier for data-model:<!-- BEFORE -->
<input
data-model="max"
data-action="live#updateDefer"
>
<!-- AFTER -->
<input
data-model="norender|max"
>
name attribute is no longer automatically used to
update a model when a parent component has data-action="change->live#update".
To make a form's fields behave like "model" fields (but using the
name attribute instead of data-model) you need to add a data-model
attribute to the <form> element around your fields (NOTE: the
new attribute is automatically added to your form element when
using ComponentWithFormTrait):<!-- BEFORE -->
<form data-action="change->live#update">
<input
name="max"
>
</form>
<!-- AFTER -->
<form data-model="on(change)|*">
<input
name="max"
>
</form>
Add new modifiers for input validations, useful to prevent unnecessary HTTP requests:
min_length and max_length: validate length from textual input elementsmin_value and max_value: validate value from numeral input elementsAdd new mapPath options (default false) to UrlMapping of a LiveProp
to allow the prop to be mapped to the path instead of the query in the url.
<!-- Do not trigger model update until 3 characters are typed -->
<input data-model="min_length(3)|username" type="text" value="" />
<!-- Only trigger updates when value number is between 10 and 100 -->
<input data-model="min_value(10)|max_value(100)|quantity" type="number" value="20" />
InteractsWithLiveComponents:$testComponent = $this->createLiveComponent(name: 'MyComponent');
$renderedComponent = $testComponent->render();
// Assert that the component did emit an event named 'event'
$this->assertComponentEmitEvent($render, 'event')
// optionally, you can assert that the event was emitted with specific data...
->withData(['arg1' => 'foo', 'arg2' => 'bar'])
// ... or only with a subset of data
->withDataSubset(['arg1' => 'foo']);
// Assert that the component did not emit an event named 'another-event'
$this->assertComponentNotEmitEvent($render, 'another-event');
LiveProp: Pass the property name as second parameter of the modifier callableSymfony\Component\PropertyInfo\PropertyInfoExtractor::getTypes().
If you use PHP 8.2 or higher, we recommend you to update dependency symfony/property-info to at least 7.1.0ComponentWithFormTrait now correctly checks for a TranslatableInterface placeholder for <select> elementsLiveComponentHydrator::hydrateValue() to hydrate null values__component property to be serialized when called JSON.stringify()The bundle now properly exposes a live controller, which can be
imported via your assets/controllers.json file (like any other
UX package). Previously, the controller needed to be imported and
registered with Stimulus directly (usually in your assets/bootstrap.js
file). That is no longer needed.
Add a generic LiveCollectionType and LiveCollectionTrait
Allow to disable CSRF per component
submitForm() to TestLiveComponent.live_action Twig functionTestLiveComponent::call() to add files to the requestmodifier option in LiveProp so options can be modified at runtime.loading attribute to defer the rendering on the component after the
page is rendered, either when the page loads (loading="defer") or when
the component becomes visible in the viewport (loading="lazy").defer attribute.UrlMapping configuration object for URL bindings in LiveComponentsLiveComponents is now stable and no longer experimental 🥳
[BC BREAK] The data-action-name attribute behavior was removed in favor of
using Stimulus "action parameters" and data-live-action-param. This is a
breaking change if you were using the data-action-name attribute directly
in your templates. #1418
To upgrade your application, follow these changes:
<button
data-action="live#action"
- data-action-name="debounce(300)|save"
+ data-live-action-param="debounce(300)|save"
>Save</button>
To pass arguments to an action, also use the Stimulus "action parameters" syntax:
<button
data-action="live#action"
- data-action-name="addItem(id={{ item.id }}, itemName=CustomItem)"
+ data-live-action-param="addItem"
+ data-live-id-param="{{ item.id }}"
+ data-live-item-name-param="CustomItem"
>Add Item</button>
Additionally, the prevent modifier (e.g. prevent|save) was removed. Replace
this with the standard Stimulus :prevent action option:
<button
- data-action="live#action
+ data-action="live#action:prevent"
- data-action-name="prevent|save"
+ data-live-action-param="save"
>Save</button>
[BC BREAK] The data-event attribute was removed in favor of using Stimulus
"action parameters": rename data-event to data-live-event-param. Additionally,
if you were passing arguments to the event name, use action parameter attributes
for those as well - e.g. data-live-foo-param="bar". #1418
Reverted setting ignoreActiveValue: true in Idiomorph #1548
New placeholder macro to generate defer/lazy skeleton #1532
improve TestLiveComponent::actingAs() #1461
Drop Twig 2 support #1436
Add better error message when hydrating dates #1431
Store TemplateMap in build_dir #1525
data-live-id attribute was changed to id #1484morphdom to idiomorph. As this is
a different morphing library, there may be some edge cases where the
morphing behavior is different.LivePropLiveListener attributes on a single method#[AsLiveComponent(method: 'get')]urlReferenceType parameter to AsLiveComponent, which allows to
generate different type URL (e.g. absolute) for the component Ajax callssymfony/serializer dependency is now optionaldata-skip-morph attribute to allow skipping morphing of an element
(the element's attributes will be morphed, but its inner HTML will be overwritten
instead of morphed)package.json file so that [@symfony](https://github.com/symfony)/ux-live-component
will appear in the user's importmap.php file if using AssetMapper. This
will allow using the JavaScript from the package without extra setup.{% embed %} with {% block %} in <twig:> componentsdata-loading not working when on root element of a componentclass attributes contained a space at start or endtype: module"type: module.LiveProp.emit() method of TestLiveComponent to properly test events.actionAs() to TestLiveComponent.onUpdated() hook for LiveProp.data-turbo="false" when handling redirects.Your component's live "data" is now send over Ajax as a JSON string.
Previously data was sent as pure query parameters or as pure POST data.
However, this made it impossible to keep certain data types, like
distinguishing between null and ''. This has no impact on end-users.
Added data-live-ignore attribute. If included in an element, that element
will not be updated on re-render.
ComponentWithFormTrait no longer has a setForm() method. But there
is also no need to call it anymore. To pass an already-built form to
your component, pass it as a form var to component(). If you have
a custom mount(), you no longer need to call setForm() or anything else.
The Live Component AJAX endpoints now return HTML in all situations instead of JSON.
Ability to send live action arguments to backend
[BC BREAK] Remove init_live_component() twig function, use {{ attributes }} instead:
- <div {{ init_live_component() }}>
+ <div {{ attributes }}>
[BC BREAK] Replace property hydration system with symfony/serializer normalizers. This
is a BC break if you've created custom hydrators. They'll need to be converted to
normalizers.
[BC BREAK] Rename BeforeReRender attribute to PreReRender.
Support for stimulus version 2 was removed and support for [@hotwired](https://github.com/hotwired)/stimulus
version 3 was added. See the [@symfony/stimulus-bridge CHANGELOG](https://github.com/symfony/stimulus-bridge/blob/main/CHANGELOG.md#300)
for more details.
Require live components have a default action (__invoke() by default) to enable
controller annotations/attributes (ie [@Security](https://github.com/Security)/[@Cache](https://github.com/Cache)). Added DefaultActionTrait
helper.
When a model is updated, a new live:update-model event is dispatched. Parent
components (in a parent-child component setup) listen to this and automatically
try to update any model with a matching name. A data-model-map was also added
to map child component model names to a parent - see #113.
Child components are now re-rendered if the parent components passes new data to the child when rendering - see #113.
Minimum PHP version was bumped to 8.0 so that PHP 8 attributes could be used.
The LiveComponentInterface was dropped and replaced by the AsLiveComponent attribute,
which extends the new AsTwigComponent from the TwigComponent library. All
other annotations (e.g. [@LiveProp](https://github.com/LiveProp) and [@LiveAction](https://github.com/LiveAction)) were also replaced by
PHP 8 attributes.
Before:
use App\Entity\Notification;
use App\Repository\NotificationRepository;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\LiveComponentInterface;
final class NotificationComponent implements LiveComponentInterface
{
private NotificationRepository $repo;
/** [@LiveProp](https://github.com/LiveProp) */
public bool $expanded = false;
public function __construct(NotificationRepository $repo)
{
$this->repo = $repo;
}
/** [@LiveAction](https://github.com/LiveAction) */
public function toggle(): void
{
$this->expanded = !$this->expanded;
}
public function getNotifications(): array
{
return $this->repo->findAll();
}
public static function getComponentName(): string
{
return 'notification';
}
}
After:
use App\Entity\Notification;
use App\Repository\NotificationRepository;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent('notification')]
final class NotificationComponent
{
private NotificationRepository $repo;
#[LiveProp]
public bool $expanded = false;
public function __construct(NotificationRepository $repo)
{
$this->repo = $repo;
}
#[LiveAction]
public function toggle(): void
{
$this->expanded = !$this->expanded;
}
public function getNotifications(): array
{
return $this->repo->findAll();
}
}
How can I help you explore Laravel packages today?