This update simplifies the tsdown configuration, we do not need our custom plugin to minify CSS anymore (replaced by `css.minify = true`), and same for our hooks that rename the built CSS (replaced by `css.fileName`) 😍
This PR was merged into the 2.x branch.
Discussion
----------
[LiveComponent] Allow empty values to bypass model validation modifiers
| Q | A
| -------------- | ---
| Bug fix? | yes
| New feature? |no
| Deprecations? | no
| Documentation? | no
| Issues | Fix#3153
| License | MIT
**The Problem:**
Currently, when using input validation modifiers like `min_length(3)` on a LiveComponent model (e.g., `<input type="search" data-model="min_length(3)|query" />`), the modifier acts as a strict gatekeeper. If a user types "abc", the model updates correctly. However, if the user clears the input (e.g., by clicking the native "X" clear button on a search input or deleting the text), the update is blocked because the length is `0`.
This creates a frustrating UX: the input appears empty on the screen, but the component fails to re-render or notify the server, leaving the page (and backend state) stuck with the previous search results.
**The Solution:**
This PR updates `LiveController` to bypass `min_length`, `max_length`, `min_value`, and `max_value` checks if the input value is completely empty (`''`, `null`, or `undefined`).
**Why this is the correct approach (The HTML5 Standard):**
This change aligns LiveComponent's frontend validation strictly with standard HTML5 form validation behavior. In HTML5, attributes like `minlength` and `min` do **not** apply to empty values. If a field is allowed to be empty, it passes validation. If a field *must not* be empty, developers are expected to use the `required` attribute.
By allowing empty strings to pass through these modifiers:
1. It restores the expected behavior for search/filter inputs, allowing the component to naturally reset to its initial/unfiltered state when cleared.
2. It solves complex state synchronization issues between parent/child components when passing models via `props:props`.
3. It prevents numeric inputs (`min_value`) from casting an empty string to `0` and incorrectly failing validation.
### **Demo**

Commits
-------
79205ce63de fix-input-validation-modifiers
This PR was squashed before being merged into the 2.x branch.
Discussion
----------
[LiveComponent][TwigComponent] Fix reflection issues for private properties from trait and parent class
| Q | A
| -------------- | ---
| Bug fix? | yes
| New feature? | no
| Deprecations? | no
| Documentation? | no
| Issues | <!-- none yet -->
| License | MIT
## Problem
When a component class *extends* another class that uses a trait with `#[ExposeInTemplate]` or `#[LiveProp]` attributes on **private** properties, those attributes are invisible to the metadata scanners.
PHP's `ReflectionClass::getProperties()` called on a child class does not return private properties from traits used in *ancestor* classes — they are only returned when `getProperties()` is called on the exact class that declares `use TraitName`. This is standard PHP reflection behaviour.
This affects any inheritance pattern where a base component uses `ComponentWithFormTrait` (or any custom trait with private `#[ExposeInTemplate]` properties) and app-level components extend it without re-declaring the trait.
**Symptom in `ux-twig-component`:** `Variable "form" does not exist` in the child component's Twig template.
**Symptom in `ux-live-component`:** `#[LiveProp(fieldName: 'callable()')]` declared in a trait used by a parent class is not registered with the correct `fieldName` on the child component, causing frontend model key mismatches.
## Fix
### `ux-twig-component` — `ComponentProperties::loadClassMetadata()`
Replace the single `$refClass->getProperties()` call with a loop that walks up `getParentClass()`, calling `getProperties()` at each level. Results are deduplicated by property name (child-class declaration takes priority), giving a complete view of all `#[ExposeInTemplate]` properties across the full hierarchy.
### `ux-live-component` — `LiveComponentMetadataFactory::createPropMetadatas()`
Pass `$property->getDeclaringClass()->getName()` instead of `$class->getName()` to `createLivePropMetadata()`. When a property is declared in a trait used by a parent class, the type extractor must be given the declaring class — not the leaf child class — to resolve the type correctly.
`#[LiveAction]` and `#[LiveListener]` on parent class methods are **not** affected: `getMethods()` already walks the full inheritance chain, so those are discovered correctly on child components.
## Tests
Added ``@group` trait-inheritance` tests in both packages using a minimal fixture: a trait with a private `#[ExposeInTemplate]` property / `#[LiveProp(fieldName: callable)]`, a parent component that uses the trait, and a child component that extends the parent without re-declaring the trait.
Commits
-------
799736ff76c [LiveComponent][TwigComponent] Fix reflection issues for private properties from trait and parent class
This PR was squashed before being merged into the 2.x branch.
Discussion
----------
Allow components use dynamically templates
| Q | A
| -------------- | ---
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Documentation? | yes
| Issues | -
| License | MIT
### Description
This PR introduces the ability to dynamically resolve component templates via a dedicated method in the component class. This is particularly useful for building responsive components or multi-theme applications where the same logic (PHP class) needs to render different templates based on its internal state or props.
Instead of relying on a "magic" method, this implementation uses a new `FromMethod` attribute passed to the `template` parameter. This approach ensures explicit intent and maintains compatibility with Symfony's service tag requirements.
**How it works**
- Define a public method in your component that returns the template path as a string.
- `Pass a new FromMethod('yourMethodName')` to the `#[AsTwigComponent]` or `#[AsLiveComponent]` attribute.
**TwigComponent Example**
```php
#[AsTwigComponent('user_profile', template: new FromMethod('getTemplateForRole'))]
class UserProfile
{
public function getTemplateForRole(): string
{
if ($this->isGranted('ROLE_ADMIN')) {
return 'components/user_profile/admin_dashboard.html.twig';
}
return 'components/user_profile/public_card.html.twig';
}
}
```
**LiveComponent Example**
```php
#[AsLiveComponent('checkout_wizard', template: new FromMethod('getStepTemplate'))]
class CheckoutWizard
{
#[LiveProp]
public int $currentStep = 1;
#[LiveAction]
public function nextStep(): void
{
$this->currentStep++;
}
public function getStepTemplate(): string
{
return match ($this->currentStep) {
1 => 'components/checkout/step_1_address.html.twig',
2 => 'components/checkout/step_2_shipping.html.twig',
3 => 'components/checkout/step_3_payment.html.twig',
default => 'components/checkout/summary.html.twig',
};
}
}
```
<details>
<summary>First PR</summary>
### Description
When building responsive or multi-theme applications, developers frequently need to reuse the exact same component logic (PHP class) with entirely different Twig templates depending on the context (e.g., a Desktop Search Dropdown vs. a Mobile Search Offcanvas).
Currently, the only ways to achieve this are:
1. **Duplicating the component class** just to change the `#[AsLiveComponent(template: '...')]` attribute.
2. **Creating a global `PreRenderEvent` listener**, which breaks component encapsulation and litters the codebase with boilerplate listeners.
This PR solves this by introducing a magic `getTemplate(): string` method. If a component defines this public method, the `ComponentRenderer` will use its return value as the template, overriding the default attribute metadata. This aligns perfectly with existing UX magic methods like `mount()` and `hydrate()`.
**Additional Note:** P.S. As this relies on the underlying ComponentRenderer, the getTemplate() method also works out-of-the-box for LiveComponents. It might be worth mentioning this in the LiveComponent documentation as well.
### Example Usage
Instead of duplicating components, developers can now encapsulate the logic gracefully:
```php
#[AsLiveComponent('SearchForm')]
class SearchForm extends AbstractController
{
#[LiveProp]
public string $theme = 'desktop';
// The component dynamically decides its own template
public function getTemplate(): string
{
return $this->theme === 'mobile'
? 'components/MobileSearch.html.twig'
: 'components/DesktopSearch.html.twig';
}
}
{# Renders the mobile template #}
{{ component('SearchForm', { theme: 'mobile' }) }}
</details>
Commits
-------
e161632b30d Allow components use dynamically templates
tsup is deprecated in favor of tsdown.
Follow https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, and many (local) tries were I was not really happy with the files generated by tsdown/rolldown, we are finally having something extra good!
We have the benefits from https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, but without their drawbacks. The code correctly follow the `es2022` target and does not do anything weird anymore with static properties (needed by controllers).
I (Claude) added a plugin to remove the region and JSDoc comments (except if they contain `@deprecated``), since I think we want to keep the code non-minified.
This PR was merged into the 2.x branch.
Discussion
----------
[LiveComponent] Add option `fetchCredentials`
| Q | A |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Bug fix? | no |
| New feature? | yes <!-- please update src/**/CHANGELOG.md files --> |
| Deprecations? | no <!-- if yes, also update UPGRADE-*.md and src/**/CHANGELOG.md --> |
| Documentation? | yes <!-- required for new features, or documentation updates --> |
| Issues | Fix#3322 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> |
| License | MIT |
<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.
Additionally (see https://symfony.com/releases):
- Always add tests and ensure they pass.
- For new features, provide some code snippets to help understand usage.
- Features and deprecations must be submitted against branch main.
- Update/add documentation as required (we can help!)
- Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
- Never break backward compatibility (see https://symfony.com/bc).
-->
Commits
-------
0a09f45e3a9 [LiveComponent] Add option `fetchCredentials`
Support for Enum cases in the configuration was added in symfony/config:6.3
1655d1763b
Error: Object of class Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode could not be converted to string
/home/runner/work/ux/ux/src/LiveComponent/vendor/symfony/config/Definition/Builder/EnumNodeDefinition.php:30
/home/runner/work/ux/ux/src/LiveComponent/vendor/zenstruck/foundry/src/ZenstruckFoundryBundle.php:152
zenstruck/foundry removed the requirement for symfony/framework-bundle in version 5.0.
0b09c20e79
Its bundle requires Symfony\Component\HttpKernel\Bundle\AbstractBundle that was introduced in symfony/http-kernel:6.1
7e8cf5df10
This commit fixes the build to lowest dependencies
Add a note that explains that the re-rendering loading states is a different concept from the components initial loading state regarding placeholders, with a link to the related "loading content" section in this doc
This PR was merged into the 2.x branch.
Discussion
----------
[LiveComponent] Add dispatch browser event assertion in InteractsWithLiveComponents
| Q | A
| -------------- | ---
| Bug fix? | no
| New feature? | yes <!-- please update src/**/CHANGELOG.md files -->
| Deprecations? | no <!-- if yes, also update UPGRADE-*.md and src/**/CHANGELOG.md -->
| Documentation? | yes <!-- required for new features, or documentation updates -->
| Issues | no <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License | MIT
Next to this PR https://github.com/symfony/ux/pull/2712 I wanted to add test assertion on dispatchBrowserEvent and not only on emited event.
I reuse same logic as the `ComponentWithEmit.php`.
```
$this->assertComponentDispatchEvent($testComponent->render(), 'browserEvent')
->withPayload(['amount' => 2, 'unit' => 'kg'])
->withPayloadSubset(['amount' => 2]) ;
```
`$this->assertComponentNotDispatchEvent($testComponent->render(), 'otherBrowserEvent');`
Commits
-------
5943c89f4ea [LiveComponent] Add dispatch browser event test
This PR was merged into the 2.x branch.
Discussion
----------
Add doc for E2E steps + minor modifications
| Q | A
| ------------- | ---
| Bug fix? | no
| New feature? | no
| Docs? | yes
| Issues | Fix#3039
| License | MIT
Commits
-------
ceefa1d8501 [Docs] Add doc for E2E steps + minor modifications