mirror of
https://github.com/symfony/stimulus-bundle.git
synced 2026-03-24 01:12:07 +01:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05af0259f2 | ||
|
|
d610a2e021 | ||
|
|
66b32eaad0 | ||
|
|
581fe67a2a | ||
|
|
90640ce587 | ||
|
|
0401477482 | ||
|
|
7eca9dbf13 | ||
|
|
0c630746cd | ||
|
|
75f6193026 | ||
|
|
7d017977d5 | ||
|
|
f380e2a352 | ||
|
|
40e5dee2da | ||
|
|
591df71416 | ||
|
|
e1d125ac83 | ||
|
|
dfbf6b443b | ||
|
|
03dfa67783 | ||
|
|
c5ea8ee2cc | ||
|
|
328774d5af | ||
|
|
5a7b1f8f9d | ||
|
|
bab42de3cd | ||
|
|
668b9efe9d | ||
|
|
42f89354c7 | ||
|
|
44ac52b92e | ||
|
|
c5245dcbac | ||
|
|
57cf364182 | ||
|
|
9f33146f3d | ||
|
|
7788eaec42 | ||
|
|
b6cda5221b | ||
|
|
f7a29fbff6 | ||
|
|
b9fbd1fce0 | ||
|
|
fedf396824 | ||
|
|
6e15880f3b | ||
|
|
435a3c851b | ||
|
|
4ebef4b41e | ||
|
|
919d8734e9 | ||
|
|
960868a682 | ||
|
|
c20fee01ae | ||
|
|
a031cf8143 | ||
|
|
caef740d94 | ||
|
|
ef937d0a91 | ||
|
|
684aa71734 | ||
|
|
c65cf963cd | ||
|
|
defaeb91bd | ||
|
|
1d74f7034c | ||
|
|
42d84c70f9 | ||
|
|
82c174ebe5 | ||
|
|
750c770f66 | ||
|
|
d5abe891c0 | ||
|
|
a0d87ada42 | ||
|
|
5a6aef0646 | ||
|
|
9954aeb3c8 | ||
|
|
520ba148f3 | ||
|
|
70175cbecb | ||
|
|
9d0a21539e | ||
|
|
92d471a50e | ||
|
|
e098403044 | ||
|
|
cb7bc7ae54 | ||
|
|
014174ae8a | ||
|
|
84cb6d10af | ||
|
|
948f3bb04c | ||
|
|
1760a45593 | ||
|
|
ffb1ab446f | ||
|
|
80bfdc877c | ||
|
|
420c2a8c87 | ||
|
|
d05cf93372 | ||
|
|
f4509ed94a | ||
|
|
517d5a9b7d | ||
|
|
254f4e05cb | ||
|
|
4659f52ec9 | ||
|
|
14845f8923 | ||
|
|
e13034d428 | ||
|
|
ec2760a19b | ||
|
|
59ff77f1b2 | ||
|
|
2e840a3b12 | ||
|
|
af341c2258 | ||
|
|
d9b83e7132 | ||
|
|
692d83891c | ||
|
|
4658e3bdfa | ||
|
|
2dc5431fd4 | ||
|
|
60ec83e56c | ||
|
|
e5f7747b51 | ||
|
|
2f1d7a9de9 | ||
|
|
4ad4fad427 | ||
|
|
ae69e3a764 | ||
|
|
5e2e1aff3e | ||
|
|
d35d6cc05f | ||
|
|
014a5842e9 | ||
|
|
66b88c05d1 | ||
|
|
d4451c162c | ||
|
|
017b60e036 | ||
|
|
9323437da4 | ||
|
|
b3cd2c207f | ||
|
|
3e1117db76 | ||
|
|
e24a12ce43 | ||
|
|
b828a32fe9 | ||
|
|
da3cac8797 | ||
|
|
1050c8b9f5 | ||
|
|
24d9c5356a | ||
|
|
7f435aaf91 | ||
|
|
5432e91c00 | ||
|
|
6add4bdab1 | ||
|
|
602fd521d3 | ||
|
|
c113ab8e92 | ||
|
|
f36422ff5e | ||
|
|
f775f6e811 | ||
|
|
47fc162b69 | ||
|
|
683042ea5b | ||
|
|
3dc86506a4 | ||
|
|
da6b4a4f95 | ||
|
|
b3acd6dc4c | ||
|
|
a7c30d95ff | ||
|
|
70e325b71f | ||
|
|
28f463fbce | ||
|
|
fb37a06629 | ||
|
|
9701135728 | ||
|
|
3794315bbf | ||
|
|
cb498f202b | ||
|
|
3064da1419 | ||
|
|
4f68312151 | ||
|
|
369d48e2d3 | ||
|
|
b3562b90a3 | ||
|
|
40a4416e6d | ||
|
|
3a7d47bf33 | ||
|
|
2b974b5347 |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -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
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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!
|
||||
20
.github/workflows/close-pull-request.yml
vendored
Normal file
20
.github/workflows/close-pull-request.yml
vendored
Normal 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
13
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
branches: ["2.x"]
|
||||
maintained_branches: ["2.x"]
|
||||
doc_dir: "doc"
|
||||
branches: ['2.x']
|
||||
maintained_branches: ['2.x']
|
||||
doc_dir: 'doc'
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
19
assets/LICENSE
Normal 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
15
assets/README.md
Normal 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)
|
||||
21
assets/dist/controllers.d.ts
vendored
21
assets/dist/controllers.d.ts
vendored
@@ -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 };
|
||||
1
assets/dist/controllers.js
vendored
1
assets/dist/controllers.js
vendored
@@ -1,5 +1,4 @@
|
||||
const eagerControllers = {};
|
||||
const lazyControllers = {};
|
||||
const isApplicationDebug = false;
|
||||
|
||||
export { eagerControllers, isApplicationDebug, lazyControllers };
|
||||
|
||||
9
assets/dist/loader.d.ts
vendored
9
assets/dist/loader.d.ts
vendored
@@ -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
131
assets/dist/loader.js
vendored
@@ -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 };
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
3
assets/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.package.json"
|
||||
}
|
||||
4
assets/vitest.config.mjs
Normal file
4
assets/vitest.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
import configShared from '../../../vitest.config.base.mjs';
|
||||
|
||||
export default mergeConfig(configShared, {});
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
|
||||
258
doc/index.rst
258
doc/index.rst
@@ -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="[1,2,3,4]"
|
||||
data-controller="hello"
|
||||
data-hello-name-value="World"
|
||||
data-hello-data-value="[1,2,3,4]"
|
||||
>
|
||||
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="[1,2,3,4]"
|
||||
data-chart-loading-class="spinner"
|
||||
data-controller="hello"
|
||||
data-hello-name-value="World"
|
||||
data-hello-data-value="[1,2,3,4]"
|
||||
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="[1,2,3,4]"
|
||||
data-chart-loading-class="spinner"
|
||||
data-chart-other-outlet=".target"
|
||||
data-controller="hello"
|
||||
data-hello-name-value="World"
|
||||
data-hello-data-value="[1,2,3,4]"
|
||||
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
|
||||
|
||||
@@ -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&max[direct]=0"/>
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace Symfony\UX\StimulusBundle\AssetMapper;
|
||||
use Symfony\Component\AssetMapper\MappedAsset;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MappedControllerAsset
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace Symfony\UX\StimulusBundle\Ux;
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @experimental
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*/
|
||||
class UxPackageMetadata
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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="""', $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="""', $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 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
|
||||
@@ -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 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="{"key":"Value with quotes ' and \"."}"',
|
||||
(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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
4
tests/fixtures/StimulusTestKernel.php
vendored
4
tests/fixtures/StimulusTestKernel.php
vendored
@@ -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],
|
||||
]);
|
||||
|
||||
|
||||
6
tests/fixtures/assets/controllers/typescript-controller.ts
vendored
Normal file
6
tests/fixtures/assets/controllers/typescript-controller.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// typescript-controller.js
|
||||
// @ts-ignore
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
}
|
||||
6
tests/fixtures/assets/more-controllers/excluded-controller.js
vendored
Normal file
6
tests/fixtures/assets/more-controllers/excluded-controller.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// excluded-controller.js
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
}
|
||||
6
tests/fixtures/assets/more-controllers/minified-controller.js
vendored
Normal file
6
tests/fixtures/assets/more-controllers/minified-controller.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// minified-controller.js
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
}
|
||||
7
tests/fixtures/assets/more-controllers/other-controller.ts
vendored
Normal file
7
tests/fixtures/assets/more-controllers/other-controller.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// other-controller.js
|
||||
// @ts-ignore
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
}
|
||||
Reference in New Issue
Block a user