124 Commits
v2.13.2 ... 2.x

Author SHA1 Message Date
github-actions[bot]
05af0259f2 Update versions to 2.34.0 2026-03-22 22:21:50 +00:00
Hugo Alliaume
d610a2e021 [Autocomplete][Chartjs][Cropperjs][Dropzone][LazyImage][LiveComponent][Map][Notify][React][StimulusBundle][Svelte][Swup][TogglePassword][Translator][Turbo][Typed][Vue] Update package.json to 2.33.0
The job that release npm packages failed https://github.com/symfony/ux/actions/runs/23216748609/job/67479440402

it should be fixed by https://github.com/symfony/ux/pull/3400
2026-03-21 23:29:11 +01:00
Hugo Alliaume
66b32eaad0 Update tsdown & use @tsdown/css
This update simplifies the tsdown configuration, we do not need our custom plugin to minify CSS anymore (replaced by `css.minify = true`), and same for our hooks that rename the built CSS (replaced by `css.fileName`) 😍
2026-03-20 09:00:50 +01:00
Hugo Alliaume
581fe67a2a Update Vitest to ^4.1.0 2026-03-15 08:57:29 +01:00
Hugo Alliaume
90640ce587 Migrate from tsup (deprecated) to tsdown
tsup is deprecated in favor of tsdown.

Follow https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, and many (local) tries were I was not really happy with the files generated by tsdown/rolldown, we are finally having something extra good!

We have the benefits from https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, but without their drawbacks. The code correctly follow the `es2022` target and does not do anything weird anymore with static properties (needed by controllers).

I (Claude) added a plugin to remove the region and JSDoc comments (except if they contain `@deprecated``), since I think we want to keep the code non-minified.
2026-02-28 09:33:37 +01:00
Hugo Alliaume
0401477482 Remove tsx dependency and rely on Node.js 22.18.0 native TypeScript runner 2026-02-27 20:17:39 +01:00
Hugo Alliaume
7eca9dbf13 Drop Biome.js for oxfmt and oxlint 2026-02-03 23:14:09 +01:00
Hugo Alliaume
0c630746cd Run PHP-CS-Fixer (no_useless_else & static_lambda) 2026-02-03 22:36:24 +01:00
Hugo Alliaume
75f6193026 [Autocomplete][Chartjs][Cropperjs][Dropzone][LazyImage][React][StimulusBundle][Svelte][Swup][TogglePassword][Translator][Turbo][Typed][Vue] Use Extension from DependencyInjection instead of HttpKernel 2026-01-31 08:23:54 +01:00
Hugo Alliaume
7d017977d5 Fix npm releases due to repository issue 2026-01-16 23:36:00 +01:00
Hugo Alliaume
f380e2a352 Update versions to 2.32.0 2026-01-16 23:35:37 +01:00
Hugo Alliaume
40e5dee2da Update root JS dependencies 2026-01-11 00:13:28 +01:00
Hugo Alliaume
591df71416 Add changelog entry for #3285 2026-01-10 10:22:44 +01:00
Hugo Alliaume
e1d125ac83 [StimulusBundle][Performance] Change AssetMapper excluded_patterns from **/controllers.json to */controllers.json
Related to https://github.com/symfony/symfony/issues/61771
2026-01-10 09:53:48 +01:00
Hugo Alliaume
dfbf6b443b Git-ignore config/reference.php 2025-12-02 08:12:06 +01:00
Hugo Alliaume
03dfa67783 Update versions to 2.31.0 2025-10-27 23:21:26 +01:00
Quentin Rogeret
c5ea8ee2cc Fix typo in documentation
Asset Mapper 6.3 example is missing a }
2025-09-24 15:27:42 +02:00
Hugo Alliaume
328774d5af Refactor "test_package.sh" to its original purpose, add multiples checks for packages definition 2025-09-20 13:50:04 +02:00
Hugo Alliaume
5a7b1f8f9d Configure .gitattributes to ignore Vitest and Playwright config files from export 2025-09-01 22:55:23 +02:00
github-actions[bot]
bab42de3cd Update versions to 2.30.0 2025-08-27 18:16:44 +00:00
Hugo Alliaume
668b9efe9d Fix changelogs 2025-08-27 17:25:48 +02:00
Hugo Alliaume
42f89354c7 Remove some indirect deprecations 2025-08-25 23:33:06 +02:00
Hugo Alliaume
44ac52b92e Ensure PHP 8.5 compatibility 2025-08-22 16:23:49 +02:00
Hugo Alliaume
c5245dcbac minor #3014 Create E2E app for browsers tests (Kocal)
This PR was merged into the 2.x branch.

Discussion
----------

 Create E2E app for browsers tests

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Docs?         | no <!-- required for new features -->
| Issues        | Fix #3009 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Update/add documentation as required (we can help!)
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

This pull request updates the browser testing workflow and related configuration to improve reliability, consistency, and maintainability across UX packages. The main changes include refactoring the browser test workflow to use a matrix strategy for Symfony versions, standardizing dependency installation steps, removing unused dependencies, and renaming the Playwright configuration for easier reuse.

**Workflow and CI improvements:**

* Refactored `.github/workflows/browser-tests.yml` to use a matrix strategy for Symfony versions, added concurrency controls to cancel in-progress runs, and split setup steps for JS and PHP dependencies, Docker containers, and E2E app configuration. Artifact uploads now only occur on browser test failures. [[1]](diffhunk://#diff-255cac5fcd7ae015d5bc1ccf14bfa2fff33bcabb653402be014e6668db1036ceR23-R38) [[2]](diffhunk://#diff-255cac5fcd7ae015d5bc1ccf14bfa2fff33bcabb653402be014e6668db1036ceL36-R116)
* Standardized JS dependency installation across workflows by replacing direct `pnpm install` commands with named steps (`Install root JS dependencies`) in code quality, unit test, dist files, and release workflows. [[1]](diffhunk://#diff-4a2765c2cfcbd3804a66aab805cb92ddda74de1730923cc5bf53671d0beccf06R27-R36) [[2]](diffhunk://#diff-b117ce55777f198ed74d5eb1cd6319c0b63837e2e9eed5c44b2477658e12248fR24) [[3]](diffhunk://#diff-b117ce55777f198ed74d5eb1cd6319c0b63837e2e9eed5c44b2477658e12248fL32-R38) [[4]](diffhunk://#diff-8e3deeaeb0bdfc6967ff8173f1d99e5001fe75dc497cbfb85fe64ceaade5e399L33-R34) [[5]](diffhunk://#diff-6e608e02c595d53ab6b70822a2bf19abcfc6ddcc976c2f536ad5bfca20f0443fR148) [[6]](diffhunk://#diff-6e608e02c595d53ab6b70822a2bf19abcfc6ddcc976c2f536ad5bfca20f0443fL157-R161)

**Testing and configuration changes:**

* Updated `package.json` to run browser tests in all workspaces concurrently, removed the unused `webdriverio` dependency, and improved the `test:browser` script for workspace aggregation. [[1]](diffhunk://#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519L13-R13) [[2]](diffhunk://#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519L28-R28)
* Renamed `playwright.config.ts` to `playwright.config.base.ts`, added documentation for usage in UX packages, and improved test matching patterns and output directory configuration. [[1]](diffhunk://#diff-8f3b25b652873317fa4aa36b920f753b44dc82f5c1f0d2ff5e6b1781ef1dc90fL1-R30) [[2]](diffhunk://#diff-8f3b25b652873317fa4aa36b920f753b44dc82f5c1f0d2ff5e6b1781ef1dc90fL30-L36)

**Dependency management:**

* Removed `webdriverio` from the lockfile and marked many transitive dependencies as optional in `pnpm-lock.yaml`, reducing the install footprint and improving clarity for unused packages. [[1]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL54-L56) [[2]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR3991) [[3]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4242) [[4]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4260-R4260) [[5]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4270-R4276) [[6]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4429) [[7]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4438-R4451) [[8]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4472-R4480) [[9]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4530) [[10]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4541) [[11]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4543-R4556) [[12]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4578-R4597) [[13]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4597-R4613) [[14]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4622) [[15]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4670) [[16]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4685) [[17]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4704-R4724) [[18]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4733) [[19]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4725-R4756) [[20]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4779-R4793) [[21]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4778-R4807) [[22]](diffhunk://#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL4791-R4828)

These changes make the browser testing workflow more robust and maintainable, ensure consistent dependency installation, and clean up unused or optional packages to streamline CI runs.

Commits
-------

dd1c13aff81 Create E2E app & run it in CI
2025-08-19 20:34:58 +02:00
Hugo Alliaume
57cf364182 Create E2E app & run it in CI 2025-08-19 20:30:58 +02:00
github-actions[bot]
9f33146f3d Update versions to 2.29.2 2025-08-19 12:08:45 +00:00
Hugo Alliaume
7788eaec42 Rename back vitest.config.unit.mjs to vitest.config.mjs 2025-08-18 11:13:24 +02:00
Hugo Alliaume
b6cda5221b Configure Vitest for unit and browser tests (use @puppeteer/browsers and webdriverio) 2025-08-18 08:22:05 +02:00
Hugo Alliaume
f7a29fbff6 Run latest PHP-CS-Fixer with improved configuration 2025-08-14 22:58:41 +02:00
github-actions[bot]
b9fbd1fce0 Update versions to 2.29.1 2025-08-08 12:45:34 +00:00
github-actions[bot]
fedf396824 Update versions to 2.29.0 2025-08-08 11:38:41 +00:00
Hugo Alliaume
6e15880f3b Add support for Symfony 8 2025-08-06 00:04:40 +02:00
github-actions[bot]
435a3c851b Update versions to 2.28.2 2025-07-30 12:24:53 +00:00
Nicolas Grekas
4ebef4b41e Apply fabbot rules 2025-07-29 17:18:27 +02:00
Hugo Alliaume
919d8734e9 Update versions to 2.28.1 2025-07-29 09:04:28 +02:00
Hugo Alliaume
960868a682 Fix package.json files to not use "catalog" feature from PNPM, as it breaks installation from vendor/ PHP packages 2025-07-28 21:36:26 +02:00
Hugo Alliaume
c20fee01ae Modernize and simplify our packages building tools, replace Rollup by tsup 2025-07-27 09:05:51 +02:00
Hugo Alliaume
a031cf8143 Replace Yarn Berry by PNPM 2025-07-21 10:05:53 +02:00
Hugo Alliaume
caef740d94 Explicitly import Vitest APIs instead of relying on globals 2025-07-20 12:48:08 +02:00
Hugo Alliaume
ef937d0a91 Migrate bin/*.js to TypeScript, use tsx 2025-06-28 09:42:29 +02:00
Hugo Alliaume
684aa71734 Migrate PHPUnit configs and homogenize them 2025-06-27 11:05:08 +02:00
github-actions[bot]
c65cf963cd Update versions to 2.27.0 2025-06-24 12:48:07 +00:00
Hugo Alliaume
defaeb91bd Run Biome.js, configure lineWidth to 160 for src/Map/**/*map_controllers.ts 2025-06-22 21:07:55 +02:00
Simon André
1d74f7034c [CI] Add missing return type in test Kernel 2025-06-08 15:02:51 +02:00
github-actions[bot]
42d84c70f9 Update versions to 2.26.1 2025-06-06 20:27:21 +00:00
github-actions[bot]
82c174ebe5 Update versions to 2.26.0 2025-06-05 17:25:17 +00:00
Hugo Alliaume
750c770f66 minor #2707 [StimulusBundle] Docs, replace chart examples by hello to avoid confusion with the ChartJS component (welcoMattic)
This PR was merged into the 2.x branch.

Discussion
----------

[StimulusBundle] Docs, replace `chart` examples by `hello` to avoid confusion with the ChartJS component

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Docs?         | yes <!-- required for new features -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

As ChartJS component exists, the examples using `chart` as controller identifier are confusing. ChartJS component does not expect `name` and `data` as values, but `view`.

Examples has been updated with a dead simple `hello world` style, which can not be confusing.

Commits
-------

77ee62edd8b Replace `chart` examples by `hello` to avoid confusion with the ChartJS component
2025-05-23 11:27:30 +02:00
PHAS Developer
d5abe891c0 [StimulusBundle] Skip mapping .ts controller if .js version is available 2025-05-21 18:38:17 +02:00
Symfony
a0d87ada42 Update versions to 2.25.2 2025-05-20 14:09:00 +00:00
Symfony
5a6aef0646 Update versions to 2.25.1 2025-05-19 11:54:27 +00:00
Symfony
9954aeb3c8 Update versions to 2.25.0 2025-05-19 07:20:25 +00:00
Hugo Alliaume
520ba148f3 [StimulusBundle] Make the JS package private (for real) 2025-05-06 13:28:20 +09:00
Mathieu Santostefano
70175cbecb Replace chart examples by hello to avoid confusion with the ChartJS component 2025-05-05 09:46:58 +02:00
Hugo Alliaume
9d0a21539e [StimulusBundle] Make the JS package private 2025-05-03 23:09:47 +02:00
Hugo Alliaume
92d471a50e Update versions to 2.24.0 2025-04-05 16:55:13 +02:00
Hugo Alliaume
e098403044 Document about alternative JS assets installation with npm packages 2025-03-09 22:10:04 +01:00
Hugo Alliaume
cb7bc7ae54 Document src/**/assets/README.md files about direct installation and recommended alternatives. 2025-03-01 08:17:06 +01:00
Hugo Alliaume
014174ae8a Remove tree from repository URL in each package.json 2025-02-28 23:57:59 +01:00
Hugo Alliaume
84cb6d10af Add README.md file for each UX packages assets 2025-02-28 23:57:59 +01:00
Hugo Alliaume
948f3bb04c Add LICENSE file for each UX packages assets 2025-02-28 23:57:59 +01:00
Hugo Alliaume
1760a45593 Normalize package.json files, to prepare publication on NPM 2025-02-28 23:57:59 +01:00
Bob van de Vijver
ffb1ab446f Add support for @​symfony/stimulus-bridge@​^4.0.0 2025-02-27 16:14:16 +01:00
Simon André
80bfdc877c bug #2590 [StimulusBundle] Fix lazy load Stimulus controllers with Turbo (smnandre)
This PR was merged into the 2.x branch.

Discussion
----------

[StimulusBundle] Fix  lazy load Stimulus controllers with Turbo

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | Fix #2576 / maybe #2583
| License       | MIT

Partial revert of some changes to fix issues with Turbo and lazyload Stimulus controllers

Commits
-------

111fd4cda85 fix: lazy load Stimulus controllers with Turbo
2025-02-24 23:50:41 +01:00
Hugo Alliaume
420c2a8c87 minor #2597 [Doc][StimulusBundle] Misc doc fixes (javiereguiluz)
This PR was merged into the 2.x branch.

Discussion
----------

[Doc][StimulusBundle] Misc doc fixes

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Issues        | -
| License       | MIT

This PR is mostly to fix this minor syntax issue in the doc intro:

<img width="853" alt="" src="https://github.com/user-attachments/assets/47b7e7aa-60fb-4053-af7b-8a911d19f83c" />

But, I also propose to remove the list of UX packages because this is very hard to maintain. For example, we're missing a lot of the packages added recently. So, I think it's better to just add a link to the always up-to-date listing on the website.

Commits
-------

eac8871cbf3 [Doc][StimulusBundle] Misc doc fixes
2025-02-24 23:32:00 +01:00
Tac Tacelosky
d05cf93372 fix missing }, reformat so it's visible
```twig
<div {{ stimulus_controller('chart',
            { 'name': 'Likes', 'data': [1, 2, 3, 4] },
            { 'loading': 'spinner' },
            { 'other': '.target' } ) }}>
```
2025-02-24 23:24:50 +01:00
Javier Eguiluz
f4509ed94a [Doc][StimulusBundle] Misc doc fixes 2025-02-24 09:21:27 +01:00
Simon André
517d5a9b7d fix: lazy load Stimulus controllers with Turbo 2025-02-22 01:18:37 +01:00
Hugo Alliaume
254f4e05cb feature #2463 [Stimulus] Fasten lazy loading + add debug lazy:loading and lazy:loaded (smnandre)
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[Stimulus] Fasten lazy loading + add debug `lazy:loading` and `lazy:loaded`

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | Fix #...
| License       | MIT

This PR..

1. improves the controller **lazy loading** by tuning algorithm (early exit, remove await, ...)

2. introduces two **new debug events** in the console to ease DX with lazy controllers
    * `my-controller#lazy:loading` when the lazy controller is detected in the DOM
    * `my-controller#lazy:loaded` after the file has been downloaded and imported

I would really like some feedback / tests IRL (especially with Webpack i'm not used to)

Commits
-------

3b8e1ce4 [Stimulus] Fasten lazy loading + add debug `lazy:loading` and `lazy:loaded`
2025-01-16 22:55:09 +01:00
Simon André
4659f52ec9 [Stimulus] Fasten lazy loading + add debug lazy:loading and lazy:loaded 2025-01-16 22:55:01 +01:00
Antoine M
14845f8923 doc: swap url for Stimulus Components 2025-01-08 19:59:51 +01:00
Matthieu Lempereur
e13034d428 remove a swup mention for translator 2024-12-06 15:30:33 +01:00
Hugo Alliaume
ec2760a19b [Meta] Drop format, lint, check-format and check-lint commands, replace them with check and ci commands
Co-authored-by: Florian Cellier <florian.cellier@sensiolabs.com>
2024-12-05 15:25:02 +01:00
Florian Cellier
59ff77f1b2 chore: Apply natural import order for JS to synchronise the code base with Biome and its configuration 2024-12-05 10:01:11 +01:00
Hugo Alliaume
2e840a3b12 Add PR template and auto-close PR on subtree split repositories 2024-11-20 08:57:38 +01:00
matlec
af341c2258 [StimulusBundle][Doc] Mention removeComments will no longer be necessary 2024-11-06 21:44:36 +01:00
Hugo Alliaume
d9b83e7132 [DX] Rework testing process, add test script to packages package.json 2024-11-02 20:57:29 +01:00
Hugo Alliaume
692d83891c [DX] Rework building process, and add build/watch scripts to packages package.json 2024-11-02 20:57:17 +01:00
Hugo Alliaume
4658e3bdfa [DX] Add lint/format/check-lint/check-format scripts to packages package.json 2024-11-02 20:20:53 +01:00
matlec
2dc5431fd4 [StimulusBundle] Check controllers source files for laziness 2024-10-28 12:18:16 +01:00
Ole Rößner
60ec83e56c update jetbrains stimulus plugin url. 2024-10-24 15:23:34 +02:00
Simon André
e5f7747b51 Add /doc to .gitattributes export-ignore 2024-10-06 00:11:16 +02:00
Hugo Alliaume
2f1d7a9de9 Clean and homogenize src/*/.gitignore 2024-10-05 23:30:31 +02:00
Stanislau Kviatkouski
4ad4fad427 [StimulusBundle] Normalize Stimulus controller name in event name 2024-09-29 13:34:32 +02:00
Hugo Alliaume
ae69e3a764 [StimulusBundle] Improve StimulusAttributes rendering performances by switching to html escaping strategy 2024-09-24 11:27:42 +02:00
Hugo Alliaume
5e2e1aff3e imp(biomejs): upgrade Biomejs, fix patterns, don't use yarn workspaces 2024-07-30 21:26:23 +02:00
Hugo Alliaume
d35d6cc05f Drop ESLint and Prettier for Biome 2024-07-30 18:00:35 +02:00
Kevin Bond
014a5842e9 minor #1938 Remove declare_strict type (WebMamba)
This PR was merged into the 2.x branch.

Discussion
----------

Remove declare_strict type

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Issues        |
| License       | MIT

This PR removes the declare(strict_types=1);, to follow Symfony convention.
See: https://github.com/Girgias/unify-typing-modes-rfc

I removed the declare(strict_types=1); only for the components and not for the website, in this PR for now.

cc: `@nicolas`-grekas

Commits
-------

73d2ad68 Remove declare_strict type
2024-06-26 10:44:37 -04:00
Simon André
66b88c05d1 [CS] Optimize sprintf calls for PHP 8.4 2024-06-26 10:05:06 -04:00
Matheo Daninos
d4451c162c Remove declare_strict type 2024-06-25 11:04:53 +02:00
Jibé Barth
017b60e036 [Docs][Stimulus] use camelCase for target naming 2024-06-11 15:21:54 +02:00
Kevin Bond
9323437da4 minor #1878 Remove subtree splits check (fabpot)
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

Remove subtree splits check

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | n/a <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

Commits
-------

e02c3fc5 Remove subtree splits check
2024-06-01 13:50:20 -04:00
Fabien Potencier
b3cd2c207f Remove subtree splits check 2024-06-01 13:50:16 -04:00
Ruud Kamphuis
3e1117db76 Use version instead of url in docs 2024-06-01 13:35:12 -04:00
Simon André
e24a12ce43 [Stimulus] Use Twig >3.9 'EscaperRuntime'
Solve CI deprecations
2024-05-03 09:02:49 +02:00
Tac Tacelosky
b828a32fe9 remove duplicate key 2024-04-21 06:23:35 -04:00
Nicolas Grekas
da3cac8797 - 2024-04-19 08:36:45 +02:00
Kevin Bond
1050c8b9f5 minor #1753 [Stimulus] Allow Twig 3.9 (smnandre)
This PR was merged into the 2.x branch.

Discussion
----------

[Stimulus] Allow Twig 3.9

As long Twig or Live component restrict Twig (until we implement yield ready) no need to restrict it here...

... and that would ease IRL tests for Twig&Live :)

Commits
-------

156974cd [Stimulus] Allow Twig 3.9
2024-04-18 16:55:49 -04:00
Kevin Bond
24d9c5356a Auto-close PRs on subtree-splits 2024-04-18 16:44:01 -04:00
Simon André
7f435aaf91 [Stimulus] Allow Twig 3.9
As long Twig or Live component restrict Twig (until we implement yield ready) no need to restrict it here...

... and that would ease IRL tests for Twig&Live :)
2024-04-18 22:02:15 +02:00
Javier Eguiluz
5432e91c00 [Doc] Misc updates in the StimulusBundle docs 2024-03-27 16:55:02 +01:00
Ryan Weaver
6add4bdab1 [Live][Stimulus] Prepping the LiveComponent Stable Release 🚀 2024-02-29 11:20:46 -05:00
Fan2Shrek
602fd521d3 Update error message with php bin/console 2024-02-23 21:55:49 +01:00
Simon André
c113ab8e92 [Twig][Live] Skip Twig 3.9 🚒 (do not set use_yield = true for now) 2024-02-14 11:26:57 -05:00
Evert Harmeling
f36422ff5e [StimulusBundle] Set "use_yield" option to fix deprecation message 2024-02-08 15:49:41 +01:00
Ryan Weaver
f775f6e811 Bumping 2.14.2 CHANGELOG and reverting minor feature temporarily 2024-02-07 15:26:48 -05:00
Ryan Weaver
47fc162b69 minor #1453 CS Update (kbond)
This PR was merged into the 2.x branch.

Discussion
----------

CS Update

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Issues        | n/a
| License       | MIT

Updates to the latest Symfony CS rules.

Refs: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7773 & https://github.com/symfony/symfony/pull/53612

Commits
-------

aad4e17d minor: update cs
2024-02-07 15:16:44 -05:00
Yoann Brieux
683042ea5b Used a Composer class to check for package existence in projects with varying structures 2024-02-07 10:45:33 +01:00
Kevin Bond
3dc86506a4 minor: update cs 2024-02-03 16:01:32 -05:00
Ryan Weaver
da6b4a4f95 Bumping changelog for 2.14.1 2024-02-02 20:11:47 -05:00
tamcy
b3acd6dc4c [StimulusBundle] Handles Windows directory separator when normalizing controller names (fixes #1422) 2024-02-01 15:42:16 +08:00
Ryan Weaver
a7c30d95ff Prepping changelogs for 2.14.0 2024-01-30 10:40:36 -05:00
Ryan Weaver
70e325b71f minor #1345 [StimulusBundle] Added docs on TypeScript support (evertharmeling, weaverryan)
This PR was merged into the 2.x branch.

Discussion
----------

[StimulusBundle] Added docs on TypeScript support

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | #1335 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

Follow-up on #1335, added more info on usage of TypeScript controllers support.

Commits
-------

237164f6 Tweaking wording
60c0b8e2 [StimulusBundle] Added docs about Typescript controller usage
37244016 [StimulusBundle] Revert adding var/ to .gitignore
2024-01-09 09:29:49 -05:00
Ryan Weaver
28f463fbce Tweaking wording 2024-01-09 09:29:38 -05:00
Ryan Weaver
fb37a06629 minor #1336 Add a note about using raw data attributes as a prefered way (bocharsky-bw)
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

Add a note about using raw data attributes as a prefered way

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | no <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

Now with the Stimulus plugin autocomplete in PhpStorm it's better to use raw data attributes instead of built-in functions/filters. Normal Stimulus code is more readable and cleaner IMO.

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

Commits
-------

8fa1e585 Add a note about using raw data attributes as a prefered way
2024-01-09 09:22:56 -05:00
Victor Bocharsky
9701135728 Add a note about using raw data attributes as a prefered way 2024-01-09 09:22:53 -05:00
Simon André
3794315bbf [CS] Update phpcs rules 2024-01-06 22:52:48 -05:00
Evert Harmeling
cb498f202b [StimulusBundle] Added docs about Typescript controller usage 2023-12-19 12:37:50 +01:00
Evert Harmeling
3064da1419 [StimulusBundle] Revert adding var/ to .gitignore 2023-12-19 09:59:03 +01:00
Ryan Weaver
4f68312151 feature #1335 [StimulusBundle] Add support for typescript controllers (evertharmeling)
This PR was merged into the 2.x branch.

Discussion
----------

[StimulusBundle] Add support for typescript controllers

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes <!-- please update src/**/CHANGELOG.md files -->
| Issues        | n/a
| License       | MIT

To be able to use `typescript`-controllers we need to be able to load those controllers. This PR adds support for loading `*.ts` files.

Full support for `typescript`-controllers needs the [sensiolabs/typescript-bundle](https://github.com/sensiolabs/AssetMapperTypeScriptBundle) to compile the controllers.

A reproducer project can be found [here](https://github.com/evertharmeling/typescript-sass-stimulus). It needs this PR to run.

Also see this [slack conversation](https://symfony-devs.slack.com/archives/C01FN4EQNLX/p1702461840204599).

Commits
-------

ce3f0e05 [StimulusBundle] Add support for typescript controllers
2023-12-18 06:40:43 -05:00
Matheo Daninos
369d48e2d3 Fix twig deprecation: twig_escape_filter 2023-12-14 21:57:29 +01:00
Evert Harmeling
b3562b90a3 [StimulusBundle] Add support for typescript controllers 2023-12-13 20:06:49 +01:00
Jibé Barth
40a4416e6d [StimulusBundle] Remove stimulus.asset_mapper.loader_javascript_compiler when no asset-mapper 2023-11-30 21:29:09 +01:00
Ryan Weaver
3a7d47bf33 minor #1265 [StimulusBundle] Removing ux_controller_link_tags() mention for 6.4 (weaverryan)
This PR was merged into the 2.x branch.

Discussion
----------

[StimulusBundle] Removing ux_controller_link_tags() mention for 6.4

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Issues        | None
| License       | MIT

Eventually, we should remove this from the recipe as well - after AssetMapper 6.4 is out. https://github.com/symfony/recipes/blob/main/symfony/stimulus-bundle/2.9/manifest.json#L40

Commits
-------

e81521c2 [StimulusBundle] Removing ux_controller_link_tags() mention for 6.4
2023-11-13 13:33:08 -05:00
Ryan Weaver
2b974b5347 [StimulusBundle] Removing ux_controller_link_tags() mention for 6.4 2023-11-09 14:03:18 -05:00
50 changed files with 716 additions and 478 deletions

5
.gitattributes vendored
View File

@@ -1,7 +1,8 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.git* export-ignore
/.symfony.bundle.yaml export-ignore
/assets/src export-ignore
/assets/test export-ignore
/assets/vitest.config.mjs export-ignore
/doc export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,7 @@
## Please do not submit any Pull Requests here. They will be closed.
Please submit your PR here instead:
https://github.com/symfony/ux
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

View File

@@ -0,0 +1,20 @@
name: Close Pull Request
on:
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/ux
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

13
.gitignore vendored
View File

@@ -1,5 +1,8 @@
.php-cs-fixer.cache
.phpunit.result.cache
composer.lock
vendor/
tests/fixtures/var
/assets/node_modules/
/config/reference.php
/vendor/
/composer.lock
/phpunit.xml
/.phpunit.result.cache
/tests/fixtures/var

View File

@@ -1,3 +1,3 @@
branches: ["2.x"]
maintained_branches: ["2.x"]
doc_dir: "doc"
branches: ['2.x']
maintained_branches: ['2.x']
doc_dir: 'doc'

View File

@@ -1,26 +1,54 @@
# CHANGELOG
## 2.33
- Change AssetMapper `excluded_patterns` from `**/controllers.json` to `*/controllers.json`
## 2.30
- Ensure compatibility with PHP 8.5
## 2.29.0
- Add Symfony 8 support
## 2.20.1
- Normalize Stimulus controller name in event name
## 2.14.2
- Fix bug with finding UX Packages with non-standard project structure
## 2.14.1
- Fixed bug with Stimulus controllers in subdirectories on Windows
## 2.14.0
- Added Typescript controllers support
## 2.13.2
- Revert "Change JavaScript package to `type: module`"
- Revert "Change JavaScript package to `type: module`"
## 2.13.0
- Normalize parameters names given to twig helper 'stimulus_action()'.
**BC Break**: previously, parameters given in camelCase (eg.
`bigCrocodile`) were incorrectly registered by the controller as
flatcase (`event.params.bigcrocodile`). This was fixed, which means
they are now correctly registered as camelCase
(`event.params.bigCrocodile`).
- Added AssetMapper 6.4 support.
- Add Symfony 7 support.
- Fix missing double dash in namespaced Stimulus outlets.
- Change JavaScript package to `type: module`
- Normalize parameters names given to twig helper 'stimulus_action()'.
**BC Break**: previously, parameters given in camelCase (eg.
`bigCrocodile`) were incorrectly registered by the controller as
flatcase (`event.params.bigcrocodile`). This was fixed, which means
they are now correctly registered as camelCase
(`event.params.bigCrocodile`).
- Added AssetMapper 6.4 support.
- Add Symfony 7 support.
- Fix missing double dash in namespaced Stimulus outlets.
- Change JavaScript package to `type: module`
## 2.10.0
- Handle Stimulus outlets
- Handle Stimulus outlets
## 2.9.0
- Introduce the bundle
- Introduce the bundle

View File

@@ -2,9 +2,9 @@
This bundle adds integration between Symfony, Stimulus and Symfony UX:
- A) Twig `stimulus_*` functions & filters to add Stimulus controllers, actions & targets in your templates;
- B) Integration with Symfony UX & AssetMapper (this integration is [experimental](https://symfony.com/doc/current/contributing/code/experimental.html));
- C) A helper service to build the Stimulus data attributes and use them in your services.
- A) Twig `stimulus_*` functions & filters to add Stimulus controllers, actions & targets in your templates;
- B) Integration with Symfony UX & AssetMapper;
- C) A helper service to build the Stimulus data attributes and use them in your services.
[Read the documentation][1]

19
assets/LICENSE Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2023-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

15
assets/README.md Normal file
View File

@@ -0,0 +1,15 @@
# @symfony/stimulus-bundle
JavaScript assets of the [symfony/stimulus-bundle](https://packagist.org/packages/symfony/stimulus-bundle) PHP package.
## Installation
Due to compatibility issues with JSDelivr causing the package not to work as expected, the package is not yet released on NPM.
Read more at [symfony/ux#2708](https://github.com/symfony/ux/issues/2708).
## Resources
- [Documentation](https://symfony.com/bundles/StimulusBundle/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)

View File

@@ -1,12 +1,13 @@
import { ControllerConstructor } from '@hotwired/stimulus';
export interface EagerControllersCollection {
[key: string]: ControllerConstructor;
import { ControllerConstructor } from "@hotwired/stimulus";
interface EagerControllersCollection {
[key: string]: ControllerConstructor;
}
export interface LazyControllersCollection {
[key: string]: () => Promise<{
default: ControllerConstructor;
}>;
interface LazyControllersCollection {
[key: string]: () => Promise<{
default: ControllerConstructor;
}>;
}
export declare const eagerControllers: EagerControllersCollection;
export declare const lazyControllers: LazyControllersCollection;
export declare const isApplicationDebug = false;
declare const eagerControllers: EagerControllersCollection;
declare const lazyControllers: LazyControllersCollection;
declare const isApplicationDebug = false;
export { EagerControllersCollection, LazyControllersCollection, eagerControllers, isApplicationDebug, lazyControllers };

View File

@@ -1,5 +1,4 @@
const eagerControllers = {};
const lazyControllers = {};
const isApplicationDebug = false;
export { eagerControllers, isApplicationDebug, lazyControllers };

View File

@@ -1,4 +1,5 @@
import { Application } from '@hotwired/stimulus';
import { EagerControllersCollection, LazyControllersCollection } from './controllers.js';
export declare const loadControllers: (application: Application, eagerControllers: EagerControllersCollection, lazyControllers: LazyControllersCollection) => void;
export declare const startStimulusApp: () => Application;
import { Application } from "@hotwired/stimulus";
import { EagerControllersCollection, LazyControllersCollection } from "./controllers.js";
declare const loadControllers: (application: Application, eagerControllers: EagerControllersCollection, lazyControllers: LazyControllersCollection) => void;
declare const startStimulusApp: () => Application;
export { loadControllers, startStimulusApp };

131
assets/dist/loader.js vendored
View File

@@ -1,83 +1,70 @@
import { Application } from '@hotwired/stimulus';
import { isApplicationDebug, eagerControllers, lazyControllers } from './controllers.js';
const controllerAttribute = 'data-controller';
import { Application } from "@hotwired/stimulus";
import { eagerControllers, isApplicationDebug, lazyControllers } from "./controllers.js";
const controllerAttribute = "data-controller";
const loadControllers = (application, eagerControllers, lazyControllers) => {
for (const name in eagerControllers) {
registerController(name, eagerControllers[name], application);
}
const lazyControllerHandler = new StimulusLazyControllerHandler(application, lazyControllers);
lazyControllerHandler.start();
for (const name in eagerControllers) registerController(name, eagerControllers[name], application);
new StimulusLazyControllerHandler(application, lazyControllers).start();
};
const startStimulusApp = () => {
const application = Application.start();
application.debug = isApplicationDebug;
loadControllers(application, eagerControllers, lazyControllers);
return application;
const application = Application.start();
application.debug = isApplicationDebug;
loadControllers(application, eagerControllers, lazyControllers);
return application;
};
var StimulusLazyControllerHandler = class {
constructor(application, lazyControllers) {
this.application = application;
this.lazyControllers = lazyControllers;
}
start() {
this.lazyLoadExistingControllers(document.documentElement);
this.lazyLoadNewControllers(document.documentElement);
}
lazyLoadExistingControllers(element) {
Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
loadLazyController(name) {
if (!this.lazyControllers[name]) return;
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];
if (!canRegisterController(name, this.application)) return;
this.application.logDebugActivity(name, "lazy:loading");
controllerLoader().then((controllerModule) => {
this.application.logDebugActivity(name, "lazy:loaded");
registerController(name, controllerModule.default, this.application);
}).catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}
lazyLoadNewControllers(element) {
if (Object.keys(this.lazyControllers).length === 0) return;
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) switch (type) {
case "attributes":
if (attributeName === controllerAttribute && target.getAttribute(controllerAttribute)) extractControllerNamesFrom(target).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
break;
case "childList": this.lazyLoadExistingControllers(target);
}
}).observe(element, {
attributeFilter: [controllerAttribute],
subtree: true,
childList: true
});
}
};
class StimulusLazyControllerHandler {
constructor(application, lazyControllers) {
this.application = application;
this.lazyControllers = lazyControllers;
}
start() {
this.lazyLoadExistingControllers(document.documentElement);
this.lazyLoadNewControllers(document.documentElement);
}
lazyLoadExistingControllers(element) {
this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
}
async loadLazyController(name) {
if (canRegisterController(name, this.application)) {
if (this.lazyControllers[name] === undefined) {
return;
}
const controllerModule = await this.lazyControllers[name]();
registerController(name, controllerModule.default, this.application);
}
}
lazyLoadNewControllers(element) {
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
case 'attributes': {
if (attributeName === controllerAttribute &&
target.getAttribute(controllerAttribute)) {
extractControllerNamesFrom(target).forEach((controllerName) => this.loadLazyController(controllerName));
}
break;
}
case 'childList': {
this.lazyLoadExistingControllers(target);
}
}
}
}).observe(element, {
attributeFilter: [controllerAttribute],
subtree: true,
childList: true,
});
}
queryControllerNamesWithin(element) {
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.map(extractControllerNamesFrom)
.flat();
}
}
function registerController(name, controller, application) {
if (canRegisterController(name, application)) {
application.register(name, controller);
}
if (canRegisterController(name, application)) application.register(name, controller);
}
function extractControllerNamesFrom(element) {
const controllerNameValue = element.getAttribute(controllerAttribute);
if (!controllerNameValue) {
return [];
}
return controllerNameValue.split(/\s+/).filter((content) => content.length);
const controllerNameValue = element.getAttribute(controllerAttribute);
if (!controllerNameValue) return [];
return controllerNameValue.split(/\s+/).filter((content) => content.length);
}
function canRegisterController(name, application) {
return !application.router.modulesByIdentifier.has(name);
return !application.router.modulesByIdentifier.has(name);
}
export { loadControllers, startStimulusApp };

View File

@@ -1,9 +1,25 @@
{
"name": "@symfony/stimulus-bundle",
"description": "Integration of @hotwired/stimulus into Symfony",
"version": "1.0.0",
"private": true,
"license": "MIT",
"version": "2.34.0",
"keywords": [
"symfony-ux"
],
"homepage": "https://ux.symfony.com/stimulus",
"repository": "https://github.com/symfony/ux",
"type": "module",
"files": [
"dist"
],
"main": "dist/loader.js",
"scripts": {
"build": "node ../../../bin/build_package.ts .",
"watch": "node ../../../bin/build_package.ts . --watch",
"test": "pnpm run test:unit && pnpm run test:browser",
"test:unit": "../../../bin/unit_test_package.sh ."
},
"symfony": {
"needsPackageAsADependency": false,
"importmap": {
@@ -13,6 +29,15 @@
},
"peerDependencies": {
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.0"
"@symfony/stimulus-bridge": "^3.2.0 || ^4.0.0"
},
"devDependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"jsdom": "^26.1.0",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"vitest": "^4.1.0"
}
}

View File

@@ -8,7 +8,7 @@
*/
// This file is dynamically rewritten by StimulusBundle + AssetMapper.
import { ControllerConstructor } from '@hotwired/stimulus';
import type { ControllerConstructor } from '@hotwired/stimulus';
export interface EagerControllersCollection {
[key: string]: ControllerConstructor;

View File

@@ -12,13 +12,13 @@
*
* Inspired by stimulus-loading.js from stimulus-rails.
*/
import { Application, ControllerConstructor } from '@hotwired/stimulus';
import { Application, type ControllerConstructor } from '@hotwired/stimulus';
import {
type EagerControllersCollection,
eagerControllers,
lazyControllers,
isApplicationDebug,
EagerControllersCollection,
LazyControllersCollection,
type LazyControllersCollection,
lazyControllers,
} from './controllers.js';
const controllerAttribute = 'data-controller';
@@ -64,22 +64,42 @@ class StimulusLazyControllerHandler {
}
private lazyLoadExistingControllers(element: Element) {
this.queryControllerNamesWithin(element).forEach((controllerName) => this.loadLazyController(controllerName));
Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.flatMap(extractControllerNamesFrom)
.forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
private async loadLazyController(name: string) {
if (canRegisterController(name, this.application)) {
if (this.lazyControllers[name] === undefined) {
return;
}
const controllerModule = await this.lazyControllers[name]();
registerController(name, controllerModule.default, this.application);
private loadLazyController(name: string): void {
if (!this.lazyControllers[name]) {
return;
}
// Delete the loader to avoid loading it twice
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];
if (!canRegisterController(name, this.application)) {
return;
}
this.application.logDebugActivity(name, 'lazy:loading');
controllerLoader()
.then((controllerModule) => {
this.application.logDebugActivity(name, 'lazy:loaded');
registerController(name, controllerModule.default, this.application);
})
.catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}
private lazyLoadNewControllers(element: Element) {
if (Object.keys(this.lazyControllers).length === 0) {
return;
}
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
@@ -88,9 +108,9 @@ class StimulusLazyControllerHandler {
attributeName === controllerAttribute &&
(target as Element).getAttribute(controllerAttribute)
) {
extractControllerNamesFrom(target as Element).forEach((controllerName) =>
this.loadLazyController(controllerName)
);
extractControllerNamesFrom(target as Element).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
break;
@@ -107,12 +127,6 @@ class StimulusLazyControllerHandler {
childList: true,
});
}
private queryControllerNamesWithin(element: Element): string[] {
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.map(extractControllerNamesFrom)
.flat();
}
}
function registerController(name: string, controller: ControllerConstructor, application: Application) {
@@ -132,6 +146,6 @@ function extractControllerNamesFrom(element: Element): string[] {
}
function canRegisterController(name: string, application: Application) {
// @ts-ignore
// @ts-expect-error
return !application.router.modulesByIdentifier.has(name);
}

View File

@@ -1,12 +1,10 @@
import { Application, Controller } from '@hotwired/stimulus';
import { waitFor } from '@testing-library/dom';
import { describe, expect, it } from 'vitest';
// load from dist because the source TypeScript file points directly to controllers.js,
// which does not actually exist in the source code
import { loadControllers } from '../dist/loader';
import { Application, Controller } from '@hotwired/stimulus';
import {
EagerControllersCollection,
LazyControllersCollection,
} from '../src/controllers';
import { waitFor } from '@testing-library/dom';
import { loadControllers } from '../../dist/loader';
import type { EagerControllersCollection, LazyControllersCollection } from '../../src/controllers';
let isController1Initialized = false;
let isController2Initialized = false;
@@ -37,11 +35,11 @@ describe('loader', () => {
const application = Application.start();
const eagerControllers: EagerControllersCollection = {
'controller1': controller1,
'controller2': controller2,
controller1,
controller2,
};
const lazyControllers: LazyControllersCollection = {
'controller3': () => Promise.resolve({ default: controller3 }),
controller3: () => Promise.resolve({ default: controller3 }),
};
loadControllers(application, eagerControllers, lazyControllers);
@@ -52,7 +50,7 @@ describe('loader', () => {
document.body.innerHTML = '<div data-controller="controller3"></div>';
// wait a moment for the MutationObserver to fire
await new Promise(resolve => setTimeout(resolve, 10));
await new Promise((resolve) => setTimeout(resolve, 10));
expect(isController3Initialized).toBe(true);
application.stop();

3
assets/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../../../tsconfig.package.json"
}

4
assets/vitest.config.mjs Normal file
View File

@@ -0,0 +1,4 @@
import { mergeConfig } from 'vitest/config';
import configShared from '../../../vitest.config.base.mjs';
export default mergeConfig(configShared, {});

View File

@@ -14,18 +14,18 @@
],
"require": {
"php": ">=8.1",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/finder": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0",
"twig/twig": "^2.15.3|^3.4.3",
"symfony/config": "^5.4|^6.0|^7.0|^8.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
"symfony/finder": "^5.4|^6.0|^7.0|^8.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0",
"twig/twig": "^2.15.3|^3.8",
"symfony/deprecation-contracts": "^2.0|^3.0"
},
"require-dev": {
"symfony/asset-mapper": "^6.3|^7.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0",
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
"zenstruck/browser": "^1.4"
},
"minimum-stability": "dev",

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*

View File

@@ -7,11 +7,9 @@ StimulusBundle: Symfony integration with Stimulus
This bundle adds integration between Symfony, `Stimulus`_ and the Symfony UX packages:
A) Twig ``stimulus_`` functions & filters to add Stimulus controllers,
actions & targets in your templates;
B) Integration to load :ref:`UX Packages <ux-packages>` (extra Stimulus controllers)
(if you're using AssetMapper, this integration is `experimental`_)
* Twig ``stimulus_`` functions & filters to add Stimulus controllers,
actions & targets in your templates;
* Integration to load :ref:`UX Packages <ux-packages>` (extra Stimulus controllers)
Installation
------------
@@ -19,11 +17,11 @@ Installation
First, if you don't have one yet, choose and install an asset handling system;
both work great with StimulusBundle:
* A) `Webpack Encore`_ Node-based packaging system:
* `AssetMapper`_: PHP-based system for handling assets
or
* B) `AssetMapper`_: PHP-based system for handling assets:
* `Webpack Encore`_ Node-based packaging system
See `Encore vs AssetMapper`_ to learn which is best for your project.
@@ -44,7 +42,7 @@ necessary files. If not, or you're curious, see :ref:`Manual Setup <manual-insta
Usage
-----
You can now create custom Stimulus controllers inside of the ``assets/controllers.``
You can now create custom Stimulus controllers inside of the ``assets/controllers``
directory. In fact, you should have an example controller there already: ``hello_controller.js``:
.. code-block:: javascript
@@ -57,7 +55,15 @@ directory. In fact, you should have an example controller there already: ``hello
}
}
Use the Twig functions from this bundle to activate your controllers:
Then, activate the controller in your HTML:
.. code-block:: html+twig
<div data-controller="hello">
...
</div>
Optionally, this bundle has a Twig function to render the attribute:
.. code-block:: html+twig
@@ -65,12 +71,25 @@ Use the Twig functions from this bundle to activate your controllers:
...
</div>
<!-- would render -->
<div data-controller="hello">
...
</div>
That's it! Whenever this element appears on the page, the ``hello`` controller
will activate.
There's a *lot* more to learn about Stimulus. See the `Stimulus Documentation`_
for all the goodies.
TypeScript Controllers
~~~~~~~~~~~~~~~~~~~~~~
If you want to use `TypeScript`_ to define your controllers, you can! Install and set up the
`sensiolabs/typescript-bundle`_. Then be sure to add the ``assets/controllers`` path to the
``sensiolabs_typescript.source_dir`` configuration. Finally, create your controller in that
directory and you're good to go.
.. _ux-packages:
The UX Packages
@@ -81,33 +100,7 @@ common problems. StimulusBundle activates any 3rd party Stimulus controllers
that are mentioned in your ``assets/controllers.json`` file. This file is updated
whenever you install a UX package.
The official UX packages are:
* `ux-autocomplete`_: Transform ``EntityType``, ``ChoiceType`` or *any*
``<select>`` element into an Ajax-powered autocomplete field
(`see demo <https://ux.symfony.com/autocomplete>`_)
* `ux-chartjs`_: Easy charts with `Chart.js`_ (`see demo <https://ux.symfony.com/chartjs>`_)
* `ux-cropperjs`_: Form Type and tools for cropping images (`see demo <https://ux.symfony.com/cropperjs>`_)
* `ux-dropzone`_: Form Type for stylized "drop zone" for file uploads
(`see demo <https://ux.symfony.com/dropzone>`_)
* `ux-lazy-image`_: Optimize Image Loading with BlurHash
(`see demo <https://ux.symfony.com/lazy-image>`_)
* `ux-live-component`_: Build Dynamic Interfaces with Zero JavaScript
(`see demo <https://ux.symfony.com/live-component>`_)
* `ux-notify`_: Send server-sent native notification with Mercure
(`see demo <https://ux.symfony.com/notify>`_)
* `ux-react`_: Render `React`_ component from Twig (`see demo <https://ux.symfony.com/react>`_)
* `ux-svelte`_: Render `Svelte`_ component from Twig (`see demo <https://ux.symfony.com/svelte>`_)
* `ux-swup`_: Integration with `Swup`_ (`see demo <https://ux.symfony.com/swup>`_)
* `ux-toggle-password`_: Toggle visibility of password inputs
(`see demo <https://ux.symfony.com/toggle-password>`_)
* `ux-translator`_: Use your Symfony translations in JavaScript `Swup`_ (`see demo <https://ux.symfony.com/translator>`_)
* `ux-turbo`_: Integration with `Turbo Drive`_ for a single-page-app experience
(`see demo <https://ux.symfony.com/turbo>`_)
* `ux-twig-component`_: Build Twig Components Backed by a PHP Class
(`see demo <https://ux.symfony.com/twig-component>`_)
* `ux-typed`_: Integration with `Typed`_ (`see demo <https://ux.symfony.com/typed>`_)
* `ux-vue`_: Render `Vue`_ component from Twig (`see demo <https://ux.symfony.com/vue>`_)
Check out the `official UX packages`_.
Lazy Stimulus Controllers
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -117,7 +110,7 @@ controllers in ``assets/controllers.json``) will be downloaded and loaded on
every page.
Sometimes you may have a controller that's only used on some pages. In that case,
you can make the controller "lazy". In this case, will *not be downloaded on
you can make the controller "lazy". In this case, will *not* be downloaded on
initial page load. Instead, as soon as an element appears on the page matching
the controller (e.g. ``<div data-controller="hello">``), the controller - and anything
else it imports - will be lazily-loaded via Ajax.
@@ -138,8 +131,9 @@ To make a third-party controller lazy, in ``assets/controllers.json``, set
.. note::
If you write your controllers using TypeScript, make sure
``removeComments`` is not set to ``true`` in your TypeScript config.
If you write your controllers using TypeScript and you're using
StimulusBundle 2.21.0 or earlier, make sure ``removeComments`` is not set
to ``true`` in your TypeScript config.
Stimulus Tools around the World
-------------------------------
@@ -156,8 +150,18 @@ exist beyond the UX packages:
Stimulus Twig Helpers
---------------------
This bundle adds 3 Twig functions/filters to help add Stimulus controllers,
actions & targets in your templates.
This bundle adds some Twig functions/filters to help add Stimulus controllers,
actions and targets in your templates.
.. note::
Though this bundle provides these helpful Twig functions/filters, it's
recommended to use raw data attributes instead, as they're straightforward.
.. tip::
If you use PhpStorm IDE - you may want to install `Stimulus plugin`_
to get nice auto-completion for the attributes.
stimulus_controller
~~~~~~~~~~~~~~~~~~~
@@ -170,15 +174,15 @@ For example:
.. code-block:: html+twig
<div {{ stimulus_controller('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }) }}>
<div {{ stimulus_controller('hello', { 'name': 'World', 'data': [1, 2, 3, 4] }) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="chart"
data-chart-name-value="Likes"
data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="&#x5B;1,2,3,4&#x5D;"
>
Hello
</div>
@@ -187,22 +191,22 @@ If you want to set CSS classes:
.. code-block:: html+twig
<div {{ stimulus_controller('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }) }}>
<div {{ stimulus_controller('hello', { 'name': 'World', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="chart"
data-chart-name-value="Likes"
data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
data-chart-loading-class="spinner"
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="&#x5B;1,2,3,4&#x5D;"
data-hello-loading-class="spinner"
>
Hello
</div>
<!-- or without values -->
<div {{ stimulus_controller('chart', controllerClasses = { 'loading': 'spinner' }) }}>
<div {{ stimulus_controller('hello', controllerClasses: { 'loading': 'spinner' }) }}>
Hello
</div>
@@ -210,23 +214,26 @@ And with outlets:
.. code-block:: html+twig
<div {{ stimulus_controller('chart', { 'name': 'Likes', 'data': [1, 2, 3, 4] }, { 'loading': 'spinner' }, { 'other': '.target' ) }}>
<div {{ stimulus_controller('hello',
{ 'name': 'World', 'data': [1, 2, 3, 4] },
{ 'loading': 'spinner' },
{ 'other': '.target' } ) }}>
Hello
</div>
<!-- would render -->
<div
data-controller="chart"
data-chart-name-value="Likes"
data-chart-data-value="&#x5B;1,2,3,4&#x5D;"
data-chart-loading-class="spinner"
data-chart-other-outlet=".target"
data-controller="hello"
data-hello-name-value="World"
data-hello-data-value="&#x5B;1,2,3,4&#x5D;"
data-hello-loading-class="spinner"
data-hello-other-outlet=".target"
>
Hello
</div>
<!-- or without values/classes -->
<div {{ stimulus_controller('chart', controllerOutlets = { 'other': '.target' }) }}>
<div {{ stimulus_controller('hello', controllerOutlets: { 'other': '.target' }) }}>
Hello
</div>
@@ -239,7 +246,12 @@ there's also a ``stimulus_controller`` filter:
.. code-block:: html+twig
<div {{ stimulus_controller('chart', { 'name': 'Likes' })|stimulus_controller('other-controller') }}>
<div {{ stimulus_controller('hello', { 'name': 'World' })|stimulus_controller('other-controller') }}>
Hello
</div>
<!-- would render -->
<div data-controller="hello other-controller" data-hello-name-value="World">
Hello
</div>
@@ -247,7 +259,7 @@ You can also retrieve the generated attributes as an array, which can be helpful
.. code-block:: twig
{{ form_start(form, { attr: stimulus_controller('chart', { 'name': 'Likes' }).toArray() }) }}
{{ form_start(form, { attr: stimulus_controller('hello', { 'name': 'World' }).toArray() }) }}
stimulus_action
~~~~~~~~~~~~~~~
@@ -303,24 +315,24 @@ For example:
.. code-block:: html+twig
<div {{ stimulus_target('controller', 'a-target') }}>Hello</div>
<div {{ stimulus_target('controller', 'a-target second-target') }}>Hello</div>
<div {{ stimulus_target('controller', 'myTarget') }}>Hello</div>
<div {{ stimulus_target('controller', 'myTarget secondTarget') }}>Hello</div>
<!-- would render -->
<div data-controller-target="a-target">Hello</div>
<div data-controller-target="a-target second-target">Hello</div>
<div data-controller-target="myTarget">Hello</div>
<div data-controller-target="myTarget secondTarget">Hello</div>
If you have multiple targets on the same element, you can chain them as there's
also a ``stimulus_target`` filter:
.. code-block:: html+twig
<div {{ stimulus_target('controller', 'a-target')|stimulus_target('other-controller', 'another-target') }}>
<div {{ stimulus_target('controller', 'myTarget')|stimulus_target('other-controller', 'anotherTarget') }}>
Hello
</div>
<!-- would render -->
<div data-controller-target="a-target" data-other-controller-target="another-target">
<div data-controller-target="myTarget" data-other-controller-target="anotherTarget">
Hello
</div>
@@ -328,7 +340,7 @@ You can also retrieve the generated attributes as an array, which can be helpful
.. code-block:: twig
{{ form_row(form.password, { attr: stimulus_target('hello-controller', 'a-target').toArray() }) }}
{{ form_row(form.password, { attr: stimulus_target('hello-controller', 'myTarget').toArray() }) }}
.. _configuration:
@@ -371,6 +383,44 @@ the `StimulusBundle Flex recipe`_. Here's a summary of what's inside:
A few other changes depend on which asset system you're using:
With AssetMapper
~~~~~~~~~~~~~~~~
If you're using AssetMapper, two new entries will be added to your ``importmap.php``
file::
// importmap.php
return [
// ...
'@symfony/stimulus-bundle' => [
'path' => '@symfony/stimulus-bundle/loader.js',
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
];
The recipe will update your ``assets/bootstrap.js`` file to look like this:
.. code-block:: javascript
// assets/bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
The ``@symfony/stimulus-bundle`` refers the one of the new entries in your
``importmap.php`` file. This file is dynamically built by the bundle and
will import all your custom controllers as well as those from ``controllers.json``.
It will also dynamically enable "debug" mode in Stimulus when your application
is running in debug mode.
.. tip::
For AssetMapper 6.3 only, you also need a ``{{ ux_controller_link_tags() }}``
in ``base.html.twig``. This is not needed in AssetMapper 6.4+.
With WebpackEncoreBundle
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -399,50 +449,6 @@ The ``assets/bootstrap.js`` file will be updated to look like this:
And 2 new packages - ``@hotwired/stimulus`` and ``@symfony/stimulus-bridge`` - will
be added to your ``package.json`` file.
With AssetMapper
~~~~~~~~~~~~~~~~
If you're using AssetMapper, two new entries will be added to your ``importmap.php``
file::
// importmap.php
return [
// ...
'@symfony/stimulus-bundle' => [
'path' => '@symfony/stimulus-bundle/loader.js',
],
'@hotwired/stimulus' => [
'url' => 'https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js',
],
];
The recipe will update your ``assets/bootstrap.js`` file to look like this:
.. code-block:: javascript
// assets/bootstrap.js
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
The ``@symfony/stimulus-bundle`` refers the one of the new entries in your
``importmap.php`` file. This file is dynamically built by the bundle and
will import all your custom controllers as well as those from ``controllers.json``.
It will also dynamically enable "debug" mode in Stimulus when your application
is running in debug mode.
Finally, to output any ``autoimport`` CSS files in your ``controllers.json`` file,
include the ``ux_controller_link_tags()`` function in your base template:
.. code-block:: html+twig
{% block stylesheets %}
{{ ux_controller_link_tags() }}
<!-- ... -->
{% endblock %}
How are the Stimulus Controllers Loaded?
----------------------------------------
@@ -521,29 +527,9 @@ it will normalize it:
.. _`parameters`: https://stimulus.hotwired.dev/reference/actions#action-parameters
.. _`Stimulus Targets`: https://stimulus.hotwired.dev/reference/targets
.. _`StimulusBundle Flex recipe`: https://github.com/symfony/recipes/tree/main/symfony/stimulus-bundle
.. _`experimental`: https://symfony.com/doc/current/contributing/code/experimental.html
.. _`ux-autocomplete`: https://symfony.com/bundles/ux-autocomplete/current/index.html
.. _`ux-chartjs`: https://symfony.com/bundles/ux-chartjs/current/index.html
.. _`ux-cropperjs`: https://symfony.com/bundles/ux-cropperjs/current/index.html
.. _`ux-dropzone`: https://symfony.com/bundles/ux-dropzone/current/index.html
.. _`ux-lazy-image`: https://symfony.com/bundles/ux-lazy-image/current/index.html
.. _`ux-live-component`: https://symfony.com/bundles/ux-live-component/current/index.html
.. _`ux-notify`: https://symfony.com/bundles/ux-notify/current/index.html
.. _`ux-react`: https://symfony.com/bundles/ux-react/current/index.html
.. _ux-translator: https://symfony.com/bundles/ux-translator/current/index.html
.. _`ux-swup`: https://symfony.com/bundles/ux-swup/current/index.html
.. _`ux-toggle-password`: https://symfony.com/bundles/ux-toggle-password/current/index.html
.. _`ux-turbo`: https://symfony.com/bundles/ux-turbo/current/index.html
.. _`ux-twig-component`: https://symfony.com/bundles/ux-twig-component/current/index.html
.. _`ux-typed`: https://symfony.com/bundles/ux-typed/current/index.html
.. _`ux-vue`: https://symfony.com/bundles/ux-vue/current/index.html
.. _`ux-svelte`: https://symfony.com/bundles/ux-svelte/current/index.html
.. _`Chart.js`: https://www.chartjs.org/
.. _`Swup`: https://swup.js.org/
.. _`React`: https://reactjs.org/
.. _`Svelte`: https://svelte.dev/
.. _`Turbo Drive`: https://turbo.hotwired.dev/
.. _`Typed`: https://github.com/mattboldt/typed.js/
.. _`Vue`: https://vuejs.org/
.. _`stimulus-use`: https://stimulus-use.github.io/stimulus-use
.. _`stimulus-components`: https://stimulus-components.netlify.app/
.. _`stimulus-components`: https://www.stimulus-components.com/
.. _`TypeScript`: https://www.typescriptlang.org/
.. _`sensiolabs/typescript-bundle`: https://github.com/sensiolabs/AssetMapperTypeScriptBundle
.. _`Stimulus plugin`: https://plugins.jetbrains.com/plugin/24562-stimulus
.. _`official UX packages`: https://ux.symfony.com/packages

View File

@@ -1,38 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_CLASS" value="Symfony\UX\Autocomplete\Tests\Fixtures\Kernel" />
<server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db" />
<ini name="error_reporting" value="-1"/>
<env name="SHELL_VERBOSITY" value="-1"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
<env name="KERNEL_CLASS" value="Symfony\UX\Autocomplete\Tests\Fixtures\Kernel"/>
<env name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db"/>
</php>
<testsuites>
<testsuite name="symfony/ux-autocomplete Test Suite">
<directory>./tests/</directory>
<testsuite name="Symfony UX Stimulus Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<coverage>
<include>
<directory>./src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>
<extensions>
<extension class="Zenstruck\Browser\Test\BrowserExtension" />
<extension class="Zenstruck\Browser\Test\BrowserExtension"/>
</extensions>
</phpunit>

View File

@@ -37,12 +37,12 @@ class AutoImportLocator
$slashPosition = strpos($path, '/');
if (false === $slashPosition) {
throw new \LogicException(sprintf('The autoimport "%s" is not valid.', $path));
throw new \LogicException(\sprintf('The autoimport "%s" is not valid.', $path));
}
$parts = explode('/', ltrim($path, '@'));
if (2 > \count($parts)) {
throw new \LogicException(sprintf('The autoimport "%s" is not valid.', $path));
throw new \LogicException(\sprintf('The autoimport "%s" is not valid.', $path));
}
$package = implode('/', \array_slice($parts, 0, 2));
$file = implode('/', \array_slice($parts, 2));
@@ -51,12 +51,12 @@ class AutoImportLocator
// this is a file local to the ux package
$filePath = $packageMetadata->packageDirectory.'/'.$file;
if (!is_file($filePath)) {
throw new \LogicException(sprintf('An "autoimport" in "controllers.json" refers to "%s". This path could not be found in the asset mapper and the file "%s" does not exist in the package path "%s". And so, the file cannot be loaded.', $path, $filePath, $packageMetadata->packageDirectory));
throw new \LogicException(\sprintf('An "autoimport" in "controllers.json" refers to "%s". This path could not be found in the asset mapper and the file "%s" does not exist in the package path "%s". And so, the file cannot be loaded.', $path, $filePath, $packageMetadata->packageDirectory));
}
$asset = $this->assetMapper->getAssetFromSourcePath($filePath);
if (!$asset) {
throw new \LogicException(sprintf('An "autoimport" in "controllers.json" refers to "%s". This file was found, but the path is not in the asset mapper. And so, the file cannot be loaded. This is a misconfiguration with the bundle providing this.', $path));
throw new \LogicException(\sprintf('An "autoimport" in "controllers.json" refers to "%s". This file was found, but the path is not in the asset mapper. And so, the file cannot be loaded. This is a misconfiguration with the bundle providing this.', $path));
}
return new MappedControllerAutoImport($asset->sourcePath, false);
@@ -64,7 +64,7 @@ class AutoImportLocator
$entry = $this->importMapConfigReader->findRootImportMapEntry($path);
if (!$entry) {
throw new \LogicException(sprintf('The autoimport "%s" could not be found in importmap.php. Try running "importmap:require %s".', $path, $path));
throw new \LogicException(\sprintf('The autoimport "%s" could not be found in importmap.php. Try running "php bin/console importmap:require %s".', $path, $path));
}
return new MappedControllerAutoImport($path, true);

View File

@@ -22,12 +22,12 @@ use Symfony\UX\StimulusBundle\Ux\UxPackageReader;
*
* @internal
*
* @experimental
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class ControllersMapGenerator
{
private const FILENAME_REGEX = '/^.*[-_](controller\.[jt]s)$/';
public function __construct(
private AssetMapperInterface $assetMapper,
private UxPackageReader $uxPackageReader,
@@ -66,16 +66,27 @@ class ControllersMapGenerator
$finder = new Finder();
$finder->in($this->controllerPaths)
->files()
->name('/^.*[-_]controller\.js$/');
->name(self::FILENAME_REGEX);
$controllersMap = [];
foreach ($finder as $file) {
// Skip .ts controller if .js version is available
if ('ts' === $file->getExtension() && file_exists(substr($file->getRealPath(), 0, -2).'js')) {
continue;
}
$name = $file->getRelativePathname();
$name = str_replace(['_controller.js', '-controller.js'], '', $name);
$name = str_replace(['_', '/'], ['-', '--'], $name);
// use regex to extract 'controller'-postfix including extension
preg_match(self::FILENAME_REGEX, $name, $matches);
$name = str_replace(['_'.$matches[1], '-'.$matches[1]], '', $name);
$name = str_replace(['_', '/', '\\'], ['-', '--', '--'], $name);
$asset = $this->assetMapper->getAssetFromSourcePath($file->getRealPath());
$content = $asset->content ?: file_get_contents($asset->sourcePath);
if (!$asset) {
throw new \RuntimeException(\sprintf('Could not find an asset mapper path that points to the "%s" controller.', $name));
}
$content = file_get_contents($asset->sourcePath);
$isLazy = preg_match('/\/\*\s*stimulusFetch:\s*\'lazy\'\s*\*\//i', $content);
$controllersMap[$name] = new MappedControllerAsset($asset, $isLazy);
@@ -106,7 +117,7 @@ class ControllersMapGenerator
$packageControllerConfig = $packageMetadata->symfonyConfig['controllers'][$controllerName] ?? null;
if (null === $packageControllerConfig) {
throw new \RuntimeException(sprintf('Controller "%s" does not exist in the "%s" package.', $controllerReference, $packageMetadata->packageName));
throw new \RuntimeException(\sprintf('Controller "%s" does not exist in the "%s" package.', $controllerReference, $packageMetadata->packageName));
}
if (!$localControllerConfig['enabled']) {
@@ -130,7 +141,7 @@ class ControllersMapGenerator
$asset = $this->assetMapper->getAssetFromSourcePath($controllerMainPath);
if (!$asset) {
throw new \RuntimeException(sprintf('Could not find an asset mapper path that points to the "%s" controller in package "%s", defined in controllers.json.', $controllerName, $packageMetadata->packageName));
throw new \RuntimeException(\sprintf('Could not find an asset mapper path that points to the "%s" controller in package "%s", defined in controllers.json.', $controllerName, $packageMetadata->packageName));
}
$autoImports = $this->collectAutoImports($localControllerConfig['autoimport'] ?? [], $packageMetadata);
@@ -152,7 +163,7 @@ class ControllersMapGenerator
return [];
}
if (null === $this->autoImportLocator) {
throw new \InvalidArgumentException(sprintf('The "autoImportLocator" argument to "%s" is required when using AssetMapper 6.4', self::class));
throw new \InvalidArgumentException(\sprintf('The "autoImportLocator" argument to "%s" is required when using AssetMapper 6.4', self::class));
}
$autoImportItems = [];

View File

@@ -14,9 +14,9 @@ namespace Symfony\UX\StimulusBundle\AssetMapper;
use Symfony\Component\AssetMapper\MappedAsset;
/**
* @experimental
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @internal
*/
class MappedControllerAsset
{

View File

@@ -12,7 +12,7 @@
namespace Symfony\UX\StimulusBundle\AssetMapper;
/**
* @experimental
* @internal
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
@@ -20,7 +20,7 @@ class MappedControllerAutoImport
{
public function __construct(
public string $path,
public bool $isBareImport
public bool $isBareImport,
) {
}
}

View File

@@ -20,7 +20,7 @@ use Symfony\Component\Filesystem\Path;
/**
* Compiles the loader.js file to dynamically import the controllers.
*
* @experimental
* @internal
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
@@ -91,43 +91,43 @@ class StimulusLoaderJavaScriptCompiler implements AssetCompilerInterface
if ($mappedControllerAsset->isLazy) {
if (!$mappedControllerAsset->autoImports) {
$lazyControllers[] = sprintf('%s: () => import(%s)', json_encode($name), $relativeImportPath);
$lazyControllers[] = \sprintf('%s: () => import(%s)', json_encode($name), $relativeImportPath);
} else {
// import $relativeImportPath and also the auto-imports
// and use a Promise.all() to wait for all of them
$lazyControllers[] = sprintf('%s: () => Promise.all([import(%s), %s]).then((ret) => ret[0])', json_encode($name), $relativeImportPath, implode(', ', array_map(fn ($path) => "import($path)", $autoImportPaths)));
$lazyControllers[] = \sprintf('%s: () => Promise.all([import(%s), %s]).then((ret) => ret[0])', json_encode($name), $relativeImportPath, implode(', ', array_map(static fn ($path) => "import($path)", $autoImportPaths)));
}
continue;
}
$controllerNameForVariable = sprintf('controller_%s', \count($eagerControllerParts));
$controllerNameForVariable = \sprintf('controller_%s', \count($eagerControllerParts));
$importLines[] = sprintf(
$importLines[] = \sprintf(
'import %s from %s;',
$controllerNameForVariable,
$relativeImportPath
);
foreach ($autoImportPaths as $autoImportRelativePath) {
$importLines[] = sprintf(
$importLines[] = \sprintf(
'import %s;',
$autoImportRelativePath
);
}
$eagerControllerParts[] = sprintf('"%s": %s', $name, $controllerNameForVariable);
$eagerControllerParts[] = \sprintf('"%s": %s', $name, $controllerNameForVariable);
}
$importCode = implode("\n", $importLines);
$eagerControllersJson = sprintf('{%s}', implode(', ', $eagerControllerParts));
$lazyControllersExpression = sprintf('{%s}', implode(', ', $lazyControllers));
$eagerControllersJson = \sprintf('{%s}', implode(', ', $eagerControllerParts));
$lazyControllersExpression = \sprintf('{%s}', implode(', ', $lazyControllers));
$isDebugString = $this->isDebug ? 'true' : 'false';
return <<<EOF
$importCode
export const eagerControllers = $eagerControllersJson;
export const lazyControllers = $lazyControllersExpression;
export const isApplicationDebug = $isDebugString;
EOF;
$importCode
export const eagerControllers = $eagerControllersJson;
export const lazyControllers = $lazyControllersExpression;
export const isApplicationDebug = $isDebugString;
EOF;
}
}

View File

@@ -15,18 +15,18 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @experimental
* @internal
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class RemoveAssetMapperServicesCompiler implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('asset_mapper')) {
$container->removeDefinition('stimulus.ux_controllers_twig_runtime');
$container->removeDefinition('stimulus.asset_mapper.controllers_map_generator');
$container->removeDefinition('stimulus.asset_mapper.stimulus_loader_javascript_compiler');
$container->removeDefinition('stimulus.asset_mapper.loader_javascript_compiler');
}
}
}

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -20,9 +18,9 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -45,7 +43,7 @@ final class StimulusExtension extends Extension implements PrependExtensionInter
}
}
public function prepend(ContainerBuilder $container)
public function prepend(ContainerBuilder $container): void
{
if (!$this->isAssetMapperAvailable($container)) {
return;
@@ -58,7 +56,7 @@ final class StimulusExtension extends Extension implements PrependExtensionInter
],
'excluded_patterns' => [
'*.d.ts',
'**/controllers.json',
'*/controllers.json',
],
],
]);

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -14,6 +12,8 @@ declare(strict_types=1);
namespace Symfony\UX\StimulusBundle\Dto;
use Twig\Environment;
use Twig\Extension\EscaperExtension;
use Twig\Runtime\EscaperRuntime;
/**
* Helper to build Stimulus-related HTML attributes.
@@ -69,13 +69,13 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
/**
* @param array $parameters Parameters to pass to the action. Optional.
*/
public function addAction(string $controllerName, string $actionName, string $eventName = null, array $parameters = []): void
public function addAction(string $controllerName, string $actionName, ?string $eventName = null, array $parameters = []): void
{
$controllerName = $this->normalizeControllerName($controllerName);
$this->actions[] = [
'controllerName' => $controllerName,
'actionName' => $actionName,
'eventName' => $eventName,
'eventName' => null !== $eventName ? $this->normalizeEventName($eventName) : null,
];
foreach ($parameters as $name => $value) {
@@ -89,7 +89,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
* @param string $controllerName the Stimulus controller name
* @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional.
*/
public function addTarget(string $controllerName, string $targetNames = null): void
public function addTarget(string $controllerName, ?string $targetNames = null): void
{
if (null === $targetNames) {
return;
@@ -107,59 +107,41 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
public function __toString(): string
{
$controllers = array_map(function (string $controllerName): string {
return $this->escapeAsHtmlAttr($controllerName);
}, $this->controllers);
// done separately so we can escape, but avoid escaping ->
$actions = array_map(function (array $actionData): string {
$controllerName = $this->escapeAsHtmlAttr($actionData['controllerName']);
$actionName = $this->escapeAsHtmlAttr($actionData['actionName']);
$eventName = $actionData['eventName'];
$action = $controllerName.'#'.$actionName;
if (null !== $eventName) {
$action = $this->escapeAsHtmlAttr($eventName).'->'.$action;
}
return $action;
}, $this->actions);
$targets = [];
foreach ($this->targets as $key => $targetNamesString) {
$targetNames = explode(' ', $targetNamesString);
$targets[$key] = implode(' ', array_map(function (string $targetName): string {
return $this->escapeAsHtmlAttr($targetName);
}, $targetNames));
}
$attributes = [];
if ($controllers) {
$attributes[] = sprintf('data-controller="%s"', implode(' ', $controllers));
if ($this->controllers) {
$attributes[] = 'data-controller="'.$this->escape(implode(' ', $this->controllers)).'"';
}
if ($actions) {
$attributes[] = sprintf('data-action="%s"', implode(' ', $actions));
if ($this->actions) {
$actions = [];
foreach ($this->actions as ['controllerName' => $controllerName, 'actionName' => $actionName, 'eventName' => $eventName]) {
$action = $this->escape($controllerName.'#'.$actionName);
if (null !== $eventName) {
// done separately so we can escape, but avoid escaping ->
$action = $this->escape($eventName).'->'.$action;
}
$actions[] = $action;
}
$attributes[] = 'data-action="'.implode(' ', $actions).'"';
}
if ($targets) {
$attributes[] = implode(' ', array_map(function (string $key, string $value): string {
return sprintf('%s="%s"', $key, $value);
}, array_keys($targets), $targets));
foreach ($this->targets as $k => $v) {
$attributes[] = $this->escape($k, 'html_attr').'="'.$this->escape($v).'"';
}
return rtrim(implode(' ', [
...$attributes,
...array_map(function (string $attribute, string $value): string {
return $attribute.'="'.$this->escapeAsHtmlAttr($value).'"';
}, array_keys($this->attributes), $this->attributes),
]));
foreach ($this->attributes as $k => $v) {
$attributes[] = $this->escape($k, 'html_attr').'="'.$this->escape($v).'"';
}
return implode(' ', $attributes);
}
public function toArray(): array
{
$actions = array_map(function (array $actionData): string {
$actions = array_map(static function (array $actionData): string {
$controllerName = $actionData['controllerName'];
$actionName = $actionData['actionName'];
$eventName = $actionData['eventName'];
@@ -193,7 +175,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
{
$escaped = [];
foreach ($this->toArray() as $key => $value) {
$escaped[$key] = $this->escapeAsHtmlAttr($value);
$escaped[$key] = $this->escape($value);
}
return $escaped;
@@ -212,9 +194,18 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
return (string) $value;
}
private function escapeAsHtmlAttr(mixed $value): string
private function escape(mixed $value, string $strategy = 'html'): string
{
return (string) twig_escape_filter($this->env, $value, 'html_attr');
if (class_exists(EscaperRuntime::class)) {
return $this->env->getRuntime(EscaperRuntime::class)->escape($value, $strategy);
}
if (method_exists(EscaperExtension::class, 'escape')) {
return EscaperExtension::escape($this->env, $value, $strategy);
}
// since twig/twig 3.9.0: Using the internal "twig_escape_filter" function is deprecated.
return (string) twig_escape_filter($this->env, $value, $strategy);
}
/**
@@ -227,6 +218,14 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
return preg_replace('/^@/', '', str_replace('_', '-', str_replace('/', '--', $controllerName)));
}
/**
* @see https://stimulus.hotwired.dev/reference/actions
*/
private function normalizeEventName(string $eventName): string
{
return preg_replace_callback('/^.+(?=:)/', fn (array $matches): string => $this->normalizeControllerName($matches[0]), $eventName);
}
/**
* Normalize a Stimulus Value API key into its HTML equivalent ("kebab case").
* Backport features from symfony/string.

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*

View File

@@ -25,7 +25,7 @@ final class StimulusBundle extends Bundle
return \dirname(__DIR__);
}
public function build(ContainerBuilder $container)
public function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new RemoveAssetMapperServicesCompiler());
}

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -70,7 +68,7 @@ final class StimulusTwigExtension extends AbstractExtension
/**
* @param array $parameters Parameters to pass to the action. Optional.
*/
public function renderStimulusAction(string $controllerName, string $actionName = null, string $eventName = null, array $parameters = []): StimulusAttributes
public function renderStimulusAction(string $controllerName, ?string $actionName = null, ?string $eventName = null, array $parameters = []): StimulusAttributes
{
$stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
$stimulusAttributes->addAction($controllerName, $actionName, $eventName, $parameters);
@@ -81,7 +79,7 @@ final class StimulusTwigExtension extends AbstractExtension
/**
* @param array $parameters Parameters to pass to the action. Optional.
*/
public function appendStimulusAction(StimulusAttributes $stimulusAttributes, string $controllerName, string $actionName, string $eventName = null, array $parameters = []): StimulusAttributes
public function appendStimulusAction(StimulusAttributes $stimulusAttributes, string $controllerName, string $actionName, ?string $eventName = null, array $parameters = []): StimulusAttributes
{
$stimulusAttributes->addAction($controllerName, $actionName, $eventName, $parameters);
@@ -92,7 +90,7 @@ final class StimulusTwigExtension extends AbstractExtension
* @param string $controllerName the Stimulus controller name
* @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional.
*/
public function renderStimulusTarget(string $controllerName, string $targetNames = null): StimulusAttributes
public function renderStimulusTarget(string $controllerName, ?string $targetNames = null): StimulusAttributes
{
$stimulusAttributes = $this->stimulusHelper->createStimulusAttributes();
$stimulusAttributes->addTarget($controllerName, $targetNames);
@@ -104,7 +102,7 @@ final class StimulusTwigExtension extends AbstractExtension
* @param string $controllerName the Stimulus controller name
* @param string|null $targetNames The space-separated list of target names if a string is passed to the 1st argument. Optional.
*/
public function appendStimulusTarget(StimulusAttributes $stimulusAttributes, string $controllerName, string $targetNames = null): StimulusAttributes
public function appendStimulusTarget(StimulusAttributes $stimulusAttributes, string $controllerName, ?string $targetNames = null): StimulusAttributes
{
$stimulusAttributes->addTarget($controllerName, $targetNames);

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -17,7 +15,7 @@ use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* @experimental
* @internal
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/

View File

@@ -20,7 +20,7 @@ use Twig\Extension\RuntimeExtensionInterface;
/**
* Returns the link tags for all autoimported CSS files in controllers.json.
*
* @experimental
* @internal
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
@@ -64,7 +64,7 @@ final class UxControllersTwigRuntime implements RuntimeExtensionInterface
foreach ($controllerData['autoimport'] ?? [] as $autoImport => $enabled) {
if ($enabled) {
$links[] = sprintf('<link rel="stylesheet" href="%s">', $this->getLinkHref($autoImport, $uxPackageName));
$links[] = \sprintf('<link rel="stylesheet" href="%s">', $this->getLinkHref($autoImport, $uxPackageName));
}
}
}
@@ -84,7 +84,7 @@ final class UxControllersTwigRuntime implements RuntimeExtensionInterface
$slashPosition = strpos($autoImport, '/');
if (false === $slashPosition) {
throw new \LogicException(sprintf('The autoimport "%s" is not valid.', $autoImport));
throw new \LogicException(\sprintf('The autoimport "%s" is not valid.', $autoImport));
}
// if the first character is @, then the package name is @symfony/ux-cropperjs
@@ -102,12 +102,12 @@ final class UxControllersTwigRuntime implements RuntimeExtensionInterface
$uxPackageMetadata = $this->uxPackageReader->readPackageMetadata($uxPackageName);
$filePath = $uxPackageMetadata->packageDirectory.'/'.$file;
if (!is_file($filePath)) {
throw new \LogicException(sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and the file "%s" does not exist in the package path "%s". And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $file, $uxPackageMetadata->packageDirectory));
throw new \LogicException(\sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and the file "%s" does not exist in the package path "%s". And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $file, $uxPackageMetadata->packageDirectory));
}
$asset = $this->assetMapper->getAssetFromSourcePath($filePath);
if (!$asset) {
throw new \LogicException(sprintf('An "autoimport" in "%s" refers to "%s". This file was found, but the path is not in the asset mapper. And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport));
throw new \LogicException(\sprintf('An "autoimport" in "%s" refers to "%s". This file was found, but the path is not in the asset mapper. And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport));
}
return $asset->publicPath;
@@ -115,12 +115,12 @@ final class UxControllersTwigRuntime implements RuntimeExtensionInterface
$importMap = $this->readImportMap();
if (!isset($importMap[$package])) {
throw new \LogicException(sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and no "%s" entry was found in importmap.php. And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $package));
throw new \LogicException(\sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and no "%s" entry was found in importmap.php. And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $package));
}
$importMapEntry = $importMap[$package];
if (!isset($importMapEntry['url'])) {
throw new \LogicException(sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and no "url" key was found in importmap.php for the package "%s". And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $package));
throw new \LogicException(\sprintf('An "autoimport" in "%s" refers to "%s". This path could not be found in the asset mapper and no "url" key was found in importmap.php for the package "%s". And so, the file cannot be loaded.', $this->shortControllersPath(), $autoImport, $package));
}
$version = $this->parseVersionFromUrl($importMapEntry['url']);
@@ -151,10 +151,10 @@ final class UxControllersTwigRuntime implements RuntimeExtensionInterface
private function getJsDelivrUrl(string $package, ?string $version, string $file): string
{
$version = $version ?? 'latest';
$version ??= 'latest';
$package = str_replace('@', '', $package);
return sprintf('https://cdn.jsdelivr.net/npm/%s@%s/%s', $package, $version, $file);
return \sprintf('https://cdn.jsdelivr.net/npm/%s@%s/%s', $package, $version, $file);
}
private function shortControllersPath(): string

View File

@@ -14,8 +14,6 @@ namespace Symfony\UX\StimulusBundle\Ux;
/**
* @internal
*
* @experimental
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class UxPackageMetadata

View File

@@ -11,11 +11,11 @@
namespace Symfony\UX\StimulusBundle\Ux;
use Composer\InstalledVersions;
/**
* @internal
*
* @experimental
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class UxPackageReader
@@ -28,16 +28,21 @@ class UxPackageReader
{
// remove the '@' from the name to get back to the PHP package name
$phpPackageName = substr($packageName, 1);
$phpPackagePath = $this->projectDir.'/vendor/'.$phpPackageName;
if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($phpPackageName)) {
$phpPackagePath = InstalledVersions::getInstallPath($phpPackageName);
} else {
$phpPackagePath = $this->projectDir.'/vendor/'.$phpPackageName;
}
if (!is_dir($phpPackagePath)) {
throw new \RuntimeException(sprintf('Could not find package "%s" referred to from controllers.json.', $phpPackageName));
throw new \RuntimeException(\sprintf('Could not find package "%s" referred to from controllers.json.', $phpPackageName));
}
$packageConfigJsonPath = $phpPackagePath.'/assets/package.json';
if (!file_exists($packageConfigJsonPath)) {
$packageConfigJsonPath = $phpPackagePath.'/Resources/assets/package.json';
}
if (!file_exists($packageConfigJsonPath)) {
throw new \RuntimeException(sprintf('Could not find package.json in the "%s" package.', $phpPackagePath));
throw new \RuntimeException(\sprintf('Could not find package.json in the "%s" package.', $phpPackagePath));
}
$packageConfigJson = file_get_contents($packageConfigJsonPath);

View File

@@ -20,20 +20,22 @@ use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator;
use Symfony\UX\StimulusBundle\AssetMapper\MappedControllerAutoImport;
use Symfony\UX\StimulusBundle\Ux\UxPackageReader;
class ControllerMapGeneratorTest extends TestCase
class ControllersMapGeneratorTest extends TestCase
{
public function testGetControllersMap()
{
$mapper = $this->createMock(AssetMapperInterface::class);
$mapper->expects($this->any())
->method('getAssetFromSourcePath')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
if (str_ends_with($path, 'package-controller-first.js')) {
$logicalPath = 'fake-vendor/ux-package1/package-controller-first.js';
} elseif (str_ends_with($path, 'package-controller-second.js')) {
$logicalPath = 'fake-vendor/ux-package1/package-controller-second.js';
} elseif (str_ends_with($path, 'package-hello-controller.js')) {
$logicalPath = 'fake-vendor/ux-package2/package-hello-controller.js';
} elseif (str_ends_with($path, 'other-controller.ts') || str_ends_with($path, 'excluded-controller.js')) {
return null;
} else {
// replace windows slashes
$path = str_replace('\\', '/', $path);
@@ -41,7 +43,12 @@ class ControllerMapGeneratorTest extends TestCase
$logicalPath = substr($path, $assetsPosition + 1);
}
return new MappedAsset($logicalPath, $path, content: file_get_contents($path));
$content = null;
if (str_ends_with($path, 'minified-controller.js')) {
$content = 'import{Controller}from"@hotwired/stimulus";export default class extends Controller{}';
}
return new MappedAsset($logicalPath, $path, content: $content);
});
$packageReader = new UxPackageReader(__DIR__.'/../fixtures');
@@ -50,7 +57,7 @@ class ControllerMapGeneratorTest extends TestCase
if (class_exists(ImportMapConfigReader::class)) {
$autoImportLocator->expects($this->any())
->method('locateAutoImport')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
return new MappedControllerAutoImport('/path/to'.$path, false);
});
} else {
@@ -70,24 +77,29 @@ class ControllerMapGeneratorTest extends TestCase
$autoImportLocator,
);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Could not find an asset mapper path that points to the "excluded" controller.');
$map = $generator->getControllersMap();
// + 3 controller.json UX controllers
// - 1 controllers.json UX controller is disabled
// + 8 custom controllers (1 file is not a controller & 1 is overridden)
$this->assertCount(10, $map);
// + 11 custom controllers (1 file is not a controller, 1 is overridden)
$this->assertCount(13, $map);
$packageNames = array_keys($map);
sort($packageNames);
$this->assertSame([
'bye',
'excluded',
'fake-vendor--ux-package1--controller-second',
'fake-vendor--ux-package2--hello-controller',
'hello',
'hello-with-dashes',
'hello-with-underscores',
'minified',
'other',
'subdir--deeper',
'subdir--deeper-with-dashes',
'subdir--deeper-with-underscores',
'typescript',
], $packageNames);
$controllerSecond = $map['fake-vendor--ux-package1--controller-second'];
@@ -114,5 +126,8 @@ class ControllerMapGeneratorTest extends TestCase
$otherController = $map['other'];
$this->assertTrue($otherController->isLazy);
$minifiedController = $map['minified'];
$this->assertTrue($minifiedController->isLazy);
}
}

View File

@@ -42,28 +42,31 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
if (class_exists(ImportMapConfigReader::class)) {
// filter out items ending in .css
$importMapJsKeys = array_filter($importMapKeys, function ($key) {
$importMapJsKeys = array_filter($importMapKeys, static function ($key) {
return '.css' !== substr($key, -4);
});
$importMapCssKeys = array_filter($importMapKeys, function ($key) {
$importMapCssKeys = array_filter($importMapKeys, static function ($key) {
return '.css' === substr($key, -4);
});
sort($importMapJsKeys);
$this->assertSame([
// 1x import from loader.js (which is aliased to @symfony/stimulus-bundle via importmap)
'/assets/@symfony/stimulus-bundle/controllers.js',
// 6x from "controllers" (hello is overridden)
// 7x from "controllers" (hello is overridden)
'/assets/controllers/bye_controller.js',
'/assets/controllers/hello-with-dashes-controller.js',
'/assets/controllers/hello_with_underscores-controller.js',
'/assets/controllers/subdir/deeper-controller.js',
'/assets/controllers/subdir/deeper-with-dashes-controller.js',
'/assets/controllers/subdir/deeper_with_underscores-controller.js',
'/assets/controllers/typescript-controller.ts',
// 2x from UX packages, which are enabled in controllers.json
'/assets/fake-vendor/ux-package1/package-controller-second.js',
'/assets/fake-vendor/ux-package2/package-hello-controller.js',
// 2x from more-controllers
// 4x from more-controllers
'/assets/more-controllers/excluded-controller.js',
'/assets/more-controllers/hello-controller.js',
'/assets/more-controllers/minified-controller.js',
'/assets/more-controllers/other-controller.js',
// 5x from importmap.php
'@hotwired/stimulus',
@@ -89,10 +92,10 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
], array_values($importMapCssKeys));
// "app" is the entry. So, all non-lazy controllers should be preloaded:
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) {
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(static function ($link) {
return $link->attr('href');
});
$this->assertCount(11, $preLoadHrefs);
$this->assertCount(12, $preLoadHrefs);
sort($preLoadHrefs);
$this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/controllers-', $preLoadHrefs[0]);
$this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/loader-', $preLoadHrefs[1]);
@@ -101,9 +104,10 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
$this->assertStringStartsWith('/assets/controllers/subdir/deeper-controller-', $preLoadHrefs[5]);
$this->assertStringStartsWith('/assets/controllers/subdir/deeper-with-dashes-controller-', $preLoadHrefs[6]);
$this->assertStringStartsWith('/assets/controllers/subdir/deeper_with_underscores-controller-', $preLoadHrefs[7]);
$this->assertStringStartsWith('/assets/fake-vendor/ux-package2/package-hello-controller-', $preLoadHrefs[8]);
$this->assertStringStartsWith('/assets/more-controllers/hello-controller-', $preLoadHrefs[9]);
$this->assertStringStartsWith('/assets/vendor/@hotwired/stimulus/stimulus.index', $preLoadHrefs[10]);
$this->assertStringStartsWith('/assets/controllers/typescript-controller-', $preLoadHrefs[8]);
$this->assertStringStartsWith('/assets/fake-vendor/ux-package2/package-hello-controller-', $preLoadHrefs[9]);
$this->assertStringStartsWith('/assets/more-controllers/hello-controller-', $preLoadHrefs[10]);
$this->assertStringStartsWith('/assets/vendor/@hotwired/stimulus/stimulus.index', $preLoadHrefs[11]);
} else {
// legacy
$this->assertSame([
@@ -131,7 +135,7 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
], $importMapKeys);
// "app" & loader.js are pre-loaded. So, all non-lazy controllers should be preloaded:
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) {
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(static function ($link) {
return $link->attr('href');
});
$this->assertCount(10, $preLoadHrefs);

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -27,21 +25,21 @@ final class StimulusAttributesTest extends TestCase
$this->stimulusAttributes = new StimulusAttributes(new Environment(new ArrayLoader()));
}
public function testAddAction(): void
public function testAddAction()
{
$this->stimulusAttributes->addAction('foo', 'bar', 'baz', ['qux' => '"']);
$attributesHtml = (string) $this->stimulusAttributes;
self::assertSame('data-action="baz->foo#bar" data-foo-qux-param="&quot;"', $attributesHtml);
}
public function testAddActionToArrayNoEscapingAttributeValues(): void
public function testAddActionToArrayNoEscapingAttributeValues()
{
$this->stimulusAttributes->addAction('foo', 'bar', 'baz', ['qux' => '"']);
$attributesArray = $this->stimulusAttributes->toArray();
self::assertSame(['data-action' => 'baz->foo#bar', 'data-foo-qux-param' => '"'], $attributesArray);
}
public function testAddActionWithMultiple(): void
public function testAddActionWithMultiple()
{
$this->stimulusAttributes->addAction('my-controller', 'onClick');
$this->assertSame('data-action="my-controller#onClick"', (string) $this->stimulusAttributes);
@@ -54,7 +52,7 @@ final class StimulusAttributesTest extends TestCase
);
}
public function testAddControllerToStringEscapingAttributeValues(): void
public function testAddControllerToStringEscapingAttributeValues()
{
$this->stimulusAttributes->addController('foo', ['bar' => '"'], ['baz' => '"']);
$attributesHtml = (string) $this->stimulusAttributes;
@@ -66,7 +64,7 @@ final class StimulusAttributesTest extends TestCase
);
}
public function testAddControllerToArrayNoEscapingAttributeValues(): void
public function testAddControllerToArrayNoEscapingAttributeValues()
{
$this->stimulusAttributes->addController('foo', ['bar' => '"'], ['baz' => '"']);
$attributesArray = $this->stimulusAttributes->toArray();
@@ -103,21 +101,21 @@ final class StimulusAttributesTest extends TestCase
);
}
public function testAddTargetToStringEscapingAttributeValues(): void
public function testAddTargetToStringEscapingAttributeValues()
{
$this->stimulusAttributes->addTarget('foo', '"');
$attributesHtml = (string) $this->stimulusAttributes;
self::assertSame('data-foo-target="&quot;"', $attributesHtml);
}
public function testAddTargetToArrayNoEscapingAttributeValues(): void
public function testAddTargetToArrayNoEscapingAttributeValues()
{
$this->stimulusAttributes->addTarget('foo', '"');
$attributesArray = $this->stimulusAttributes->toArray();
self::assertSame(['data-foo-target' => '"'], $attributesArray);
}
public function testAddTargetWithMultiple(): void
public function testAddTargetWithMultiple()
{
$this->stimulusAttributes->addTarget('my-controller', 'myTarget');
$this->assertSame('data-my-controller-target="myTarget"', (string) $this->stimulusAttributes);
@@ -147,7 +145,94 @@ final class StimulusAttributesTest extends TestCase
public function testAddAttribute()
{
$this->stimulusAttributes->addAttribute('foo', 'bar baz');
$this->assertSame('foo="bar&#x20;baz"', (string) $this->stimulusAttributes);
$this->assertSame('foo="bar baz"', (string) $this->stimulusAttributes);
$this->assertSame(['foo' => 'bar baz'], $this->stimulusAttributes->toArray());
}
/**
* @dataProvider provideAddComplexActionData
*/
public function testAddComplexAction(string $controllerName, string $actionName, ?string $eventName, string $expectedAction)
{
$this->stimulusAttributes->addAction($controllerName, $actionName, $eventName);
$attributesHtml = (string) $this->stimulusAttributes;
self::assertSame(\sprintf('data-action="%s"', $expectedAction), $attributesHtml);
}
/**
* @return iterable<array{
* controllerName: string,
* actionName: string,
* eventName: ?string,
* expectedAction: string,
* }>
*/
public static function provideAddComplexActionData(): iterable
{
// basic datasets
yield 'foo#bar' => [
'controllerName' => 'foo',
'actionName' => 'bar',
'eventName' => null,
'expectedAction' => 'foo#bar',
];
yield 'baz->foo#bar' => [
'controllerName' => 'foo',
'actionName' => 'bar',
'eventName' => 'baz',
'expectedAction' => 'baz->foo#bar',
];
// datasets from https://github.com/hotwired/stimulus
yield 'keydown.esc@document->a#log' => [
'controllerName' => 'a',
'actionName' => 'log',
'eventName' => 'keydown.esc@document',
'expectedAction' => 'keydown.esc@document->a#log',
];
yield 'keydown.enter->a#log' => [
'controllerName' => 'a',
'actionName' => 'log',
'eventName' => 'keydown.enter',
'expectedAction' => 'keydown.enter->a#log',
];
yield 'keydown.shift+a->a#log' => [
'controllerName' => 'a',
'actionName' => 'log',
'eventName' => 'keydown.shift+a',
'expectedAction' => 'keydown.shift+a->a#log',
];
yield 'keydown@window->c#log' => [
'controllerName' => 'c',
'actionName' => 'log',
'eventName' => 'keydown@window',
'expectedAction' => 'keydown@window->c#log',
];
yield 'click->c#log:once' => [
'controllerName' => 'c',
'actionName' => 'log:once',
'eventName' => 'click',
'expectedAction' => 'click->c#log:once',
];
// extended datasets
yield 'vue:mount->foo#bar:passive' => [
'controllerName' => 'foo',
'actionName' => 'bar:passive',
'eventName' => 'vue:mount',
'expectedAction' => 'vue:mount->foo#bar:passive',
];
yield 'foo--controller-1:baz->bar--controller-2#log' => [
'controllerName' => '@bar/controller_2',
'actionName' => 'log',
'eventName' => '@foo/controller_1:baz',
'expectedAction' => 'foo--controller-1:baz->bar--controller-2#log',
];
yield 'foo--controller-1:baz@document->bar--controller-2#log:capture' => [
'controllerName' => '@bar/controller_2',
'actionName' => 'log:capture',
'eventName' => '@foo/controller_1:baz@document',
'expectedAction' => 'foo--controller-1:baz@document->bar--controller-2#log:capture',
];
}
}

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -20,7 +18,7 @@ use Twig\Environment;
final class StimulusHelperTest extends TestCase
{
public function testCreateStimulusAttributes(): void
public function testCreateStimulusAttributes()
{
$helper = new StimulusHelper($this->createMock(Environment::class));
$attributes = $helper->createStimulusAttributes();

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Symfony package.
*
@@ -34,7 +32,7 @@ final class StimulusTwigExtensionTest extends TestCase
/**
* @dataProvider provideRenderStimulusController
*/
public function testRenderStimulusController(string $controllerName, array $controllerValues, array $controllerClasses, array $controllerOutlets, string $expectedString, array $expectedArray): void
public function testRenderStimulusController(string $controllerName, array $controllerValues, array $controllerClasses, array $controllerOutlets, string $expectedString, array $expectedArray)
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusController($controllerName, $controllerValues, $controllerClasses, $controllerOutlets);
@@ -132,20 +130,20 @@ final class StimulusTwigExtensionTest extends TestCase
];
}
public function testAppendStimulusController(): void
public function testAppendStimulusController()
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusController('my-controller', ['myValue' => 'scalar-value']);
$this->assertSame(
'data-controller="my-controller another-controller" data-my-controller-my-value-value="scalar-value" data-another-controller-another-value-value="scalar-value&#x20;2"',
(string) $extension->appendStimulusController($dto, 'another-controller', ['another-value' => 'scalar-value 2']),
'data-controller="my-controller another-controller" data-my-controller-my-value-value="scalar-value" data-another-controller-another-value-value="scalar-value 2" data-another-controller-json-value-value="{&quot;key&quot;:&quot;Value with quotes &#039; and \&quot;.&quot;}"',
(string) $extension->appendStimulusController($dto, 'another-controller', ['another-value' => 'scalar-value 2', 'jsonValue' => json_encode(['key' => 'Value with quotes \' and ".'])]),
);
}
/**
* @dataProvider provideRenderStimulusAction
*/
public function testRenderStimulusAction(string $controllerName, ?string $actionName, ?string $eventName, array $parameters, string $expectedString, array $expectedArray): void
public function testRenderStimulusAction(string $controllerName, ?string $actionName, ?string $eventName, array $parameters, string $expectedString, array $expectedArray)
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusAction($controllerName, $actionName, $eventName, $parameters);
@@ -205,13 +203,12 @@ final class StimulusTwigExtensionTest extends TestCase
'actionName' => 'onClick',
'eventName' => null,
'parameters' => ['boolParam' => true, 'intParam' => 4, 'stringParam' => 'test'],
'expectedString' => 'data-action="onClick"',
'expectedString' => 'data-action="my-controller#onClick" data-my-controller-bool-param-param="true" data-my-controller-int-param-param="4" data-my-controller-string-param-param="test"',
'expectedArray' => ['data-action' => 'my-controller#onClick', 'data-my-controller-bool-param-param' => 'true', 'data-my-controller-int-param-param' => '4', 'data-my-controller-string-param-param' => 'test'],
];
}
public function testAppendStimulusAction(): void
public function testAppendStimulusAction()
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusAction('my-controller', 'onClick', 'click');
@@ -249,7 +246,7 @@ final class StimulusTwigExtensionTest extends TestCase
];
}
public function testAppendStimulusTarget(): void
public function testAppendStimulusTarget()
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusTarget('my-controller', 'myTarget');

View File

@@ -39,7 +39,7 @@ class UxControllersTwigRuntimeTest extends TestCase
$assetMapper = $this->createMock(AssetMapperInterface::class);
$assetMapper->expects($this->any())
->method('getAsset')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
if (str_starts_with($path, 'in/asset/mapper')) {
return new MappedAsset(basename($path), publicPath: '/assets/mapper/'.basename($path));
}

View File

@@ -63,7 +63,9 @@ class StimulusTestKernel extends Kernel
'importmap_path' => '%kernel.project_dir%/'.(class_exists(ImportMapConfigReader::class) ? 'importmap.php' : 'legacy/importmap.php'),
],
'test' => true,
'handle_all_throwables' => true,
...(self::VERSION_ID >= 60200 ? [
'handle_all_throwables' => true,
] : []),
'php_errors' => ['log' => true],
]);

View File

@@ -0,0 +1,6 @@
// typescript-controller.js
// @ts-ignore
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
}

View File

@@ -0,0 +1,6 @@
// excluded-controller.js
import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
}

View File

@@ -0,0 +1,6 @@
// minified-controller.js
import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
}

View File

@@ -0,0 +1,7 @@
// other-controller.js
// @ts-ignore
import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
}