Repository: symfony/ux
Pull request #2843 (2025-06-16 15:07:49)

[Doc] Update the docs about the mount() method of Twig components
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Docs?         | yes
| Issues        | -
| License       | MIT

Today I had some issues understanding how the `mount()` method works, so I propose these changes to clarify things.

javiereguiluz: Thanks for the review Hugo! I reworded some parts. Hopefully it's better now.

Patch:
diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml
index 4ce75fba..08e83634 100644
--- a/.github/workflows/code-quality.yaml
+++ b/.github/workflows/code-quality.yaml
@@ -46,18 +46,17 @@ jobs:
         strategy:
             fail-fast: false
             matrix:
-                php-version: ['8.1']
+                php-version: [ '8.1', '8.2', '8.3', '8.4']
                 dependency-version: ['']
                 symfony-version: ['']
                 minimum-stability: ['stable']
                 include:
-                    # dev packages (probably not needed to have multiple such jobs)
-                    - minimum-stability: 'dev'
-                      php-version: 8.4
                     # lowest deps
-                    - dependency-version: 'lowest'
+                    -   php-version: '8.1'
+                        dependency-version: 'lowest'
                     # LTS version of Symfony
-                    - symfony-version: '6.4.*'
+                    -   php-version: '8.1'
+                        symfony-version: '6.4.*'
         steps:
             -   name: Checkout
                 uses: actions/checkout@v4
@@ -79,7 +78,7 @@ jobs:
             -   name: Setup PHP
                 uses: shivammathur/setup-php@v2
                 with:
-                    php-version: 8.1
+                    php-version: ${{ matrix.php-version }}
                     tools: flex
 
             - name: Install root dependencies
diff --git a/src/Icons/doc/index.rst b/src/Icons/doc/index.rst
index 3c9e42bd..49f440eb 100644
--- a/src/Icons/doc/index.rst
+++ b/src/Icons/doc/index.rst
@@ -17,7 +17,7 @@ Installation
     $ composer require symfony/ux-icons
 
     # To use provided on-demand icon sets, you also need HTTP client:
-    $ composer require symfony/http-client
+    $ composer require symfony/http-client    
 
 SVG Icons
 ---------
@@ -77,38 +77,6 @@ define the HTML attributes added to the ``<svg>`` element:
     {{ ux_icon('user-profile', {height: '16px', width: '16px', 'aria-hidden': true}) }}
     {# renders <svg height="16" width="16" aria-hidden="true"> ... </svg> #}
 
-Icon Sizes
-~~~~~~~~~~
-
-.. note::
-
-    ``<svg>`` elements will be dynamically sized by the browser.
-    For icons, we therefore recommend to explicitly set the size.
-
-To align icons naturally with surrounding text and inherit font sizing, use ``em``
-units. This works well for buttons, links, or inline text. Defining the height alone
- is sufficient—the width will scale proportionally:
-
-.. code-block:: html+twig
-
-    {# Twig function #}
-    {{ ux_icon('user-profile', {style: 'height: 1em;'}) }}
-
-    {# HTML syntax #}
-    <twig:ux:icon name="profile" style="height: 1em;" />
-
-If your project uses a CSS framework like Tailwind or Bootstrap, prefer their
-sizing utilities for consistency and theming:
-
-.. code-block:: html+twig
-
-    {# Twig function #}
-    {{ ux_icon('bi:chat', {class: 'size-4'}) }}
-
-    {# HTML syntax #}
-    <twig:ux:icon name="bi:chat" class="size-4" />
-
-To keep your design consistent and easily adjustable, consider defining a :ref:default attribute <icons_default_attributes>. This allows you to control the size of all your icons from a single place.
 
 Icon Sets
 ~~~~~~~~~
@@ -360,8 +328,6 @@ HTML Syntax
 
     ``symfony/ux-twig-component`` is required to use the HTML syntax.
 
-.. _icons_default_attributes:
-
 Default Attributes
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/src/TwigComponent/doc/index.rst b/src/TwigComponent/doc/index.rst
index 1161f385..59b2be4d 100644
--- a/src/TwigComponent/doc/index.rst
+++ b/src/TwigComponent/doc/index.rst
@@ -447,15 +447,24 @@ need to populate, you can render it with:
 Mounting Data
 -------------
 
-Most of the time, you will create public properties and then pass values
-to those as "props" when rendering. But there are several hooks in case
-you need to do something more complex.
+Most of the time, you will create public properties and pass values to them
+as "props" when rendering the component. However, there are several hooks
+available when you need to perform more complex logic.
 
 The mount() Method
 ~~~~~~~~~~~~~~~~~~
 
-For more control over how your "props" are handled, you can create a method
-called ``mount()``::
+The ``mount()`` method gives you more control over how your "props" are handled.
+It is called once, immediately after the component is instantiated, **but before**
+the component system assigns the props you passed when rendering.
+
+For example, if you call your component like this:
+
+.. code-block:: html+twig
+
+    <twig:Alert type="error" message="..."/>
+
+The following code won't work as expected::
 
     // src/Twig/Components/Alert.php
     // ...
@@ -466,30 +475,60 @@ called ``mount()``::
         public string $message;
         public string $type = 'success';
 
-        public function mount(bool $isSuccess = true): void
+        public function mount(): void
         {
-            $this->type = $isSuccess ? 'success' : 'danger';
+            {# ❌ this won't work: at this point $type still has its default value.
+                   Passed values are not yet available in props. #}
+            if ('error' === $this->type) {
+                // ...
+            }
         }
 
         // ...
     }
 
-The ``mount()`` method is called just one time: immediately after your
-component is instantiated. Because the method has an ``$isSuccess``
-argument, if we pass an ``isSuccess`` prop when rendering, it will be
-passed to ``mount()``.
+Inside ``mount()``, each prop has only its *default* value (or ``null`` if it is
+untyped and has no default). If you need a prop's value, declare a parameter in
+``mount()`` whose name matches the prop instead of reading the public property::
+
+    public function mount(string $type): void
+    {
+        {# ✅ this works as expected: the $type argument in PHP has the value
+               passed to the 'type' prop in the Twig template #}
+        if ('error' === $type) {
+            // ...
+        }
+    }
+
+If a prop name (e.g. ``type``) matches an argument name in ``mount()``,
+its value will be passed only to the method. The component system **will not**
+set it on a public property or use it in the component's ``attributes``.
+
+``mount()`` can also receive props **even when no matching public property
+exists**. For example, pass an ``isError`` prop instead of ``type``:
 
 .. code-block:: html+twig
 
-    <twig:Alert
-        isSuccess="{{ false }}"
-        message="Danger Will Robinson!"
-    />
+    <twig:Alert isError="{{ true }}" message="..."/>
 
-If a prop name (e.g. ``isSuccess``) matches an argument name in ``mount()``,
-the prop will be passed as that argument and the component system will
-**not** try to set it directly on a property or use it for the component
-``attributes``.
+Define a ``$isError`` argument to capture the prop and initialize other
+properties using that value::
+
+    #[AsTwigComponent]
+    class Alert
+    {
+        public string $message;
+        public string $type = 'success';
+
+        public function mount(bool $isError = false): void
+        {
+            if ($isError) {
+                $this->type = 'danger';
+            }
+        }
+
+        // ...
+    }
 
 PreMount Hook
 ~~~~~~~~~~~~~
@@ -738,8 +777,6 @@ There is also a non-HTML syntax that can be used:
         {% block footer %}... footer content{% endblock %}
     {% endcomponent %}
 
-.. _embedded-components-context:
-
 Context / Variables Inside of Blocks
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -803,7 +840,10 @@ When overriding the ``alert_message`` block, you have access to the ``message``
         </twig:block>
     </twig:SuccessAlert>
 
-.. _embedded-components-outerScope:
+.. versionadded:: 2.13
+
+    The ability to refer to the scope of higher components via the ``outerScope``
+    variable was added in 2.13.
 
 As mentioned before, variables from lower components are merged with those from
 upper components. When you need access to some properties or functions from higher
@@ -824,11 +864,6 @@ components, that can be done via the ``outerScope...`` variable:
         {{ outerScope.this.someProp }} {# references a "someProp" prop from SuccessAlert #}
     {% endcomponent %}
 
-.. versionadded:: 2.13
-
-    The ability to refer to the scope of higher components via the ``outerScope``
-    variable was added in 2.13.
-
 You can keep referring to components higher up as well. Just add another ``outerScope``.
 Remember though that the ``outerScope`` reference only starts once you're INSIDE the (embedded) component.
 
@@ -846,8 +881,6 @@ Remember though that the ``outerScope`` reference only starts once you're INSIDE
         {% endblock %}
     {% endcomponent %}
 
-.. _embedded-components-outerBlocks:
-
 Inheritance & Forwarding "Outer Blocks"
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1569,33 +1602,9 @@ listen to ``PreMountEvent`` or ``PostMountEvent``.
 Nested Components
 -----------------
 
-It's possible to include components inside another component:
-
-.. code-block:: html+twig
-
-    <twig:SomeComponent>
-        {% for i in 1..10 %}
-            <twig:AnotherComponent>
-                {{ i }}
-            </twig:AnotherComponent>
-        {% endfor %}
-    </twig:SomeComponent>
-
-When you do this, each component is rendered independently, but there are some
-important caveats:
-
-#. As :ref:`explained above <embedded-components-context>`, the variables of a
-   component are merged with the variables of its parent component. Variables
-   with the same name (e.g. ``this`` or ``computed``) are overridden by the
-   inner component;
-#. The original parent component variables are available via the special
-   :ref:`outerScope variable <embedded-components-outerScope>`, so you can, for
-   example, call ``outerScope.this.someFunction()``;
-#. The contents of blocks defined in parent components are available via the
-   special :ref:`outerBlocks variable <embedded-components-outerBlocks>`, so you
-   can call ``block(outerBlocks.someBlockName)``.
-
-Finally, you cannot mix the Twig syntax and the :ref:`HTML syntax <component_html_syntax>`
+It's totally possible to include components as part of the contents of another
+component. When you do this, all components render independently. The only
+caveat is that you cannot mix the Twig syntax and the :ref:`HTML syntax <component_html_syntax>`
 when using nested components:
 
 .. code-block:: html+twig
@@ -1629,9 +1638,8 @@ when using nested components:
         {% endblock %}
     {% endcomponent %}
 
-If you're using `Live Components`_, there *are* additional guidelines related
-to how parent and child components are re-rendered. See `Live Nested Components`_
-for details.
+If you're using `Live Components`_, then there *are* some guidelines related to
+how the re-rendering of parent and child components works. Read `Live Nested Components`_.
 
 Configuration
 -------------


Test patch:
diff --git a/src/Autocomplete/tests/Fixtures/Kernel.php b/src/Autocomplete/tests/Fixtures/Kernel.php
index 7fc8adcf..de2877b0 100644
--- a/src/Autocomplete/tests/Fixtures/Kernel.php
+++ b/src/Autocomplete/tests/Fixtures/Kernel.php
@@ -11,7 +11,6 @@
 
 namespace Symfony\UX\Autocomplete\Tests\Fixtures;
 
-use Composer\InstalledVersions;
 use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
 use Doctrine\ORM\Mapping\AssociationMapping;
 use Fixtures\Form\CategoryWithCallbackAsCustomValue;
@@ -105,7 +104,7 @@ final class Kernel extends BaseKernel
             'auto_refresh_proxies' => false,
         ]);
 
-        $doctrineConfig = [
+        $config = [
             'dbal' => ['url' => '%env(resolve:DATABASE_URL)%'],
             'orm' => [
                 'auto_generate_proxy_classes' => true,
@@ -123,17 +122,11 @@ final class Kernel extends BaseKernel
         ];
         if (class_exists(AssociationMapping::class)) {
             // Doctrine ORM >= 3.0
-            $doctrineConfig['orm']['controller_resolver'] = [
+            $config['orm']['controller_resolver'] = [
                 'auto_mapping' => true,
             ];
         }
-        if (null !== $doctrineBundleVersion = InstalledVersions::getVersion('doctrine/doctrine-bundle')) {
-            if (\PHP_VERSION_ID >= 80400 && version_compare($doctrineBundleVersion, '2.15.0', '>=')) {
-                $doctrineConfig['orm']['enable_native_lazy_objects'] = true;
-            }
-        }
-
-        $c->extension('doctrine', $doctrineConfig);
+        $c->extension('doctrine', $config);
 
         $c->extension('security', [
             'password_hashers' => [
diff --git a/src/LiveComponent/tests/Fixtures/Kernel.php b/src/LiveComponent/tests/Fixtures/Kernel.php
index 4b0cd90d..508dd242 100644
--- a/src/LiveComponent/tests/Fixtures/Kernel.php
+++ b/src/LiveComponent/tests/Fixtures/Kernel.php
@@ -186,9 +186,6 @@ final class Kernel extends BaseKernel
             if (version_compare($doctrineBundleVersion, '2.12.0', '>=')) {
                 $doctrineConfig['orm']['controller_resolver']['auto_mapping'] = false;
             }
-            if (\PHP_VERSION_ID >= 80400 && version_compare($doctrineBundleVersion, '2.15.0', '>=')) {
-                $doctrineConfig['orm']['enable_native_lazy_objects'] = true;
-            }
         }
 
         $c->extension('doctrine', $doctrineConfig);