diff --git a/README.md b/README.md index 5bacce20043..df51ed2f29e 100644 --- a/README.md +++ b/README.md @@ -9,55 +9,20 @@ Symfony UX is an initiative and set of libraries to seamlessly integrate JavaScript tools into your application. For example, want to render a chart with [Chart.js](https://www.chartjs.org/)? Use -[UX Chart.js](https://github.com/symfony/ux-chartjs) +[UX Chart.js](https://symfony.com/bundles/ux-chartjs/current/index.html) to build the chart in PHP. The JavaScript is handled for you automatically. **That's Symfony UX.** -[Read all the details about the Symfony UX initiative](https://symfony.com/ux) - -Or watch the [Stimulus Screencast on SymfonyCasts](https://symfonycasts.com/screencast/stimulus). - -## Components of UX - Symfony UX leverages [Stimulus](https://stimulus.hotwired.dev/) for JavaScript and the [Stimulus Bridge](https://github.com/symfony/stimulus-bridge) for integrating it into [Webpack Encore](https://github.com/symfony/webpack-encore). -## Packages - -- [UX Chart.js](https://github.com/symfony/ux-chartjs): - [Chart.js](https://www.chartjs.org/) chart library integration for Symfony -- [UX Cropper.js](https://github.com/symfony/ux-cropperjs): - [Cropper.js](https://fengyuanchen.github.io/cropperjs/) image cropper library integration for Symfony -- [UX Dropzone](https://github.com/symfony/ux-dropzone): - File input drag-and-drop zones for Symfony Forms -- [UX LazyImage](https://github.com/symfony/ux-lazy-image): - Improve image loading performances through lazy-loading and data-uri thumbnails -- [UX Swup](https://github.com/symfony/ux-swup): - [Swup](https://swup.js.org/) page transition library integration for Symfony -- [UX Turbo](https://github.com/symfony/ux-turbo): [Hotwire Turbo](https://turbo.hotwired.dev/) library integration for Symfony -- [Twig Component](https://github.com/symfony/ux-twig-component): - A system to build reusable "components" with Twig -- [Live Component](https://github.com/symfony/ux-live-component): - Gives Twig Components a URL and a JavaScript library to automatically re-render via Ajax as your user interacts with it - -## Stimulus Tools around the World - -Because Stimulus is used by developers outside of Symfony, many tools -exist beyond the UX packages: - -- [stimulus-use](https://github.com/stimulus-use/stimulus-use): Add composable - behaviors to your Stimulus controllers, like [debouncing](https://stimulus-use.github.io/stimulus-use/#/use-debounce), - [detecting outside clicks](https://stimulus-use.github.io/stimulus-use/#/use-click-outside) - and many other things. See: https://stimulus-use.github.io/stimulus-use/#/ +## Resources -- [stimulus-components](https://stimulus-components.netlify.app/): A - large number of pre-made Stimulus controllers, like for - [Copying to clipboard](https://stimulus-components.netlify.app/docs/components/stimulus-clipboard/), - [Sortable](https://stimulus-components.netlify.app/docs/components/stimulus-sortable/), - [Popover](https://stimulus-components.netlify.app/docs/components/stimulus-popover/) (similar to tooltips) - and much more. +- [Install Symfony UX](https://symfony.com/doc/current/frontend/ux.html). +- [List of UX Packages](https://symfony.com/doc/current/frontend/ux.html#ux-packages-list). +- Watch the [Stimulus Screencast on SymfonyCasts](https://symfonycasts.com/screencast/stimulus). ## Let's build an amazing ecosystem together @@ -68,7 +33,7 @@ fields of your Symfony forms? Or the ability to make the `EntityType` render wit [AJAX auto-completion](https://tarekraafat.github.io/autoComplete.js)? Anything you do in JavaScript could be done streamlined as a UX package. -We have some ideas and we will release more packages in the coming days. The rest +We have some ideas, and we will release more packages in the coming days. The rest is on you: let's create an amazing ecosystem together! ## Contributing diff --git a/src/Chartjs/.symfony.bundle.yaml b/src/Chartjs/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Chartjs/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Chartjs/README.md b/src/Chartjs/README.md index a21a2257d3a..167f55c995a 100644 --- a/src/Chartjs/README.md +++ b/src/Chartjs/README.md @@ -3,161 +3,12 @@ Symfony UX Chart.js is a Symfony bundle integrating the [Chart.js](https://www.chartjs.org/) library in Symfony applications. It is part of [the Symfony UX initiative](https://symfony.com/ux). -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX Chart.js requires PHP 7.2+ and Symfony 4.4+. +## Resources -Install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-chartjs - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -Also make sure you have at least version 3.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge) -in your `package.json` file. - -## Usage - -To use Symfony UX Chart.js, inject the `ChartBuilderInterface` service and -create charts in PHP: - -```php -// ... -use Symfony\UX\Chartjs\Builder\ChartBuilderInterface; -use Symfony\UX\Chartjs\Model\Chart; - -class HomeController extends AbstractController -{ - #[Route('/', name: 'app_homepage')] - public function index(ChartBuilderInterface $chartBuilder): Response - { - $chart = $chartBuilder->createChart(Chart::TYPE_LINE); - - $chart->setData([ - 'labels' => ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - 'datasets' => [ - [ - 'label' => 'My First dataset', - 'backgroundColor' => 'rgb(255, 99, 132)', - 'borderColor' => 'rgb(255, 99, 132)', - 'data' => [0, 10, 5, 2, 20, 30, 45], - ], - ], - ]); - - $chart->setOptions([ - 'scales' => [ - 'y' => [ - 'suggestedMin' => 0, - 'suggestedMax' => 100, - ], - ], - ]); - - return $this->render('home/index.html.twig', [ - 'chart' => $chart, - ]); - } -} -``` - -All options and data are provided as-is to Chart.js. You can read -[Chart.js documentation](https://www.chartjs.org/docs/latest/) to discover them all. - -Once created in PHP, a chart can be displayed using Twig if installed -(requires [Symfony Webpack Encore](https://symfony.com/doc/current/frontend/encore/installation.html)): - -```twig -{{ render_chart(chart) }} - -{# You can pass HTML attributes as a second argument to add them on the tag #} -{{ render_chart(chart, {'class': 'my-chart'}) }} -``` - -### Extend the default behavior - -Symfony UX Chart.js allows you to extend its default behavior using a custom Stimulus controller: - -```js -// mychart_controller.js - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - this.element.addEventListener('chartjs:pre-connect', this._onPreConnect); - this.element.addEventListener('chartjs:connect', this._onConnect); - } - - disconnect() { - // You should always remove listeners when the controller is disconnected to avoid side effects - this.element.removeEventListener('chartjs:pre-connect', this._onPreConnect); - this.element.removeEventListener('chartjs:connect', this._onConnect); - } - - _onPreConnect(event) { - // The chart is not yet created - console.log(event.detail.options); // You can access the chart options using the event details - - // For instance you can format Y axis - event.detail.options.scales = { - yAxes: [ - { - ticks: { - callback: function (value, index, values) { - /* ... */ - }, - }, - }, - ], - }; - } - - _onConnect(event) { - // The chart was just created - console.log(event.detail.chart); // You can access the chart instance using the event details - - // For instance you can listen to additional events - event.detail.chart.options.onHover = (mouseEvent) => { - /* ... */ - }; - event.detail.chart.options.onClick = (mouseEvent) => { - /* ... */ - }; - } -} -``` - -Then in your render call, add your controller as an HTML attribute: - -```twig -{{ render_chart(chart, {'data-controller': 'mychart'}) }} -``` - -## Backward Compatibility promise - -This bundle aims at following the same Backward Compatibility promise as the Symfony framework: -[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) - -However it is currently considered -[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), -meaning it is not bound to Symfony's BC policy for the moment. - -## Run tests - -### PHP tests - -```sh -php vendor/bin/phpunit -``` - -### JavaScript tests - -```sh -cd Resources/assets -yarn test -``` +- [Documentation](https://symfony.com/bundles/ux-chartjs/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Chartjs/Resources/doc/index.rst b/src/Chartjs/Resources/doc/index.rst new file mode 100644 index 00000000000..957e3a3c28b --- /dev/null +++ b/src/Chartjs/Resources/doc/index.rst @@ -0,0 +1,160 @@ +Symfony UX Chart.js +=================== + +Symfony UX Chart.js is a Symfony bundle integrating the +`Chart.js`_ library in Symfony applications. +It is part of `the Symfony UX initiative`_. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then, install this bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-chartjs + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +Also make sure you have at least version 3.0 of `@symfony/stimulus-bridge`_ +in your ``package.json`` file. + +Usage +----- + +To use Symfony UX Chart.js, inject the ``ChartBuilderInterface`` service +and create charts in PHP:: + + // ... + use Symfony\UX\Chartjs\Builder\ChartBuilderInterface; + use Symfony\UX\Chartjs\Model\Chart; + + class HomeController extends AbstractController + { + #[Route('/', name: 'app_homepage')] + public function index(ChartBuilderInterface $chartBuilder): Response + { + $chart = $chartBuilder->createChart(Chart::TYPE_LINE); + + $chart->setData([ + 'labels' => ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + 'datasets' => [ + [ + 'label' => 'My First dataset', + 'backgroundColor' => 'rgb(255, 99, 132)', + 'borderColor' => 'rgb(255, 99, 132)', + 'data' => [0, 10, 5, 2, 20, 30, 45], + ], + ], + ]); + + $chart->setOptions([ + 'scales' => [ + 'y' => [ + 'suggestedMin' => 0, + 'suggestedMax' => 100, + ], + ], + ]); + + return $this->render('home/index.html.twig', [ + 'chart' => $chart, + ]); + } + } + +All options and data are provided as-is to Chart.js. You can read +`Chart.js documentation`_ to discover them all. + +Once created in PHP, a chart can be displayed using Twig if installed +(requires `Symfony Webpack Encore`_): + +.. code-block:: twig + + {{ render_chart(chart) }} + + {# You can pass HTML attributes as a second argument to add them on the tag #} + {{ render_chart(chart, {'class': 'my-chart'}) }} + +Extend the default behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX Chart.js allows you to extend its default behavior using a +custom Stimulus controller: + +.. code-block:: javascript + + // mychart_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + connect() { + this.element.addEventListener('chartjs:pre-connect', this._onPreConnect); + this.element.addEventListener('chartjs:connect', this._onConnect); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side effects + this.element.removeEventListener('chartjs:pre-connect', this._onPreConnect); + this.element.removeEventListener('chartjs:connect', this._onConnect); + } + + _onPreConnect(event) { + // The chart is not yet created + console.log(event.detail.options); // You can access the chart options using the event details + + // For instance you can format Y axis + event.detail.options.scales = { + yAxes: [ + { + ticks: { + callback: function (value, index, values) { + /* ... */ + }, + }, + }, + ], + }; + } + + _onConnect(event) { + // The chart was just created + console.log(event.detail.chart); // You can access the chart instance using the event details + + // For instance you can listen to additional events + event.detail.chart.options.onHover = (mouseEvent) => { + /* ... */ + }; + event.detail.chart.options.onClick = (mouseEvent) => { + /* ... */ + }; + } + } + +Then in your render call, add your controller as an HTML attribute: + +.. code-block:: twig + + {{ render_chart(chart, {'data-controller': 'mychart'}) }} + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: https://symfony.com/doc/current/contributing/code/bc.html. + +However it is currently considered `experimental`, meaning it is not +bound to Symfony's BC policy for the moment. + +.. _`Chart.js`: https://www.chartjs.org +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge +.. _`Chart.js documentation`: https://www.chartjs.org/docs/latest/ +.. _`Symfony Webpack Encore`: https://symfony.com/doc/current/frontend/encore/installation.html +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/Cropperjs/.symfony.bundle.yaml b/src/Cropperjs/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Cropperjs/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Cropperjs/README.md b/src/Cropperjs/README.md index 544ba1d18f6..dafdef22ec9 100644 --- a/src/Cropperjs/README.md +++ b/src/Cropperjs/README.md @@ -3,158 +3,12 @@ Symfony UX Cropper.js is a Symfony bundle integrating the [Cropper.js](https://fengyuanchen.github.io/cropperjs/) library in Symfony applications. It is part of [the Symfony UX initiative](https://symfony.com/ux). -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX Cropper.js requires PHP 7.2+ and Symfony 4.4+. +## Resources -Install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-cropperjs - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -Also make sure you have at least version 3.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge) -in your `package.json` file. - -## Usage - -To use Symfony UX Cropper.js, inject the `CropperInterface` service, -create a Crop object, and use this object inside a standard form: - -```php -// ... -use Symfony\Component\HttpFoundation\Request; -use Symfony\UX\Cropperjs\Form\CropperType; -use Symfony\UX\Cropperjs\Factory\CropperInterface; - -class HomeController extends AbstractController -{ - /** - * #[Route('/', name: 'app_homepage')] - */ - public function index(CropperInterface $cropper, Request $request): Response - { - $crop = $cropper->createCrop('/server/path/to/the/image.jpg'); - $crop->setCroppedMaxSize(2000, 1500); - - $form = $this->createFormBuilder(['crop' => $crop]) - ->add('crop', CropperType::class, [ - 'public_url' => '/public/url/to/the/image.jpg', - 'cropper_options' => [ - 'aspectRatio' => 2000 / 1500, - ], - ]) - ->getForm() - ; - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - // Get the cropped image data (as a string) - $crop->getCroppedImage(); - - // Create a thumbnail of the cropped image (as a string) - $crop->getCroppedThumbnail(200, 150); - - // ... - } - - return $this->render('home/index.html.twig', [ - 'form' => $form->createView(), - ]); - } -} -``` - -These `cropper_options` can be any [the Cropper.js option](https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options). - -Once created in PHP, a crop form is a normal form, meaning you can display it using Twig -as you would normally with any form: - -```twig -{{ form(form) }} -``` - -### Extend the default behavior - -Symfony UX Cropper.js allows you to extend its default behavior using a custom Stimulus controller: - -```js -// mycropper_controller.js - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - this.element.addEventListener('cropperjs:pre-connect', this._onPreConnect); - this.element.addEventListener('cropperjs:connect', this._onConnect); - } - - disconnect() { - // You should always remove listeners when the controller is disconnected to avoid side effects - this.element.removeEventListener('cropperjs:pre-connect', this._onConnect); - this.element.removeEventListener('cropperjs:connect', this._onConnect); - } - - _onPreConnect(event) { - // The cropper has not yet been created and options can be modified - console.log(event.detail.options); - console.log(event.detail.img); - } - - _onConnect(event) { - // The cropper was just created and you can access details from the event - console.log(event.detail.cropper); - console.log(event.detail.options); - console.log(event.detail.img); - - // For instance you can listen to additional events - event.detail.img.addEventListener('cropend', function () { - // ... - }); - } -} -``` - -Then in your form, add your controller as an HTML attribute: - -```php -$form = $this->createFormBuilder(['crop' => $crop]) - ->add('crop', CropperType::class, [ - 'public_url' => '/public/url/to/the/image.jpg', - 'cropper_options' => [ - 'aspectRatio' => 2000 / 1800, - ], - 'attr' => ['data-controller' => 'mycropper'], - ]) - ->getForm() -; -``` - -## Backward Compatibility promise - -This bundle aims at following the same Backward Compatibility promise as the Symfony framework: -[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) - -However it is currently considered -[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), -meaning it is not bound to Symfony's BC policy for the moment. - -## Run tests - -### PHP tests - -```sh -php vendor/bin/phpunit -``` - -### JavaScript tests - -```sh -cd Resources/assets -yarn test -``` +- [Documentation](https://symfony.com/bundles/ux-cropperjs/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Cropperjs/Resources/doc/index.rst b/src/Cropperjs/Resources/doc/index.rst new file mode 100644 index 00000000000..7253e5a97ab --- /dev/null +++ b/src/Cropperjs/Resources/doc/index.rst @@ -0,0 +1,155 @@ +Symfony UX Cropper.js +===================== + +Symfony UX Cropper.js is a Symfony bundle integrating the +`Cropper.js`_ library in Symfony applications. It is part of +`the Symfony UX initiative`_. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then, install this bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-cropperjs + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +Also make sure you have at least version 3.0 of +`@symfony/stimulus-bridge`_ in your ``package.json`` file. + +Usage +----- + +To use Symfony UX Cropper.js, inject the ``CropperInterface`` service, +create a Crop object, and use this object inside a standard form:: + + // ... + use Symfony\Component\HttpFoundation\Request; + use Symfony\UX\Cropperjs\Form\CropperType; + use Symfony\UX\Cropperjs\Factory\CropperInterface; + + class HomeController extends AbstractController + { + /** + * #[Route('/', name: 'app_homepage')] + */ + public function index(CropperInterface $cropper, Request $request): Response + { + $crop = $cropper->createCrop('/server/path/to/the/image.jpg'); + $crop->setCroppedMaxSize(2000, 1500); + + $form = $this->createFormBuilder(['crop' => $crop]) + ->add('crop', CropperType::class, [ + 'public_url' => '/public/url/to/the/image.jpg', + 'cropper_options' => [ + 'aspectRatio' => 2000 / 1500, + ], + ]) + ->getForm() + ; + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Get the cropped image data (as a string) + $crop->getCroppedImage(); + + // Create a thumbnail of the cropped image (as a string) + $crop->getCroppedThumbnail(200, 150); + + // ... + } + + return $this->render('home/index.html.twig', [ + 'form' => $form->createView(), + ]); + } + } + +These ``cropper_options`` can be any `the Cropper.js options`_. + +Once created in PHP, a crop form is a normal form, meaning you can +display it using Twig as you would normally with any form: + +.. code-block:: twig + + {{ form(form) }} + +Extend the default behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX Cropper.js allows you to extend its default behavior using a +custom Stimulus controller: + +.. code-block:: javascript + + // mycropper_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + connect() { + this.element.addEventListener('cropperjs:pre-connect', this._onPreConnect); + this.element.addEventListener('cropperjs:connect', this._onConnect); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side effects + this.element.removeEventListener('cropperjs:pre-connect', this._onConnect); + this.element.removeEventListener('cropperjs:connect', this._onConnect); + } + + _onPreConnect(event) { + // The cropper has not yet been created and options can be modified + console.log(event.detail.options); + console.log(event.detail.img); + } + + _onConnect(event) { + // The cropper was just created and you can access details from the event + console.log(event.detail.cropper); + console.log(event.detail.options); + console.log(event.detail.img); + + // For instance you can listen to additional events + event.detail.img.addEventListener('cropend', function () { + // ... + }); + } + } + +Then in your form, add your controller as an HTML attribute:: + + $form = $this->createFormBuilder(['crop' => $crop]) + ->add('crop', CropperType::class, [ + 'public_url' => '/public/url/to/the/image.jpg', + 'cropper_options' => [ + 'aspectRatio' => 2000 / 1800, + ], + 'attr' => ['data-controller' => 'mycropper'], + ]) + ->getForm() + ; + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, +meaning it is not bound to Symfony’s BC policy for the moment. + +.. _`Cropper.js`: https://fengyuanchen.github.io/cropperjs/ +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge +.. _`the Cropper.js options`: https://github.com/fengyuanchen/cropperjs/blob/main/README.md#options +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/Dropzone/.symfony.bundle.yaml b/src/Dropzone/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Dropzone/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Dropzone/README.md b/src/Dropzone/README.md index 615d618402b..1332fe323bd 100644 --- a/src/Dropzone/README.md +++ b/src/Dropzone/README.md @@ -6,158 +6,12 @@ in Symfony Forms. It is part of [the Symfony UX initiative](https://symfony.com/ It allows visitors to drag and drop files into a container instead of having to browse their computer for a file. -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX Dropzone requires PHP 7.2+ and Symfony 4.4+. +## Resources -Install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-dropzone - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -Also make sure you have at least version 3.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge) -in your `package.json` file. - -## Usage - -The most common usage of Symfony UX Dropzone is to use it as a drop-in replacement of -the native FileType class: - -```php -// ... -use Symfony\UX\Dropzone\Form\DropzoneType; - -class CommentFormType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - // ... - ->add('photo', DropzoneType::class) - // ... - ; - } - - // ... -} -``` - -### Customizing the design - -Symfony UX Dropzone provides a default stylesheet in order to ease usage. You can -disable it to add your own design if you wish. - -In `assets/controllers.json`, disable the default stylesheet by switching -the `@symfony/ux-dropzone/src/style.css` autoimport to `false`: - -```json -{ - "controllers": { - "@symfony/ux-dropzone": { - "dropzone": { - "enabled": true, - "fetch": "eager", - "autoimport": { - "@symfony/ux-dropzone/src/style.css": false - } - } - } - }, - "entrypoints": [] -} -``` - -> _Note_: you should put the value to `false` and not remove the line so that Symfony Flex -> won't try to add the line again in the future. - -Once done, the default stylesheet won't be used anymore and you can implement your own CSS on -top of the Dropzone. - -### Extend the default behavior - -Symfony UX Dropzone allows you to extend its default behavior using a custom Stimulus controller: - -```js -// mydropzone_controller.js - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - this.element.addEventListener('dropzone:connect', this._onConnect); - this.element.addEventListener('dropzone:change', this._onChange); - this.element.addEventListener('dropzone:clear', this._onClear); - } - - disconnect() { - // You should always remove listeners when the controller is disconnected to avoid side-effects - this.element.removeEventListener('dropzone:connect', this._onConnect); - this.element.removeEventListener('dropzone:change', this._onChange); - this.element.removeEventListener('dropzone:clear', this._onClear); - } - - _onConnect(event) { - // The dropzone was just created - } - - _onChange(event) { - // The dropzone just changed - } - - _onClear(event) { - // The dropzone has just been cleared - } -} -``` - -Then in your form, add your controller as an HTML attribute: - -```php -// ... -use Symfony\UX\Dropzone\Form\DropzoneType; - -class CommentFormType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - // ... - ->add('photo', DropzoneType::class, [ - 'attr' => ['data-controller' => 'mydropzone'], - ]) - // ... - ; - } - - // ... -} -``` - -## Backward Compatibility promise - -This bundle aims at following the same Backward Compatibility promise as the Symfony framework: -[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) - -However it is currently considered -[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), -meaning it is not bound to Symfony's BC policy for the moment. - -## Run tests - -### PHP tests - -```sh -php vendor/bin/phpunit -``` - -### JavaScript tests - -```sh -cd Resources/assets -yarn test -``` +- [Documentation](https://symfony.com/bundles/ux-dropzone/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Dropzone/Resources/doc/index.rst b/src/Dropzone/Resources/doc/index.rst new file mode 100644 index 00000000000..0dffdfc1b58 --- /dev/null +++ b/src/Dropzone/Resources/doc/index.rst @@ -0,0 +1,158 @@ +Symfony UX Dropzone +=================== + +Symfony UX Dropzone is a Symfony bundle providing light dropzones for +file inputs in Symfony Forms. It is part of `the Symfony UX initiative`_. + +It allows visitors to drag and drop files into a container instead of +having to browse their computer for a file. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then, install this bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-dropzone + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +Also make sure you have at least version 3.0 of +`@symfony/stimulus-bridge`_ in your ``package.json`` file. + +Usage +----- + +The most common usage of Symfony UX Dropzone is to use it as a drop-in +replacement of the native FileType class:: + + // ... + use Symfony\UX\Dropzone\Form\DropzoneType; + + class CommentFormType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('photo', DropzoneType::class) + // ... + ; + } + + // ... + } + +Customizing the design +~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX Dropzone provides a default stylesheet in order to ease +usage. You can disable it to add your own design if you wish. + +In ``assets/controllers.json``, disable the default stylesheet by +switching the ``@symfony/ux-dropzone/src/style.css`` autoimport to +``false``: + +.. code-block:: json + + { + "controllers": { + "@symfony/ux-dropzone": { + "dropzone": { + "enabled": true, + "fetch": "eager", + "autoimport": { + "@symfony/ux-dropzone/src/style.css": false + } + } + } + }, + "entrypoints": [] + } + +.. note:: + *Note*: you should put the value to ``false`` and not remove the line + so that Symfony Flex won’t try to add the line again in the future. + +Once done, the default stylesheet won’t be used anymore and you can +implement your own CSS on top of the Dropzone. + +Extend the default behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX Dropzone allows you to extend its default behavior using a +custom Stimulus controller: + +.. code-block:: javascript + + // mydropzone_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + connect() { + this.element.addEventListener('dropzone:connect', this._onConnect); + this.element.addEventListener('dropzone:change', this._onChange); + this.element.addEventListener('dropzone:clear', this._onClear); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side-effects + this.element.removeEventListener('dropzone:connect', this._onConnect); + this.element.removeEventListener('dropzone:change', this._onChange); + this.element.removeEventListener('dropzone:clear', this._onClear); + } + + _onConnect(event) { + // The dropzone was just created + } + + _onChange(event) { + // The dropzone just changed + } + + _onClear(event) { + // The dropzone has just been cleared + } + } + +Then in your form, add your controller as an HTML attribute:: + + // ... + use Symfony\UX\Dropzone\Form\DropzoneType; + + class CommentFormType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('photo', DropzoneType::class, [ + 'attr' => ['data-controller' => 'mydropzone'], + ]) + // ... + ; + } + + // ... + } + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, meaning it is not +bound to Symfony’s BC policy for the moment. + +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/LazyImage/.symfony.bundle.yaml b/src/LazyImage/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/LazyImage/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/LazyImage/README.md b/src/LazyImage/README.md index 85b7581257b..5d1cb3c85d0 100644 --- a/src/LazyImage/README.md +++ b/src/LazyImage/README.md @@ -8,165 +8,12 @@ It provides two key features: - a Stimulus controller to load lazily heavy images, with a placeholder - a [BlurHash implementation](https://blurha.sh/) to create data-uri thumbnails for images -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX LazyImage requires PHP 7.2+ and Symfony 4.4+. +## Resources -You can install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-lazy-image - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -Also make sure you have at least version 3.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge) -in your `package.json` file. - -## Usage - -The default usage of Symfony UX LazyImage is to use its Stimulus controller to first load -a small placeholder image that will then be replaced by the high-definition version once the -page has been rendered: - -```twig - -``` - -With this setup, the user will initially see `images/small.png`. Then, -once the page has loaded and the user's browser has downloaded the larger -image, the `src` attribute will change to `image/large.png`. - -There is also support for the `srcset` attribute by passing an -`srcset` value to the controller: - -```twig - -``` - -**Note** The `stimulus_controller()` function comes from -[WebpackEncoreBundle v1.10](https://github.com/symfony/webpack-encore-bundle). - -Instead of using a generated thumbnail that would exist on your filesystem, you can use -the BlurHash algorithm to create a light, blurred, data-uri thumbnail of the image: - -```twig - -``` - -The `data_uri_thumbnail` function receives 3 arguments: - -- the server path to the image to generate the data-uri thumbnail for ; -- the width of the BlurHash to generate -- the height of the BlurHash to generate - -You should try to generate small BlurHash images as generating the image can be CPU-intensive. -Instead, you can rely on the browser scaling abilities by generating a small image and using the -`width` and `height` HTML attributes to scale up the image. - -### Extend the default behavior - -Symfony UX LazyImage allows you to extend its default behavior using a custom Stimulus controller: - -```js -// mylazyimage_controller.js - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - this.element.addEventListener('lazy-image:connect', this._onConnect); - this.element.addEventListener('lazy-image:ready', this._onReady); - } - - disconnect() { - // You should always remove listeners when the controller is disconnected to avoid side-effects - this.element.removeEventListener('lazy-image:connect', this._onConnect); - this.element.removeEventListener('lazy-image:ready', this._onReady); - } - - _onConnect(event) { - // The lazy-image behavior just started - } - - _onReady(event) { - // The HD version has just been loaded - } -} -``` - -Then in your template, add your controller to the HTML attribute: - -```twig - -``` - -> **Note**: be careful to add your controller **before** the LazyImage controller so that -> it is executed before and can listen on the `lazy-image:connect` event properly. - -## Backward Compatibility promise - -This bundle aims at following the same Backward Compatibility promise as the Symfony framework: -[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) - -However it is currently considered -[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), -meaning it is not bound to Symfony's BC policy for the moment. - -## Run tests - -### PHP tests - -```sh -php vendor/bin/phpunit -``` - -### JavaScript tests - -```sh -cd Resources/assets -yarn test -``` +- [Documentation](https://symfony.com/bundles/ux-lazy-image/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/LazyImage/Resources/doc/index.rst b/src/LazyImage/Resources/doc/index.rst new file mode 100644 index 00000000000..08627e1f44c --- /dev/null +++ b/src/LazyImage/Resources/doc/index.rst @@ -0,0 +1,175 @@ +Symfony UX LazyImage +==================== + +Symfony UX LazyImage is a Symfony bundle providing utilities to improve +image loading performance. It is part of `the Symfony UX initiative`_. + +It provides two key features: + +- a Stimulus controller to load lazily heavy images, with a placeholder +- a `BlurHash implementation`_ to create data-uri thumbnails for images + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then install this bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-lazy-image + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +Also make sure you have at least version 3.0 of +`@symfony/stimulus-bridge`_ in your ``package.json`` file. + +Usage +----- + +The default usage of Symfony UX LazyImage is to use its Stimulus +controller to first load a small placeholder image that will then be +replaced by the high-definition version once the page has been rendered: + +.. code-block:: twig + + + +With this setup, the user will initially see ``images/small.png``. Then, +once the page has loaded and the user’s browser has downloaded the +larger image, the ``src`` attribute will change to ``image/large.png``. + +There is also support for the ``srcset`` attribute by passing an +``srcset`` value to the controller: + +.. code-block:: twig + + + +**Note** The ``stimulus_controller()`` function comes from +`WebpackEncoreBundle v1.10`_. + +Instead of using a generated thumbnail that would exist on your +filesystem, you can use the BlurHash algorithm to create a light, +blurred, data-uri thumbnail of the image: + +.. code-block:: twig + + + +The ``data_uri_thumbnail`` function receives 3 arguments: + +- the server path to the image to generate the data-uri thumbnail for ; +- the width of the BlurHash to generate +- the height of the BlurHash to generate + +You should try to generate small BlurHash images as generating the image +can be CPU-intensive. Instead, you can rely on the browser scaling +abilities by generating a small image and using the ``width`` and +``height`` HTML attributes to scale up the image. + +Extend the default behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX LazyImage allows you to extend its default behavior using a +custom Stimulus controller: + +.. code-block:: javascript + + // mylazyimage_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + connect() { + this.element.addEventListener('lazy-image:connect', this._onConnect); + this.element.addEventListener('lazy-image:ready', this._onReady); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side-effects + this.element.removeEventListener('lazy-image:connect', this._onConnect); + this.element.removeEventListener('lazy-image:ready', this._onReady); + } + + _onConnect(event) { + // The lazy-image behavior just started + } + + _onReady(event) { + // The HD version has just been loaded + } + } + +Then in your template, add your controller to the HTML attribute: + +.. code-block:: twig + + + +.. + + **Note**: be careful to add your controller **before** the LazyImage + controller so that it is executed before and can listen on the + ``lazy-image:connect`` event properly. + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, +meaning it is not bound to Symfony’s BC policy for the moment. + +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge +.. _`BlurHash implementation`: https://blurha.sh +.. _`WebpackEncoreBundle v1.10`: https://github.com/symfony/webpack-encore-bundle +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/LiveComponent/.symfony.bundle.yaml b/src/LiveComponent/.symfony.bundle.yaml new file mode 100644 index 00000000000..17001f559e7 --- /dev/null +++ b/src/LiveComponent/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "src/Resources/doc" diff --git a/src/LiveComponent/README.md b/src/LiveComponent/README.md index 4ba9605b3c6..7916dbcac50 100644 --- a/src/LiveComponent/README.md +++ b/src/LiveComponent/README.md @@ -3,1450 +3,20 @@ **EXPERIMENTAL** This component is currently experimental and is likely to change, or even change drastically. -Live components work with the [TwigComponent](https://github.com/symfony/ux-twig-component) +Live components work with the [TwigComponent](https://symfony.com/bundles/ux-twig-component/current/index.html) library to give you the power to automatically update your Twig components on the frontend as the user interacts with them. Inspired by [Livewire](https://laravel-livewire.com/) and [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html). -A real-time product search component might look like this: - -```php -// src/Components/ProductSearchComponent.php -namespace App\Components; - -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\DefaultActionTrait; - -#[AsLiveComponent('product_search')] -class ProductSearchComponent -{ - use DefaultActionTrait; - - public string $query = ''; - - private ProductRepository $productRepository; - - public function __construct(ProductRepository $productRepository) - { - $this->productRepository = $productRepository; - } - - public function getProducts(): array - { - // example method that returns an array of Products - return $this->productRepository->search($this->query); - } -} -``` - -```twig -{# templates/components/product_search.html.twig #} -
- - -
    - {% for product in this.products %} -
  • {{ product.name }}
  • - {% endfor %} -
-
-``` - -As a user types into the box, the component will automatically -re-render and show the new results! - Want a demo? Check out https://github.com/weaverryan/live-demo. -## Installation - -Let's get started! Install the library with: - -``` -composer require symfony/ux-live-component -``` - -This comes with an embedded JavaScript Stimulus controller. Unlike -other Symfony UX packages, this needs to be enabled manually -in your `assets/bootstrap.js` file: - -```js -// assets/bootstrap.js -import LiveController from '@symfony/ux-live-component'; -import '@symfony/ux-live-component/styles/live.css'; -// ... - -app.register('live', LiveController); -``` - -Finally, reinstall your Node dependencies and restart Encore: - -``` -yarn install --force -yarn encore dev -``` - -Oh, and just one more step! Import a routing file from the bundle: - -```yaml -# config/routes.yaml -live_component: - resource: '@LiveComponentBundle/Resources/config/routing/live_component.xml' -``` - -That's it! We're ready! - -## Making your Component "Live" - -If you haven't already, check out the [Twig Component](https://github.com/symfony/ux-twig-component) -documentation to get the basics of Twig components. - -Suppose you've already built a basic Twig component: - -```php -// src/Components/RandomNumberComponent.php -namespace App\Components; - -use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; - -#[AsTwigComponent('random_number')] -class RandomNumberComponent -{ - public function getRandomNumber(): int - { - return rand(0, 1000); - } -} -``` - -```twig -{# templates/components/random_number.html.twig #} -
- {{ this.randomNumber }} -
-``` - -To transform this into a "live" component (i.e. one that -can be re-rendered live on the frontend), replace the -component's `AsTwigComponent` attribute with `AsLiveComponent` -and add the `DefaultActionTrait`: - -```diff -// src/Components/RandomNumberComponent.php - --use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; -+use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -+use Symfony\UX\LiveComponent\DefaultActionTrait; - --#[AsTwigComponent('random_number')] -+#[AsLiveComponent('random_number')] -class RandomNumberComponent -{ -+ use DefaultActionTrait; -} -``` - -Then, in the template, make sure there is _one_ HTML element around -your entire component and use the `{{ init_live_component() }}` function -to initialize the Stimulus controller: - -```diff --
-+
- {{ this.randomNumber }} -
-``` - -Your component is now a live component... except that we haven't added -anything that would cause the component to update. Let's start simple, -by adding a button that - when clicked - will re-render the component -and give the user a new random number: - -```twig -
- {{ this.randomNumber }} - - -
-``` - -That's it! When you click the button, an Ajax call will be made to -get a fresh copy of our component. That HTML will replace the current -HTML. In other words, you just generated a new random number! That's -cool, but let's keep going because... things get cooler. - -## LiveProps: Stateful Component Properties - -Let's make our component more flexible by adding `$min` and `$max` properties: - -```php -// src/Components/RandomNumberComponent.php -namespace App\Components; - -// ... -use Symfony\UX\LiveComponent\Attribute\LiveProp; - -#[AsLiveComponent('random_number')] -class RandomNumberComponent -{ - #[LiveProp] - public int $min = 0; - - #[LiveProp] - public int $max = 1000; - - public function getRandomNumber(): int - { - return rand($this->min, $this->max); - } - - // ... -} -``` - -With this change, we can control the `$min` and `$max` properties -when rendering the component: - -``` -{{ component('random_number', { min: 5, max: 500 }) }} -``` - -But what's up with those `LiveProp` attributes? A property with -the `LiveProp` attribute becomes a "stateful" property for this -component. In other words, each time we click the "Generate a -new number!" button, when the component re-renders, it will -_remember_ the original values for the `$min` and `$max` properties -and generate a random number between 5 and 500. If you forgot to -add `LiveProp`, when the component re-rendered, those two values -would _not_ be set on the object. - -In short: LiveProps are "stateful properties": they will always -be set when rendering. Most properties will be LiveProps, with -common exceptions being properties that hold services (these don't -need to be stateful because they will be autowired each time before -the component is rendered) and -[properties used for computed properties](https://github.com/symfony/ux-twig-component/blob/main/README.md#computed-properties). - -## data-action="live#update": Re-rendering on LiveProp Change - -Could we allow the user to _choose_ the `$min` and `$max` values -and automatically re-render the component when they do? Definitely! -And _that_ is where live components really shine. - -Let's add two inputs to our template: - -```twig -{# templates/components/random_number.html.twig #} -
- - - - - Generating a number between {{ this.min }} and {{ this.max }} - {{ this.randomNumber }} -
-``` - -Notice the `data-action="live#update"` on each `input`. When the -user types, live components reads the `data-model` attribute (e.g. `min`) -and re-renders the component using the _new_ value for that field! Yes, -as you type in a box, the component automatically updates to reflect the -new number! - -Well, actually, we're missing one step. By default, a `LiveProp` is -"read only". For security purposes, a user cannot change the value of -a `LiveProp` and re-render the component unless you allow it with -the `writable=true` option: - -```diff -// src/Components/RandomNumberComponent.php -// ... - -class RandomNumberComponent -{ -- #[LiveProp] -+ #[LiveProp(writable: true)] - public int $min = 0; - -- #[LiveProp] -+ #[LiveProp(writable: true)] - public int $max = 1000; - - // ... -} -``` - -Now it works: as you type into the `min` or `max` boxes, the component -will re-render with a new random number between that range! - -### Debouncing - -If the user types 5 characters really quickly into an `input`, we -don't want to send 5 Ajax requests. Fortunately, the `live#update` -method has built-in debouncing: it waits for a 150ms pause before -sending an Ajax request to re-render. This is built in, so you -don't need to think about it. - -### Lazy Updating on "blur" or "change" of a Field - -Sometimes, you might want a field to re-render only after the user -has changed an input _and_ moved to another field. Browsers dispatch -a `change` event in this situation. To re-render when this event -happens, add it to the `data-action` call: - -```diff - -``` - -The `data-action="change->live#update"` syntax is standard Stimulus -syntax, which says: - -> When the "change" event occurs, call the `update` method on the -> `live` controller. - -### Deferring a Re-Render Until Later - -Other times, you might want to update the internal value of a property, -but wait until later to re-render the component (e.g. until a button -is clicked). To do that, use the `updateDefer` method: - -```diff - -``` - -Now, as you type, the `max` "model" will be updated in JavaScript, but -it won't, yet, make an Ajax call to re-render the component. Whenever -the next re-render _does_ happen, the updated `max` value will be used. - -### Using name="" instead of data-model - -Instead of communicating the property name of a field via `data-model`, -you can communicate it via the standard `name` property. The following -code works identically to the previous example: - -```diff -
- - - // ... -
-``` - -If an element has _both_ `data-model` and `name` attributes, the -`data-model` attribute takes precedence. - -### Using data-value="" for non input elements - -If you want to set the value of a model with an element that is not an input element and does not have a `value` attribute you can use `data-value` - -```twig -
- - - Set min to 10 - - - // ... -
-``` - -If an element has _both_ `data-value` and `value` attributes, the -`data-value` attribute takes precedence. - -## Loading States - -Often, you'll want to show (or hide) an element while a component is -re-rendering or an [action](#actions) is processing. For example: - -```twig - -Loading - - -Loading -``` - -Or, to _hide_ an element while the component is loading: - -```twig - -Saved! -``` - -### Adding and Removing Classes or Attributes - -Instead of hiding or showing an entire element, you could -add or remove a class: - -```twig - -
...
- - -
...
- - -
...
-``` - -Sometimes you may want to add or remove an attribute when loading. -That can be accomplished with `addAttribute` or `removeAttribute`: - -```twig - -
...
-``` - -You can also combine any number of directives by separating them -with a space: - -```twig -
...
-``` - -Finally, you can add the `delay` modifier to not trigger the loading -changes until loading has taken longer than a certain amount of time: - -```twig - -
...
- - -
Loading
- - -
Loading
-``` - -## Actions - -Live components require a single "default action" that is -used to re-render it. By default, this is an empty `__invoke()` -method and can be added with the `DefaultActionTrait`. -Live components are actually Symfony controllers so you -can add the normal controller attributes/annotations (ie -`@Cache`/`@Security`) to either the entire class just a -single action. - -You can also trigger custom actions on your component. Let's -pretend we want to add a "Reset Min/Max" button to our "random -number" component that, when clicked, sets the min/max numbers -back to a default value. - -First, add a method with a `LiveAction` attribute above it that -does the work: - -```php -// src/Components/RandomNumberComponent.php -namespace App\Components; - -// ... -use Symfony\UX\LiveComponent\Attribute\LiveAction; - -class RandomNumberComponent -{ - // ... - - #[LiveAction] - public function resetMinMax() - { - $this->min = 0; - $this->max = 1000; - } - - // ... -} -``` - -To call this, add `data-action="live#action"` and `data-action-name` -to an element (e.g. a button or form): - -```twig - -``` - -Done! When the user clicks this button, a POST request will be sent -that will trigger the `resetMinMax()` method! After calling that method, -the component will re-render like normal, using the new `$min` and `$max` -properties! - -You can also add several "modifiers" to the action: - -```twig -
- -
-``` - -The `prevent` modifier would prevent the form from submitting -(`event.preventDefault()`). The `debounce(300)` modifier will -add 300ms of "debouncing" before the action is executed. In -other words, if you click really fast 5 times, only one Ajax -request will be made! - -#### Actions & Services - -One really neat thing about component actions is that they are -_real_ Symfony controllers. Internally, they are processed -identically to a normal controller method that you would create -with a route. - -This means that, for example, you can use action autowiring: - -```php -// src/Components/RandomNumberComponent.php -namespace App\Components; - -// ... -use Psr\Log\LoggerInterface; - -class RandomNumberComponent -{ - // ... - - #[LiveAction] - public function resetMinMax(LoggerInterface $logger) - { - $this->min = 0; - $this->max = 1000; - $logger->debug('The min/max were reset!'); - } - - // ... -} -``` - -### Actions and CSRF Protection - -When you trigger an action, a POST request is sent that contains -a `X-CSRF-TOKEN` header. This header is automatically populated -and validated. In other words... you get CSRF protection without -any work. - -Your only job is to make sure that the CSRF component is installed: - -``` -composer require symfony/security-csrf -``` - -### Actions, Redirecting and AbstractController - -Sometimes, you may want to redirect after an action is executed -(e.g. your action saves a form and then you want to redirect to -another page). You can do that by returning a `RedirectResponse` -from your action: - -```php -// src/Components/RandomNumberComponent.php -namespace App\Components; - -// ... -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - -class RandomNumberComponent extends AbstractController -{ - // ... - - #[LiveAction] - public function resetMinMax() - { - // ... - - $this->addFlash('success', 'Min/Max have been reset!'); - - return $this->redirectToRoute('app_random_number'); - } - - // ... -} -``` - -You probably noticed one interesting trick: to make redirecting easier, -the component now extends `AbstractController`! That is totally allowed, -and gives you access to all of your normal controller shortcuts. We -even added a flash message! - -## Forms - -A component can also help render a [Symfony form](https://symfony.com/doc/current/forms.html), -either the entire form (useful for automatic validation as you type) or just -one or some fields (e.g. a markdown preview for a `textarea` or -[dependent form fields](https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms)). - -### Rendering an Entire Form in a Component - -Suppose you have a `PostType` form class that's bound to a `Post` entity -and you'd like to render this in a component so that you can get instant -validation as the user types: - -```php -namespace App\Form; - -use App\Entity\Post; -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class PostType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('title') - ->add('slug') - ->add('content') - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'data_class' => Post::class, - ]); - } -} -``` - -Before you start thinking about the component, make sure that you -have your controller set up so you can handle the form submit. There's -nothing special about this controller: it's written however you normally -write your form controller logic: - -```php -namespace App\Controller; - -use App\Entity\Post; -use App\Form\PostType; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; - -class PostController extends AbstractController -{ - /** - * #[Route('/admin/post/{id}/edit', name: 'app_post_edit')] - */ - public function edit(Request $request, Post $post): Response - { - $form = $this->createForm(PostType::class, $post); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - $this->getDoctrine()->getManager()->flush(); - - return $this->redirectToRoute('app_post_index'); - } - - // renderForm() is new in Symfony 5.3. - // Use render() and call $form->createView() if on a lower version - return $this->renderForm('post/edit.html.twig', [ - 'post' => $post, - 'form' => $form, - ]); - } -} -``` - -Great! In the template, instead of rendering the form, let's render -a `post_form` component that we will create next: - -```twig -{# templates/post/edit.html.twig #} - -{% extends 'base.html.twig' %} - -{% block body %} -

Edit Post

- - {{ component('post_form', { - post: post, - form: form - }) }} -{% endblock %} -``` - -Ok: time to build that `post_form` component! The Live Components package -comes with a special trait - `ComponentWithFormTrait` - to make it easy to -deal with forms: - -```php -namespace App\Twig\Components; - -use App\Entity\Post; -use App\Form\PostType; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\Form\FormInterface; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\ComponentWithFormTrait; - -#[AsLiveComponent('post_form')] -class PostFormComponent extends AbstractController -{ - use ComponentWithFormTrait; - - /** - * The initial data used to create the form. - * - * Needed so the same form can be re-created - * when the component is re-rendered via Ajax. - * - * The `fieldName` option is needed in this situation because - * the form renders fields with names like `name="post[title]"`. - * We set `fieldName: ''` so that this live prop doesn't collide - * with that data. The value - initialFormData - could be anything. - */ - #[LiveProp(fieldName: 'initialFormData')] - public ?Post $post = null; - - /** - * Used to re-create the PostType form for re-rendering. - */ - protected function instantiateForm(): FormInterface - { - // we can extend AbstractController to get the normal shortcuts - return $this->createForm(PostType::class, $this->post); - } -} -``` - -The trait forces you to create an `instantiateForm()` method, -which is used when the component is rendered via AJAX. Notice that, -in order to recreate the _same_ form, we pass in the `Post` object -and set it as a `LiveProp`. - -The template for this component will render the form, which is -available as `this.form` thanks to the trait: - -```twig -{# templates/components/post_form.html.twig #} -
- {{ form_start(this.form) }} - {{ form_row(this.form.title) }} - {{ form_row(this.form.slug) }} - {{ form_row(this.form.content) }} - - - {{ form_end(this.form) }} -
-``` - -Mostly, this is a pretty boring template! It includes the normal -`init_live_component(this)` and then you render the form however you want. - -But the result is incredible! As you finish changing each field, the -component automatically re-renders - including showing any validation -errors for that field! Amazing! - -This is possible thanks to a few interesting pieces: - -- `data-action="change->live#update"`: instead of adding `data-action` - to _every_ field, you can place this on a parent element. Thanks to - this, as you change or type into fields (i.e. the `input` event), - the model for that field will update and the component will re-render. - -- The fields in our form do not have a `data-model=""` attribute. But - that's ok! When that is absent, the `name` attribute is used instead. - `ComponentWithFormTrait` has a modifiable `LiveProp` that captures - these and submits the form using them. That's right: each render time - the component re-renders, the form is _submitted_ using the values. - However, if a field has not been modified yet by the user, its - validation errors are cleared so that they aren't rendered. - -### Handling "Cannot dehydrate an unpersisted entity" Errors. - -If you're building a form to create a _new_ entity, then when you -render the component, you may be passing in a new, non-persisted entity. - -For example, imagine you create a `new Post()` in your controller, -pass this "not-yet-persisted" entity into your template as a `post` -variable and pass _that_ into your component: - -```twig -{{ component('post_form', { - post: post, - form: form -}) }} -``` - -If you do this, you'll likely see this error: - -> Cannot dehydrate an unpersisted entity (App\Entity\Post). If you -> want to allow this, add a dehydrateWith= option to LiveProp - -The problem is that the Live component system doesn't know how to -transform this object into something that can be sent to the frontend, -called "dehydration". If an entity has already been saved to the database, -its "id" is sent to the frontend. But if the entity hasn't been saved -yet, that's not possible. - -The solution is to pass `null` into your component instead of a -non-persisted entity object. If you need to, you can re-create -your `new Post()` inside of your component: - -```diff -{{ component('post_form', { -- post: post, -+ post: post.id ? post : null, - form: form -}) }} -``` - -### Form Rendering Problems - -For the most part, rendering a form inside a component works beautifully. -But there are a few situations when your form may not behave how you -want. - -**A) Text Boxes Removing Trailing Spaces** - -If you're re-rendering a field on the `input` event (that's the default -event on a field, which is fired each time you type in a text box), then -if you type a "space" and pause for a moment, the space will disappear! - -This is because Symfony text fields "trim spaces" automatically. When -your component re-renders, the space will disappear... as the user is typing! -To fix this, either re-render on the `change` event (which fires after -the text box loses focus) or set the `trim` option of your field to -`false`: - -```php -public function buildForm(FormBuilderInterface $builder, array $options) -{ - $builder - // ... - ->add('content', TextareaType::class, [ - 'trim' => false, - ]) - ; -} -``` - -**B) `PasswordType` loses the password on re-render** - -If you're using the `PasswordType`, when the component re-renders, -the input will become blank! That's because, by default, the -`PasswordType` does not re-fill the `` after -a submit. - -To fix this, set the `always_empty` option to `false` in your form: - -```php -public function buildForm(FormBuilderInterface $builder, array $options) -{ - $builder - // ... - ->add('plainPassword', PasswordType::class, [ - 'always_empty' => false, - ]) - ; -} -``` - -### Submitting the Form via an action() - -Notice that, while we _could_ add a `save()` [component action](#actions) -that handles the form submit through the component, we've chosen not -to do that so far. The reason is simple: by creating a normal route & -controller that handles the submit, our form continues to work without -JavaScript. - -However, you _can_ do this if you'd like. In that case, you wouldn't -need any form logic in your controller: - -```php -/** - * #[Route('/admin/post/{id}/edit', name: 'app_post_edit')] - */ -public function edit(Post $post): Response -{ - return $this->render('post/edit.html.twig', [ - 'post' => $post, - ]); -} -``` - -And you wouldn't pass any `form` into the component: - -```twig -{# templates/post/edit.html.twig #} - -

Edit Post

- -{{ component('post_form', { - post: post -}) }} -``` - -When you do _not_ pass a `form` into a component that uses `ComponentWithFormTrait`, -the form will be created for you automatically. Let's add the `save()` -action to the component: - -```php -// ... -use Doctrine\ORM\EntityManagerInterface; -use Symfony\UX\LiveComponent\Attribute\LiveAction; - -class PostFormComponent extends AbstractController -{ - // ... - - #[LiveAction] - public function save(EntityManagerInterface $entityManager) - { - // shortcut to submit the form with form values - // if any validation fails, an exception is thrown automatically - // and the component will be re-rendered with the form errors - $this->submitForm(); - - /** @var Post $post */ - $post = $this->getFormInstance()->getData(); - $entityManager->persist($post); - $entityManager->flush(); - - $this->addFlash('success', 'Post saved!'); - - return $this->redirectToRoute('app_post_show', [ - 'id' => $this->post->getId(), - ]); - } -} -``` - -Finally, tell the `form` element to use this action: - -``` -{# templates/components/post_form.html.twig #} -{# ... #} - -{{ form_start(this.form, { - attr: { - 'data-action': 'live#action', - 'data-action-name': 'prevent|save' - } -}) }} -``` - -Now, when the form is submitted, it will execute the `save()` method -via Ajax. If the form fails validation, it will re-render with the -errors. And if it's successful, it will redirect. - -**NOTE**: Make sure that each time the user changes a field, you -update the component's model. If you don't do this, when you trigger -the action, it will _not_ contain the form's data because the data -in the fields and the component's data will be out of sync. - -An easy way to accomplish this (explained more in the -[Forms](#forms) section above) is to add: - -```diff -
-``` - -## Modifying Embedded Properties with the "exposed" Option - -If your component will render a form, you don't need to use -the Symfony form component. Let's build an `EditPostComponent` -without a form. This will need one `LiveProp`: the `Post` object -that is being edited: - -```php -namespace App\Twig\Components; - -use App\Entity\Post; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; - -#[AsLiveComponent('edit_post')] -class EditPostComponent -{ - #[LiveProp] - public Post $post; -} -``` - -In the template, let's render an HTML form _and_ a "preview" area -where the user can see, as they type, what the post will look like -(including rendered the `content` through a Markdown filter from the -`twig/markdown-extra` library): - -``` -
- - - - -
-

{{ this.post.title }}

- {{ this.post.content|markdown_to_html }} -
-
-``` - -This is pretty straightforward, except for one thing: the `data-model` -attributes aren't targeting properties on the component class itself, -they're targeting _embedded_ properties within the `$post` property. - -Out-of-the-box, modifying embedded properties is _not_ allowed. However, -you can enable it via the `exposed` option: - -```diff -// ... - -class EditPostComponent -{ -- #[LiveProp] -+ #[LiveProp(exposed: ['title', 'content'])] - public Post $post; - - // ... -} -``` - -With this, both the `title` and the `content` properties of the -`$post` property _can_ be modified by the user. However, notice -that the `LiveProp` does _not_ have `writable=true`. This -means that while the `title` and `content` properties can be -changed, the `Post` object itself **cannot** be changed. In other -words, if the component was originally created with a Post -object with id=2, a bad user could _not_ make a request that -renders the component with id=3. Your component is protected from -someone changing to see the form for a different `Post` object, -unless you added `writable=true` to this property. - -### Validation (without a Form) - -**NOTE** If your component [contains a form](#forms), then validation -is built-in automatically. Follow those docs for more details. - -If you're building some sort of form _without_ using Symfony's form -component, you _can_ still validate your data. - -First use the `ValidatableComponentTrait` and add any constraints you need: - -```php -use App\Entity\User; -use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; -use Symfony\UX\LiveComponent\Attribute\LiveProp; -use Symfony\UX\LiveComponent\ValidatableComponentTrait; -use Symfony\Component\Validator\Constraints as Assert; - -#[AsLiveComponent('edit_user')] -class EditUserComponent -{ - use ValidatableComponentTrait; - - #[LiveProp(exposed: ['email', 'plainPassword'])] - #[Assert\Valid] - public User $user; - - #[LiveProp] - #[Assert\IsTrue] - public bool $agreeToTerms = false; -} -``` - -Be sure to add the `IsValid` attribute/annotation to any property where -you want the object on that property to also be validated. - -Thanks to this setup, the component will now be automatically validated -on each render, but in a smart way: a property will only be validated -once its "model" has been updated on the frontend. The system keeps track -of which models have been updated (e.g. `data-action="live#update"`) -and only stores the errors for those fields on re-render. - -You can also trigger validation of your _entire_ object manually -in an action: - -```php -use Symfony\UX\LiveComponent\Attribute\LiveAction; - -#[AsLiveComponent('edit_user')] -class EditUserComponent -{ - // ... - - #[LiveAction] - public function save() - { - // this will throw an exception if validation fails - $this->validate(); - - // perform save operations - } -} -``` - -If validation fails, an exception is thrown, but the component will be -re-rendered. In your template, render errors using the `getError()` -method: - -```twig -{% if this.getError('post.content') %} -
- {{ this.getError('post.content').message }} -
-{% endif %} - -``` - -Once a component has been validated, the component will "rememeber" -that it has been validated. This means that, if you edit a field and -the component re-renders, it will be validated again. - -## Real Time Validation - -As soon as you enable validation, each field will automatically -be validated when its model is updated. For example, if you want -a single field to be validated "on change" (when you change the field -and then blur the field), update the model via the `change` event: - -```twig - -``` - -When the component re-renders, it will signal to the server that this -one field should be validated. Like with normal validation, once an -individual field has been validated, the component "remembers" that, -and re-validates it on each render. - -## Polling - -You can also use "polling" to continually refresh a component. On -the **top-level** element for your component, add `data-poll`: - -```diff -
-``` - -This will make a request every 2 seconds to re-render the component. You -can change this by adding a `delay()` modifier. When you do this, you need -to be specific that you want to call the `$render` method. To delay for -500ms: - -```twig -
-``` - -You can also trigger a specific "action" instead of a normal re-render: - -```twig -
-``` - -## Embedded Components - -Need to embed one live component inside another one? No problem! As a rule -of thumb, **each component exists in its own, isolated universe**. This -means that embedding one component inside another could be really simple -or a bit more complex, depending on how inter-connected you want your components -to be. - -Here are a few helpful things to know: - -### Each component re-renders independent of one another - -If a parent component re-renders, the child component will _not_ (most -of the time) be updated, even though it lives inside the parent. Each -component is its own, isolated universe. - -But this is not always what you want. For example, suppose you have a -parent component that renders a form and a child component that renders -one field in that form. When you click a "Save" button on the parent -component, that validates the form and re-renders with errors - including -a new `error` value that it passes into the child: - -```twig -{# templates/components/post_form.html.twig #} - -{{ component('textarea_field', { - value: this.content, - error: this.getError('content') -}) }} -``` - -In this situation, when the parent component re-renders after clicking -"Save", you _do_ want the updated child component (with the validation -error) to be rendered. And this _will_ happen automatically. Why? because -the live component system detects that the **parent component has -_changed_ how it's rendering the child**. - -This may not always be perfect, and if your child component has its own -`LiveProp` that has changed since it was first rendered, that value will -be lost when the parent component causes the child to re-render. If you -have this situation, use `data-model-map` to map that child `LiveProp` to -a `LiveProp` in the parent component, and pass it into the child when -rendering. - -### Actions, methods and model updates in a child do not affect the parent - -Again, each component is its own, isolated universe! For example, suppose -your child component has: - -```html - -``` - -When the user clicks that button, it will attempt to call the `save` action -in the _child_ component only, even if the `save` action actually only -exists in the parent. The same is true for `data-model`, though there is -some special handling for this case (see next point). - -### If a child model updates, it will attempt to update the parent model - -Suppose a child component has a: - -```html - +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -
- {{ this.value|markdown_to_html }} -
-
-``` +## Resources -Notice that `MarkdownTextareaComponent` allows a dynamic `name` attribute to -be passed in. This makes that component re-usable in any form. But it -also makes sure that when the `textarea` changes, both the `value` model -in `MarkdownTextareaComponent` _and_ the `post.content` model in -`EditPostcomponent` will be updated. +- [Documentation](https://symfony.com/bundles/ux-live-component/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/LiveComponent/src/Resources/doc/index.rst b/src/LiveComponent/src/Resources/doc/index.rst new file mode 100644 index 00000000000..13bffda987e --- /dev/null +++ b/src/LiveComponent/src/Resources/doc/index.rst @@ -0,0 +1,1449 @@ +Live Components +=============== + +**EXPERIMENTAL** This component is currently experimental and is likely +to change, or even change drastically. + +Live components work with the `TwigComponent`_ library +to give you the power to automatically update your Twig components on +the frontend as the user interacts with them. Inspired by +`Livewire`_ and `Phoenix LiveView`_. + +A real-time product search component might look like this:: + + // src/Components/ProductSearchComponent.php + namespace App\Components; + + use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; + use Symfony\UX\LiveComponent\DefaultActionTrait; + + #[AsLiveComponent('product_search')] + class ProductSearchComponent + { + use DefaultActionTrait; + + public string $query = ''; + + private ProductRepository $productRepository; + + public function __construct(ProductRepository $productRepository) + { + $this->productRepository = $productRepository; + } + + public function getProducts(): array + { + // example method that returns an array of Products + return $this->productRepository->search($this->query); + } + } + +.. code-block:: twig + + {# templates/components/product_search.html.twig #} +
+ + +
    + {% for product in this.products %} +
  • {{ product.name }}
  • + {% endfor %} +
+
+ +As a user types into the box, the component will automatically re-render +and show the new results! + +Want a demo? Check out https://github.com/weaverryan/live-demo. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Now install the library with: + +.. code-block:: terminal + + $ composer require symfony/ux-live-component + +This comes with an embedded JavaScript Stimulus controller. Unlike other +Symfony UX packages, this needs to be enabled manually in your +``assets/bootstrap.js`` file: + +.. code-block:: javascript + + // assets/bootstrap.js + import LiveController from '@symfony/ux-live-component'; + import '@symfony/ux-live-component/styles/live.css'; + // ... + + app.register('live', LiveController); + +Finally, reinstall your Node dependencies and restart Encore: + +.. code-block:: terminal + + $ yarn install --force + $ yarn encore dev + +Oh, and just one more step! Import a routing file from the bundle: + +.. code-block:: yaml + + # config/routes.yaml + live_component: + resource: '@LiveComponentBundle/Resources/config/routing/live_component.xml' + +That's it! We're ready! + +Making your Component “Live” +---------------------------- + +If you haven't already, check out the `Twig Component`_ +documentation to get the basics of Twig components. + +Suppose you've already built a basic Twig component:: + + // src/Components/RandomNumberComponent.php + namespace App\Components; + + use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + + #[AsTwigComponent('random_number')] + class RandomNumberComponent + { + public function getRandomNumber(): int + { + return rand(0, 1000); + } + } + +.. code-block:: twig + + {# templates/components/random_number.html.twig #} +
+ {{ this.randomNumber }} +
+ +To transform this into a “live” component (i.e. one that can be +re-rendered live on the frontend), replace the component's +``AsTwigComponent`` attribute with ``AsLiveComponent`` and add the +``DefaultActionTrait``: + +.. code-block:: diff + + // src/Components/RandomNumberComponent.php + + - use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; + + use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; + + use Symfony\UX\LiveComponent\DefaultActionTrait; + + - #[AsTwigComponent('random_number')] + + #[AsLiveComponent('random_number')] + class RandomNumberComponent + { + + use DefaultActionTrait; + } + +Then, in the template, make sure there is *one* HTML element around your +entire component and use the ``{{ init_live_component() }}`` function to +initialize the Stimulus controller: + +.. code-block:: diff + + -
+ +
+ {{ this.randomNumber }} +
+ +Your component is now a live component… except that we haven't added +anything that would cause the component to update. Let's start simple, +by adding a button that - when clicked - will re-render the component +and give the user a new random number: + +.. code-block:: twig + +
+ {{ this.randomNumber }} + + +
+ +That's it! When you click the button, an Ajax call will be made to get a +fresh copy of our component. That HTML will replace the current HTML. In +other words, you just generated a new random number! That's cool, but +let's keep going because… things get cooler. + +LiveProps: Stateful Component Properties +---------------------------------------- + +Let's make our component more flexible by adding ``$min`` and ``$max`` +properties:: + + // src/Components/RandomNumberComponent.php + namespace App\Components; + + // ... + use Symfony\UX\LiveComponent\Attribute\LiveProp; + + #[AsLiveComponent('random_number')] + class RandomNumberComponent + { + #[LiveProp] + public int $min = 0; + + #[LiveProp] + public int $max = 1000; + + public function getRandomNumber(): int + { + return rand($this->min, $this->max); + } + + // ... + } + +With this change, we can control the ``$min`` and ``$max`` properties +when rendering the component: + +.. code-block:: twig + + {{ component('random_number', { min: 5, max: 500 }) }} + +But what's up with those ``LiveProp`` attributes? A property with the +``LiveProp`` attribute becomes a “stateful” property for this component. +In other words, each time we click the “Generate a new number!” button, +when the component re-renders, it will *remember* the original values +for the ``$min`` and ``$max`` properties and generate a random number +between 5 and 500. If you forgot to add ``LiveProp``, when the component +re-rendered, those two values would *not* be set on the object. + +In short: LiveProps are “stateful properties”: they will always be set +when rendering. Most properties will be LiveProps, with common +exceptions being properties that hold services (these don't need to be +stateful because they will be autowired each time before the component +is rendered) and `properties used for computed properties`_. + +data-action=“live#update”: Re-rendering on LiveProp Change +---------------------------------------------------------- + +Could we allow the user to *choose* the ``$min`` and ``$max`` values and +automatically re-render the component when they do? Definitely! And +*that* is where live components really shine. + +Let's add two inputs to our template: + +.. code-block:: twig + + {# templates/components/random_number.html.twig #} +
+ + + + + Generating a number between {{ this.min }} and {{ this.max }} + {{ this.randomNumber }} +
+ +Notice the ``data-action="live#update"`` on each ``input``. When the +user types, live components reads the ``data-model`` attribute +(e.g. ``min``) and re-renders the component using the *new* value for +that field! Yes, as you type in a box, the component automatically +updates to reflect the new number! + +Well, actually, we're missing one step. By default, a ``LiveProp`` is +“read only”. For security purposes, a user cannot change the value of a +``LiveProp`` and re-render the component unless you allow it with the +``writable=true`` option: + +.. code-block:: diff + + // src/Components/RandomNumberComponent.php + // ... + + class RandomNumberComponent + { + - #[LiveProp] + + #[LiveProp(writable: true)] + public int $min = 0; + + - #[LiveProp] + + #[LiveProp(writable: true)] + public int $max = 1000; + + // ... + } + +Now it works: as you type into the ``min`` or ``max`` boxes, the +component will re-render with a new random number between that range! + +Debouncing +~~~~~~~~~~ + +If the user types 5 characters really quickly into an ``input``, we +don't want to send 5 Ajax requests. Fortunately, the ``live#update`` +method has built-in debouncing: it waits for a 150ms pause before +sending an Ajax request to re-render. This is built in, so you don't +need to think about it. + +Lazy Updating on “blur” or “change” of a Field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, you might want a field to re-render only after the user has +changed an input *and* moved to another field. Browsers dispatch a +``change`` event in this situation. To re-render when this event +happens, add it to the ``data-action`` call: + +.. code-block:: diff + + + +The ``data-action="change->live#update"`` syntax is standard Stimulus +syntax, which says: + + When the “change” event occurs, call the ``update`` method on the + ``live`` controller. + +.. _deferring-a-re-render-until-later: + +Deferring a Re-Render Until Later +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Other times, you might want to update the internal value of a property, +but wait until later to re-render the component (e.g. until a button is +clicked). To do that, use the ``updateDefer`` method: + +.. code-block:: diff + + + +Now, as you type, the ``max`` “model” will be updated in JavaScript, but +it won't, yet, make an Ajax call to re-render the component. Whenever +the next re-render *does* happen, the updated ``max`` value will be +used. + +Using name="" instead of data-model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of communicating the property name of a field via +``data-model``, you can communicate it via the standard ``name`` +property. The following code works identically to the previous example: + +.. code-block:: diff + +
+ + + // ... +
+ +If an element has *both* ``data-model`` and ``name`` attributes, the +``data-model`` attribute takes precedence. + +Loading States +-------------- + +Often, you'll want to show (or hide) an element while a component is +re-rendering or an :ref:`action ` is processing. For example: + +.. code-block:: twig + + + Loading + + + Loading + +Or, to *hide* an element while the component is loading: + +.. code-block:: twig + + + Saved! + +Adding and Removing Classes or Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of hiding or showing an entire element, you could add or remove +a class: + +.. code-block:: twig + + +
...
+ + +
...
+ + +
...
+ +Sometimes you may want to add or remove an attribute when loading. That +can be accomplished with ``addAttribute`` or ``removeAttribute``: + +.. code-block:: twig + + +
...
+ +You can also combine any number of directives by separating them with a +space: + +.. code-block:: twig + +
...
+ +Finally, you can add the ``delay`` modifier to not trigger the loading +changes until loading has taken longer than a certain amount of time: + +.. code-block:: twig + + +
...
+ + +
Loading
+ + +
Loading
+ +.. _actions: + +Actions +------- + +Live components require a single “default action” that is used to +re-render it. By default, this is an empty ``__invoke()`` method and can +be added with the ``DefaultActionTrait``. Live components are actually +Symfony controllers so you can add the normal controller +attributes/annotations (ie ``@Cache``/``@Security``) to either the +entire class just a single action. + +You can also trigger custom actions on your component. Let's pretend we +want to add a “Reset Min/Max” button to our “random number” component +that, when clicked, sets the min/max numbers back to a default value. + +First, add a method with a ``LiveAction`` attribute above it that does +the work:: + + // src/Components/RandomNumberComponent.php + namespace App\Components; + + // ... + use Symfony\UX\LiveComponent\Attribute\LiveAction; + + class RandomNumberComponent + { + // ... + + #[LiveAction] + public function resetMinMax() + { + $this->min = 0; + $this->max = 1000; + } + + // ... + } + +To call this, add ``data-action="live#action"`` and ``data-action-name`` +to an element (e.g. a button or form): + +.. code-block:: twig + + + +Done! When the user clicks this button, a POST request will be sent that +will trigger the ``resetMinMax()`` method! After calling that method, +the component will re-render like normal, using the new ``$min`` and +``$max`` properties! + +You can also add several “modifiers” to the action: + +.. code-block:: twig + +
+ +
+ +The ``prevent`` modifier would prevent the form from submitting +(``event.preventDefault()``). The ``debounce(300)`` modifier will add +300ms of “debouncing” before the action is executed. In other words, if +you click really fast 5 times, only one Ajax request will be made! + +Actions & Services +^^^^^^^^^^^^^^^^^^ + +One really neat thing about component actions is that they are *real* +Symfony controllers. Internally, they are processed identically to a +normal controller method that you would create with a route. + +This means that, for example, you can use action autowiring:: + + // src/Components/RandomNumberComponent.php + namespace App\Components; + + // ... + use Psr\Log\LoggerInterface; + + class RandomNumberComponent + { + // ... + + #[LiveAction] + public function resetMinMax(LoggerInterface $logger) + { + $this->min = 0; + $this->max = 1000; + $logger->debug('The min/max were reset!'); + } + + // ... + } + +Actions and CSRF Protection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you trigger an action, a POST request is sent that contains a +``X-CSRF-TOKEN`` header. This header is automatically populated and +validated. In other words… you get CSRF protection without any work. + +Your only job is to make sure that the CSRF component is installed: + +.. code-block:: terminal + + $ composer require symfony/security-csrf + +Actions, Redirecting and AbstractController +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes, you may want to redirect after an action is executed +(e.g. your action saves a form and then you want to redirect to another +page). You can do that by returning a ``RedirectResponse`` from your +action:: + + // src/Components/RandomNumberComponent.php + namespace App\Components; + + // ... + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class RandomNumberComponent extends AbstractController + { + // ... + + #[LiveAction] + public function resetMinMax() + { + // ... + + $this->addFlash('success', 'Min/Max have been reset!'); + + return $this->redirectToRoute('app_random_number'); + } + + // ... + } + +You probably noticed one interesting trick: to make redirecting easier, +the component now extends ``AbstractController``! That is totally +allowed, and gives you access to all of your normal controller +shortcuts. We even added a flash message! + +.. _forms: + +Forms +----- + +A component can also help render a `Symfony form`_, either the entire +form (useful for automatic validation as you type) or just one or some +fields (e.g. a markdown preview for a ``textarea`` or `dependent form fields`_. + +Rendering an Entire Form in a Component +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you have a ``PostType`` form class that's bound to a ``Post`` +entity and you'd like to render this in a component so that you can get +instant validation as the user types:: + + namespace App\Form; + + use App\Entity\Post; + use Symfony\Component\Form\AbstractType; + use Symfony\Component\Form\FormBuilderInterface; + use Symfony\Component\OptionsResolver\OptionsResolver; + + class PostType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('title') + ->add('slug') + ->add('content') + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Post::class, + ]); + } + } + +Before you start thinking about the component, make sure that you have +your controller set up so you can handle the form submit. There's +nothing special about this controller: it's written however you normally +write your form controller logic:: + + namespace App\Controller; + + use App\Entity\Post; + use App\Form\PostType; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + + class PostController extends AbstractController + { + /** + * #[Route('/admin/post/{id}/edit', name: 'app_post_edit')] + */ + public function edit(Request $request, Post $post): Response + { + $form = $this->createForm(PostType::class, $post); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->getDoctrine()->getManager()->flush(); + + return $this->redirectToRoute('app_post_index'); + } + + // renderForm() is new in Symfony 5.3. + // Use render() and call $form->createView() if on a lower version + return $this->renderForm('post/edit.html.twig', [ + 'post' => $post, + 'form' => $form, + ]); + } + } + +Great! In the template, instead of rendering the form, let's render a +``post_form`` component that we will create next: + +.. code-block:: twig + + {# templates/post/edit.html.twig #} + + {% extends 'base.html.twig' %} + + {% block body %} +

Edit Post

+ + {{ component('post_form', { + post: post, + form: form + }) }} + {% endblock %} + +Ok: time to build that ``post_form`` component! The Live Components +package comes with a special trait - ``ComponentWithFormTrait`` - to +make it easy to deal with forms:: + + namespace App\Twig\Components; + + use App\Entity\Post; + use App\Form\PostType; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Form\FormInterface; + use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; + use Symfony\UX\LiveComponent\Attribute\LiveProp; + use Symfony\UX\LiveComponent\ComponentWithFormTrait; + + #[AsLiveComponent('post_form')] + class PostFormComponent extends AbstractController + { + use ComponentWithFormTrait; + + /** + * The initial data used to create the form. + * + * Needed so the same form can be re-created + * when the component is re-rendered via Ajax. + * + * The `fieldName` option is needed in this situation because + * the form renders fields with names like `name="post[title]"`. + * We set `fieldName: ''` so that this live prop doesn't collide + * with that data. The value - initialFormData - could be anything. + */ + #[LiveProp(fieldName: 'initialFormData')] + public ?Post $post = null; + + /** + * Used to re-create the PostType form for re-rendering. + */ + protected function instantiateForm(): FormInterface + { + // we can extend AbstractController to get the normal shortcuts + return $this->createForm(PostType::class, $this->post); + } + } + +The trait forces you to create an ``instantiateForm()`` method, which is +used when the component is rendered via AJAX. Notice that, in order to +recreate the *same* form, we pass in the ``Post`` object and set it as a +``LiveProp``. + +The template for this component will render the form, which is available +as ``this.form`` thanks to the trait: + +.. code-block:: twig + + {# templates/components/post_form.html.twig #} +
+ {{ form_start(this.form) }} + {{ form_row(this.form.title) }} + {{ form_row(this.form.slug) }} + {{ form_row(this.form.content) }} + + + {{ form_end(this.form) }} +
+ +Mostly, this is a pretty boring template! It includes the normal +``init_live_component(this)`` and then you render the form however you +want. + +But the result is incredible! As you finish changing each field, the +component automatically re-renders - including showing any validation +errors for that field! Amazing! + +This is possible thanks to a few interesting pieces: + +- ``data-action="change->live#update"``: instead of adding + ``data-action`` to *every* field, you can place this on a parent + element. Thanks to this, as you change or type into fields (i.e. the + ``input`` event), the model for that field will update and the + component will re-render. + +- The fields in our form do not have a ``data-model=""`` attribute. But + that's ok! When that is absent, the ``name`` attribute is used + instead. ``ComponentWithFormTrait`` has a modifiable ``LiveProp`` + that captures these and submits the form using them. That's right: + each render time the component re-renders, the form is *submitted* + using the values. However, if a field has not been modified yet by + the user, its validation errors are cleared so that they aren't + rendered. + +Handling “Cannot dehydrate an unpersisted entity” Errors. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're building a form to create a *new* entity, then when you render +the component, you may be passing in a new, non-persisted entity. + +For example, imagine you create a ``new Post()`` in your controller, +pass this “not-yet-persisted” entity into your template as a ``post`` +variable and pass *that* into your component: + +.. code-block:: twig + + {{ component('post_form', { + post: post, + form: form + }) }} + +If you do this, you'll likely see this error: + + Cannot dehydrate an unpersisted entity + App\Entity\Post. If you want to allow + this, add a dehydrateWith= option to LiveProp + +The problem is that the Live component system doesn't know how to +transform this object into something that can be sent to the frontend, +called “dehydration”. If an entity has already been saved to the +database, its “id” is sent to the frontend. But if the entity hasn't +been saved yet, that's not possible. + +The solution is to pass ``null`` into your component instead of a +non-persisted entity object. If you need to, you can re-create your +``new Post()`` inside of your component: + +.. code-block:: diff + + {{ component('post_form', { + - post: post, + + post: post.id ? post : null, + form: form + }) }} + +Form Rendering Problems +~~~~~~~~~~~~~~~~~~~~~~~ + +For the most part, rendering a form inside a component works +beautifully. But there are a few situations when your form may not +behave how you want. + +**A) Text Boxes Removing Trailing Spaces** + +If you're re-rendering a field on the ``input`` event (that's the +default event on a field, which is fired each time you type in a text +box), then if you type a “space” and pause for a moment, the space will +disappear! + +This is because Symfony text fields “trim spaces” automatically. When +your component re-renders, the space will disappear… as the user is +typing! To fix this, either re-render on the ``change`` event (which +fires after the text box loses focus) or set the ``trim`` option of your +field to ``false``:: + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('content', TextareaType::class, [ + 'trim' => false, + ]) + ; + } + +**B) ``PasswordType`` loses the password on re-render** + +If you're using the ``PasswordType``, when the component re-renders, the +input will become blank! That's because, by default, the +``PasswordType`` does not re-fill the ```` after +a submit. + +To fix this, set the ``always_empty`` option to ``false`` in your form:: + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + // ... + ->add('plainPassword', PasswordType::class, [ + 'always_empty' => false, + ]) + ; + } + +Submitting the Form via an action() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Notice that, while we *could* add a ``save()`` :ref:`component action ` that handles the form submit through the component, +we've chosen not to do that so far. The reason is simple: by creating a +normal route & controller that handles the submit, our form continues to +work without JavaScript. + +However, you *can* do this if you'd like. In that case, you wouldn't +need any form logic in your controller:: + + #[Route('/admin/post/{id}/edit', name: 'app_post_edit')] + public function edit(Post $post): Response + { + return $this->render('post/edit.html.twig', [ + 'post' => $post, + ]); + } + +And you wouldn't pass any ``form`` into the component: + +.. code-block:: twig + + {# templates/post/edit.html.twig #} + +

Edit Post

+ + {{ component('post_form', { + post: post + }) }} + +When you do *not* pass a ``form`` into a component that uses +``ComponentWithFormTrait``, the form will be created for you +automatically. Let's add the ``save()`` action to the component:: + + // ... + use Doctrine\ORM\EntityManagerInterface; + use Symfony\UX\LiveComponent\Attribute\LiveAction; + + class PostFormComponent extends AbstractController + { + // ... + + #[LiveAction] + public function save(EntityManagerInterface $entityManager) + { + // shortcut to submit the form with form values + // if any validation fails, an exception is thrown automatically + // and the component will be re-rendered with the form errors + $this->submitForm(); + + /** @var Post $post */ + $post = $this->getFormInstance()->getData(); + $entityManager->persist($post); + $entityManager->flush(); + + $this->addFlash('success', 'Post saved!'); + + return $this->redirectToRoute('app_post_show', [ + 'id' => $this->post->getId(), + ]); + } + } + +Finally, tell the ``form`` element to use this action: + +.. code-block:: twig + + {# templates/components/post_form.html.twig #} + {# ... #} + + {{ form_start(this.form, { + attr: { + 'data-action': 'live#action', + 'data-action-name': 'prevent|save' + } + }) }} + +Now, when the form is submitted, it will execute the ``save()`` method +via Ajax. If the form fails validation, it will re-render with the +errors. And if it's successful, it will redirect. + +**NOTE**: Make sure that each time the user changes a field, you update +the component's model. If you don't do this, when you trigger the +action, it will *not* contain the form's data because the data in the +fields and the component's data will be out of sync. + +An easy way to accomplish this (explained more in the :ref:`Forms ` +section above) is to add: + +.. code-block:: diff + +
+ +Modifying Embedded Properties with the "exposed" Option +------------------------------------------------------- + +If your component will render a form, you don't need to use the Symfony +form component. Let's build an ``EditPostComponent`` without a form. +This will need one ``LiveProp``: the ``Post`` object that is being +edited:: + + namespace App\Twig\Components; + + use App\Entity\Post; + use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; + use Symfony\UX\LiveComponent\Attribute\LiveProp; + + #[AsLiveComponent('edit_post')] + class EditPostComponent + { + #[LiveProp] + public Post $post; + } + +In the template, let's render an HTML form *and* a “preview” area where +the user can see, as they type, what the post will look like (including +rendered the ``content`` through a Markdown filter from the +``twig/markdown-extra`` library): + +.. code-block:: twig + +
+ + + + +
+

{{this.post.title}}

+ {{this.post.content | markdown_to_html}} +
+
+ +This is pretty straightforward, except for one thing: the ``data-model`` +attributes aren't targeting properties on the component class itself, +they're targeting *embedded* properties within the ``$post`` property. + +Out-of-the-box, modifying embedded properties is *not* allowed. However, +you can enable it via the ``exposed`` option: + +.. code-block:: diff + + // ... + + class EditPostComponent + { + - #[LiveProp] + + #[LiveProp(exposed: ['title', 'content'])] + public Post $post; + + // ... + } + +With this, both the ``title`` and the ``content`` properties of the +``$post`` property *can* be modified by the user. However, notice that +the ``LiveProp`` does *not* have ``writable=true``. This means that +while the ``title`` and ``content`` properties can be changed, the +``Post`` object itself **cannot** be changed. In other words, if the +component was originally created with a Post object with id=2, a bad +user could *not* make a request that renders the component with id=3. +Your component is protected from someone changing to see the form for a +different ``Post`` object, unless you added ``writable=true`` to this +property. + +Validation (without a Form) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**NOTE** If your component :ref:`contains a form `, then validation +is built-in automatically. Follow those docs for more details. + +If you're building some sort of form *without* using Symfony's form +component, you *can* still validate your data. + +First use the ``ValidatableComponentTrait`` and add any constraints you +need:: + + use App\Entity\User; + use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; + use Symfony\UX\LiveComponent\Attribute\LiveProp; + use Symfony\UX\LiveComponent\ValidatableComponentTrait; + use Symfony\Component\Validator\Constraints as Assert; + + #[AsLiveComponent('edit_user')] + class EditUserComponent + { + use ValidatableComponentTrait; + + #[LiveProp(exposed: ['email', 'plainPassword'])] + #[Assert\Valid] + public User $user; + + #[LiveProp] + #[Assert\IsTrue] + public bool $agreeToTerms = false; + } + +Be sure to add the ``IsValid`` attribute/annotation to any property +where you want the object on that property to also be validated. + +Thanks to this setup, the component will now be automatically validated +on each render, but in a smart way: a property will only be validated +once its “model” has been updated on the frontend. The system keeps +track of which models have been updated +(e.g. ``data-action="live#update"``) and only stores the errors for +those fields on re-render. + +You can also trigger validation of your *entire* object manually in an +action:: + + use Symfony\UX\LiveComponent\Attribute\LiveAction; + + #[AsLiveComponent('edit_user')] + class EditUserComponent + { + // ... + + #[LiveAction] + public function save() + { + // this will throw an exception if validation fails + $this->validate(); + + // perform save operations + } + } + +If validation fails, an exception is thrown, but the component will be +re-rendered. In your template, render errors using the ``getError()`` +method: + +.. code-block:: twig + + {% if this.getError('post.content') %} +
+ {{ this.getError('post.content').message }} +
+ {% endif %} + + +Once a component has been validated, the component will “rememeber” that +it has been validated. This means that, if you edit a field and the +component re-renders, it will be validated again. + +Real Time Validation +-------------------- + +As soon as you enable validation, each field will automatically be +validated when its model is updated. For example, if you want a single +field to be validated “on change” (when you change the field and then +blur the field), update the model via the ``change`` event: + +.. code-block:: twig + + + +When the component re-renders, it will signal to the server that this +one field should be validated. Like with normal validation, once an +individual field has been validated, the component “remembers” that, and +re-validates it on each render. + +Polling +------- + +You can also use “polling” to continually refresh a component. On the +**top-level** element for your component, add ``data-poll``: + +.. code-block:: diff + +
+ +This will make a request every 2 seconds to re-render the component. You +can change this by adding a ``delay()`` modifier. When you do this, you +need to be specific that you want to call the ``$render`` method. To +delay for 500ms: + +.. code-block:: twig + +
+ +You can also trigger a specific “action” instead of a normal re-render: + +.. code-block:: twig + +
+ +Embedded Components +------------------- + +Need to embed one live component inside another one? No problem! As a +rule of thumb, **each component exists in its own, isolated universe**. +This means that embedding one component inside another could be really +simple or a bit more complex, depending on how inter-connected you want +your components to be. + +Here are a few helpful things to know: + +Each component re-renders independent of one another +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a parent component re-renders, the child component will *not* (most +of the time) be updated, even though it lives inside the parent. Each +component is its own, isolated universe. + +But this is not always what you want. For example, suppose you have a +parent component that renders a form and a child component that renders +one field in that form. When you click a “Save” button on the parent +component, that validates the form and re-renders with errors - +including a new ``error`` value that it passes into the child: + +.. code-block:: twig + + {# templates/components/post_form.html.twig #} + + {{ component('textarea_field', { + value: this.content, + error: this.getError('content') + }) }} + +In this situation, when the parent component re-renders after clicking +"Save", you *do* want the updated child component (with the validation +error) to be rendered. And this *will* happen automatically. Why? +because the live component system detects that the **parent component +has changed how it's rendering the child**. + +This may not always be perfect, and if your child component has its own +``LiveProp`` that has changed since it was first rendered, that value +will be lost when the parent component causes the child to re-render. If +you have this situation, use ``data-model-map`` to map that child +``LiveProp`` to a ``LiveProp`` in the parent component, and pass it into +the child when rendering. + +Actions, methods and model updates in a child do not affect the parent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Again, each component is its own, isolated universe! For example, +suppose your child component has: + +.. code-block:: html + + + +When the user clicks that button, it will attempt to call the ``save`` +action in the *child* component only, even if the ``save`` action +actually only exists in the parent. The same is true for ``data-model``, +though there is some special handling for this case (see next point). + +If a child model updates, it will attempt to update the parent model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose a child component has a: + +.. code-block:: html + + + +
+ {{ this.value|markdown_to_html }} +
+
+ +Notice that ``MarkdownTextareaComponent`` allows a dynamic ``name`` +attribute to be passed in. This makes that component re-usable in any +form. But it also makes sure that when the ``textarea`` changes, both +the ``value`` model in ``MarkdownTextareaComponent`` *and* the +``post.content`` model in ``EditPostcomponent`` will be updated. + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, meaning it is not +bound to Symfony's BC policy for the moment. + +.. _`TwigComponent`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`Livewire`: https://laravel-livewire.com +.. _`Phoenix LiveView`: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html +.. _`Twig Component`: https://symfony.com/bundles/ux-twig-component/current/index.html +.. _`properties used for computed properties`: https://symfony.com/bundles/ux-live-component/current/index.html#computed-properties +.. _`Symfony form`: https://symfony.com/doc/current/forms.html +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`dependent form fields`: https://symfony.com/doc/current/form/dynamic_form_modification.html#dynamic-generation-for-submitted-forms +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/Swup/.symfony.bundle.yaml b/src/Swup/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Swup/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Swup/README.md b/src/Swup/README.md index 32da2d86b10..ffc8278e939 100644 --- a/src/Swup/README.md +++ b/src/Swup/README.md @@ -7,193 +7,12 @@ Swup is a complete and easy to use page transition library for Web applications. a Single Page Application feel to Web applications without having to change anything on the server and without bringing the complexity of a React/Vue/Angular application. -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX Swup does not use any PHP and can be installed with any PHP/Symfony version. +## Resources -You can install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-swup - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -Also make sure you have at least version 3.0 of [@symfony/stimulus-bridge](https://github.com/symfony/stimulus-bridge) -in your `package.json` file. - -## Usage - -In order to implement page transitions, Swup works by transforming the links of your -application in AJAX calls to the target in their href. Once the AJAX call result is -received, Swup is able to swap the content of the current page with the new content -received by AJAX. When doing this swap, it is therefore able to animate a transition -between pages. - -The main usage of Symfony UX Swup is to use its Stimulus controller to initialize Swup: - -```twig - - - Swup - - {% block javascripts %} - {{ encore_entry_script_tags('app') }} - {% endblock %} - - - {# ... #} - -
- {# ... #} -
- - -``` - -**Note** The `stimulus_controller()` function comes from -[WebpackEncoreBundle v1.10](https://github.com/symfony/webpack-encore-bundle). - -That's it! Swup now reacts to a link click and run the default fade-in transition. - -By default, Swup will use the `#swup` selector as a container, meaning it will only swap -the content of this container from one page to another. If you wish, you can configure -additional containers, for instance to have a navigation menu that updates when changing pages: - -```twig - - - Swup - - {% block javascripts %} - {{ encore_entry_script_tags('app') }} - {% endblock %} - - - {# ... #} - - - -
- {# ... #} -
- - -``` - -You can configure several other options using values on the controller. -Most of these correspond to [Swup Options](https://swup.js.org/options), -but there are a few extra added: - -```twig - - - Swup - - - {# ... #} - - -``` - -The extra options are: - -- `theme`: either `slide` or `fade` (the default); -- `debug`: add this attribute to enable debug. - -### Extend the default behavior - -Symfony UX Swup allows you to extend its default behavior using a custom Stimulus controller: - -```js -// assets/controllers/myswup_controller.js - -import { Controller } from '@hotwired/stimulus'; - -export default class extends Controller { - connect() { - this.element.addEventListener('swup:pre-connect', this._onPreConnect); - this.element.addEventListener('swup:connect', this._onConnect); - } - - disconnect() { - // You should always remove listeners when the controller is disconnected to avoid side-effects - this.element.removeEventListener('swup:pre-connect', this._onConnect); - this.element.removeEventListener('swup:connect', this._onPreConnect); - } - - _onPreConnect(event) { - // Swup has not been initialized - options can be changed - console.log(event.detail.options); // Options that will be used to initialize Swup - } - - _onConnect(event) { - // Swup has just been intialized and you can access details from the event - console.log(event.detail.swup); // Swup instance - console.log(event.detail.options); // Options used to initialize Swup - } -} -``` - -Then in your template, add your controller to the HTML attribute: - -```twig - - - Swup - {# ... #} - - - {# ... #} - - -``` - -> **Note**: be careful to add your controller **before** the Swup controller so that -> it is executed before and can listen on the `swup:connect` event properly. - -## Backward Compatibility promise - -This bundle aims at following the same Backward Compatibility promise as the Symfony framework: -[https://symfony.com/doc/current/contributing/code/bc.html](https://symfony.com/doc/current/contributing/code/bc.html) - -However it is currently considered -[**experimental**](https://symfony.com/doc/current/contributing/code/experimental.html), -meaning it is not bound to Symfony's BC policy for the moment. - -## Run tests - -### PHP tests - -```sh -php vendor/bin/phpunit -``` - -### JavaScript tests - -```sh -cd Resources/assets -yarn test -``` +- [Documentation](https://symfony.com/bundles/ux-swup/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Swup/Resources/doc/index.rst b/src/Swup/Resources/doc/index.rst new file mode 100644 index 00000000000..65c3d39bbe7 --- /dev/null +++ b/src/Swup/Resources/doc/index.rst @@ -0,0 +1,205 @@ +Symfony UX Swup +=============== + +Symfony UX Swup is a Symfony bundle integrating `Swup`_ in +Symfony applications. It is part of `the Symfony UX initiative`_. + +Swup is a complete and easy to use page transition library for Web +applications. It creates a Single Page Application feel to Web +applications without having to change anything on the server and without +bringing the complexity of a React/Vue/Angular application. + +Installation +------------ + +Before you start, make sure you have `Symfony UX configured in your app`_. + +Then install the bundle using Composer and Symfony Flex: + +.. code-block:: terminal + + $ composer require symfony/ux-swup + + # Don't forget to install the JavaScript dependencies as well and compile + $ yarn install --force + $ yarn encore dev + +Also make sure you have at least version 3.0 of +`@symfony/stimulus-bridge`_ in your ``package.json`` file. + +Usage +----- + +In order to implement page transitions, Swup works by transforming the +links of your application in AJAX calls to the target in their href. +Once the AJAX call result is received, Swup is able to swap the content +of the current page with the new content received by AJAX. When doing +this swap, it is therefore able to animate a transition between pages. + +The main usage of Symfony UX Swup is to use its Stimulus controller to +initialize Swup: + +.. code-block:: twig + + + + Swup + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + + {# ... #} + +
+ {# ... #} +
+ + + +**Note** The ``stimulus_controller()`` function comes from +`WebpackEncoreBundle v1.10`_. + +That's it! Swup now reacts to a link click and run the default fade-in +transition. + +By default, Swup will use the ``#swup`` selector as a container, meaning +it will only swap the content of this container from one page to +another. If you wish, you can configure additional containers, for +instance to have a navigation menu that updates when changing pages: + +.. code-block:: twig + + + + Swup + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + + {# ... #} + + + +
+ {# ... #} +
+ + + +You can configure several other options using values on the controller. +Most of these correspond to `Swup Options`_, but there are a few extra +added: + +.. code-block:: twig + + + + Swup + + + {# ... #} + + + +The extra options are: + +- ``theme``: either ``slide`` or ``fade`` (the default); +- ``debug``: add this attribute to enable debug. + +Extend the default behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Symfony UX Swup allows you to extend its default behavior using a custom +Stimulus controller: + +.. code-block:: javascript + + // assets/controllers/myswup_controller.js + + import { Controller } from '@hotwired/stimulus'; + + export default class extends Controller { + connect() { + this.element.addEventListener('swup:pre-connect', this._onPreConnect); + this.element.addEventListener('swup:connect', this._onConnect); + } + + disconnect() { + // You should always remove listeners when the controller is disconnected to avoid side-effects + this.element.removeEventListener('swup:pre-connect', this._onConnect); + this.element.removeEventListener('swup:connect', this._onPreConnect); + } + + _onPreConnect(event) { + // Swup has not been initialized - options can be changed + console.log(event.detail.options); // Options that will be used to initialize Swup + } + + _onConnect(event) { + // Swup has just been intialized and you can access details from the event + console.log(event.detail.swup); // Swup instance + console.log(event.detail.options); // Options used to initialize Swup + } + } + +Then in your template, add your controller to the HTML attribute: + +.. code-block:: twig + + + + Swup + {# ... #} + + + {# ... #} + + + +.. + + **Note**: be careful to add your controller **before** the Swup + controller so that it is executed before and can listen on the + ``swup:connect`` event properly. + +Backward Compatibility promise +------------------------------ + +This bundle aims at following the same Backward Compatibility promise as +the Symfony framework: +https://symfony.com/doc/current/contributing/code/bc.html + +However it is currently considered `experimental`_, +meaning it is not bound to Symfony's BC policy for the moment. + +.. _`Swup`: https://swup.js.org/ +.. _`the Symfony UX initiative`: https://symfony.com/ux +.. _`@symfony/stimulus-bridge`: https://github.com/symfony/stimulus-bridge +.. _`WebpackEncoreBundle v1.10`: https://github.com/symfony/webpack-encore-bundle +.. _`Swup Options`: https://swup.js.org/options +.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html +.. _`Symfony UX configured in your app`: https://symfony.com/doc/current/frontend/ux.html diff --git a/src/Turbo/.symfony.bundle.yaml b/src/Turbo/.symfony.bundle.yaml new file mode 100644 index 00000000000..50b8d4a3040 --- /dev/null +++ b/src/Turbo/.symfony.bundle.yaml @@ -0,0 +1,3 @@ +branches: ["2.x"] +maintained_branches: ["2.x"] +doc_dir: "Resources/doc" diff --git a/src/Turbo/Bridge/Mercure/README.md b/src/Turbo/Bridge/Mercure/README.md new file mode 100644 index 00000000000..1bae5b3cf9c --- /dev/null +++ b/src/Turbo/Bridge/Mercure/README.md @@ -0,0 +1,15 @@ +# Symfony Turbo-Mercure + +This library integrates [Symfony UX Turbo](https://symfony.com/bundles/ux-turbo/current/index.html) +with [Mercure](https://mercure.rocks/) to allow you to broadcast real-time page +updates to anyone using your site. + +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. + +## Resources + +* [Documentation](https://symfony.com/bundles/ux-turbo-mercure/current/index.html) +* [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/src/Turbo/README.md b/src/Turbo/README.md index 0265fd547d1..beab709b564 100644 --- a/src/Turbo/README.md +++ b/src/Turbo/README.md @@ -9,741 +9,17 @@ but without having to write a single line of JavaScript! Symfony UX Turbo also integrates with [Symfony Mercure](https://symfony.com/doc/current/mercure.html) or any other transports to broadcast DOM changes to all currently connected users! -You're in a hurry? Take a look at [the chat example](#sending-async-changes-using-mercure-a-chat) +You're in a hurry? Take a look at [the chat example](https://symfony.com/bundles/ux-turbo/current/index.html#chat-example) to discover the full potential of Symfony UX Turbo. Or watch the [Turbo Screencast on SymfonyCasts](https://symfonycasts.com/screencast/turbo). -## Installation +**This repository is a READ-ONLY sub-tree split**. See +https://github.com/symfony/ux to create issues or submit pull requests. -Symfony UX Turbo requires PHP 7.2+ and Symfony 5.2+. +## Resources -Install this bundle using Composer and Symfony Flex: - -```sh -composer require symfony/ux-turbo - -# Don't forget to install the JavaScript dependencies as well and compile -yarn install --force -yarn encore dev -``` - -## Usage - -### Accelerating Navigation with Turbo Drive - -Turbo Drive enhances page-level navigation. It watches for link clicks and form submissions, -performs them in the background, and updates the page without doing a full reload. -This gives you the "single-page-app" experience without major changes to your code! - -Turbo Drive is automatically enabled when you install Symfony UX Turbo. And while -you don't need to make major changes to get things to work smoothly, there are 3 -things to be aware of: - -#### 1. Make sure your JavaScript is Turbo-ready - -Because navigation no longer results in full page refreshes, you may need to -adjust your JavaScript to work properly. The best solution is to write your -JavaScript using [Stimulus](https://stimulus.hotwired.dev/) or something similar. - -We also recommend that you place your `script` tags live inside your `head` tag so -that they aren't reloaded on every navigation (Turbo re-executes any `script` tags -inside `body` on every navigation). Add a `defer` attribute to each `script` tag -to prevent it from blocking the page load. See -[Moving