50 Commits
v2.25.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
Mathieu Santostefano
70175cbecb Replace chart examples by hello to avoid confusion with the ChartJS component 2025-05-05 09:46:58 +02:00
36 changed files with 284 additions and 245 deletions

1
.gitattributes vendored
View File

@@ -2,6 +2,7 @@
/.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

View File

@@ -1,5 +1,4 @@
Please do not submit any Pull Requests here. They will be closed.
---
## Please do not submit any Pull Requests here. They will be closed.
Please submit your PR here instead:
https://github.com/symfony/ux

View File

@@ -1,20 +1,20 @@
name: Close Pull Request
on:
pull_request_target:
types: [opened]
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!
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!

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/assets/node_modules/
/config/reference.php
/vendor/
/composer.lock
/phpunit.xml

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,42 +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
- Normalize Stimulus controller name in event name
## 2.14.2
- Fix bug with finding UX Packages with non-standard project structure
- Fix bug with finding UX Packages with non-standard project structure
## 2.14.1
- Fixed bug with Stimulus controllers in subdirectories on Windows
- Fixed bug with Stimulus controllers in subdirectories on Windows
## 2.14.0
- Added Typescript controllers support
- 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;
- 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]

View File

@@ -9,7 +9,7 @@ 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)
- [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 type { 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 { type EagerControllersCollection, type 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 };

141
assets/dist/loader.js vendored
View File

@@ -1,93 +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) {
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,
});
}
}
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

@@ -3,23 +3,22 @@
"description": "Integration of @hotwired/stimulus into Symfony",
"private": true,
"license": "MIT",
"version": "2.25.1",
"version": "2.34.0",
"keywords": [
"symfony-ux"
],
"homepage": "https://ux.symfony.com/stimulus",
"repository": "https://github.com/symfony/stimulus-bundle",
"repository": "https://github.com/symfony/ux",
"type": "module",
"files": [
"dist"
],
"main": "dist/loader.js",
"scripts": {
"build": "node ../../../bin/build_package.js .",
"watch": "node ../../../bin/build_package.js . --watch",
"test": "../../../bin/test_package.sh .",
"check": "biome check",
"ci": "biome ci"
"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,
@@ -31,5 +30,14 @@
"peerDependencies": {
"@hotwired/stimulus": "^3.0.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

@@ -15,9 +15,9 @@
import { Application, type ControllerConstructor } from '@hotwired/stimulus';
import {
type EagerControllersCollection,
type LazyControllersCollection,
eagerControllers,
isApplicationDebug,
type LazyControllersCollection,
lazyControllers,
} from './controllers.js';
@@ -66,10 +66,12 @@ class StimulusLazyControllerHandler {
private lazyLoadExistingControllers(element: Element) {
Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.flatMap(extractControllerNamesFrom)
.forEach((controllerName) => this.loadLazyController(controllerName));
.forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
private loadLazyController(name: string) {
private loadLazyController(name: string): void {
if (!this.lazyControllers[name]) {
return;
}
@@ -106,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;
@@ -144,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,9 +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 type { EagerControllersCollection, LazyControllersCollection } from '../src/controllers';
import { loadControllers } from '../../dist/loader';
import type { EagerControllersCollection, LazyControllersCollection } from '../../src/controllers';
let isController1Initialized = false;
let isController2Initialized = false;

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",
"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

@@ -174,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>
@@ -191,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>
@@ -214,8 +214,8 @@ And with outlets:
.. 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] },
{ 'loading': 'spinner' },
{ 'other': '.target' } ) }}>
Hello
@@ -223,17 +223,17 @@ And with outlets:
<!-- 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>
@@ -246,12 +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="chart other-controller" data-chart-name-value="Likes">
<div data-controller="hello other-controller" data-hello-name-value="World">
Hello
</div>
@@ -259,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
~~~~~~~~~~~~~~~
@@ -418,7 +418,7 @@ is running in debug mode.
.. tip::
For AssetMapper 6.3 only, you also need a ``{{ ux_controller_link_tags() }``
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

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

@@ -70,6 +70,11 @@ class ControllersMapGenerator
$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();
// use regex to extract 'controller'-postfix including extension
preg_match(self::FILENAME_REGEX, $name, $matches);
@@ -77,6 +82,10 @@ class ControllersMapGenerator
$name = str_replace(['_', '/', '\\'], ['-', '--', '--'], $name);
$asset = $this->assetMapper->getAssetFromSourcePath($file->getRealPath());
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);

View File

@@ -95,7 +95,7 @@ class StimulusLoaderJavaScriptCompiler implements AssetCompilerInterface
} 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;
@@ -124,10 +124,10 @@ class StimulusLoaderJavaScriptCompiler implements AssetCompilerInterface
$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

@@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
*/
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');

View File

@@ -18,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>
@@ -43,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;
@@ -56,7 +56,7 @@ final class StimulusExtension extends Extension implements PrependExtensionInter
],
'excluded_patterns' => [
'*.d.ts',
'**/controllers.json',
'*/controllers.json',
],
],
]);

View File

@@ -141,7 +141,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
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'];

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

@@ -151,7 +151,7 @@ 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);

View File

@@ -27,13 +27,15 @@ class ControllersMapGeneratorTest extends TestCase
$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);
@@ -55,7 +57,7 @@ class ControllersMapGeneratorTest 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 {
@@ -75,15 +77,18 @@ class ControllersMapGeneratorTest 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
// + 10 custom controllers (1 file is not a controller & 1 is overridden)
$this->assertCount(12, $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',

View File

@@ -42,10 +42,10 @@ 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);
@@ -63,7 +63,8 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
// 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',
// 3x 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',
@@ -91,7 +92,7 @@ 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(12, $preLoadHrefs);
@@ -134,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

@@ -25,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);
@@ -52,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;
@@ -64,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();
@@ -101,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);
@@ -152,7 +152,7 @@ final class StimulusAttributesTest extends TestCase
/**
* @dataProvider provideAddComplexActionData
*/
public function testAddComplexAction(string $controllerName, string $actionName, ?string $eventName, string $expectedAction): void
public function testAddComplexAction(string $controllerName, string $actionName, ?string $eventName, string $expectedAction)
{
$this->stimulusAttributes->addAction($controllerName, $actionName, $eventName);
$attributesHtml = (string) $this->stimulusAttributes;

View File

@@ -18,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

@@ -32,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);
@@ -130,7 +130,7 @@ 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']);
@@ -143,7 +143,7 @@ final class StimulusTwigExtensionTest extends TestCase
/**
* @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);
@@ -208,7 +208,7 @@ final class StimulusTwigExtensionTest extends TestCase
];
}
public function testAppendStimulusAction(): void
public function testAppendStimulusAction()
{
$extension = new StimulusTwigExtension(new StimulusHelper($this->twig));
$dto = $extension->renderStimulusAction('my-controller', 'onClick', 'click');
@@ -246,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 @@
// excluded-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 {
}