Skip to content

Commit 2c221c6

Browse files
weaverryanjmsche
andcommitted
[StimulusBundle] Initial import + AssetMapper integration
Co-authored-by: jmsche <contact@jmsche.fr>
1 parent a9cf40c commit 2c221c6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2311
-0
lines changed

.github/workflows/test.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,15 @@ jobs:
173173
run: php vendor/bin/simple-phpunit
174174
working-directory: src/Translator
175175

176+
- name: StimulusBundle Dependencies
177+
uses: ramsey/composer-install@v2
178+
with:
179+
working-directory: src/StimulusBundle
180+
dependency-versions: lowest
181+
- name: StimulusBundle Tests
182+
working-directory: src/StimulusBundle
183+
run: php vendor/bin/simple-phpunit
184+
176185
tests-php-high-deps:
177186
runs-on: ubuntu-latest
178187
steps:

src/StimulusBundle/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.php-cs-fixer.cache
2+
.phpunit.cache
3+
composer.lock
4+
vendor/
5+
tests/fixtures/var
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
branches: ["2.x"]
2+
maintained_branches: ["2.x"]
3+
doc_dir: "doc"

src/StimulusBundle/CONTRIBUTING.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Contributing
2+
============
3+
4+
When contributing, you can fix some things that will be detected by CI anyway *before* sending your pull request.
5+
6+
The following tools will be installed in the `tools` directory, so they don't share the bundle requirements.
7+
8+
PHPStan
9+
-------
10+
11+
```bash
12+
composer install --working-dir=tools/phpstan
13+
tools/phpstan/vendor/bin/phpstan analyze
14+
# Based on the results, you may want to update the baseline
15+
tools/phpstan/vendor/bin/phpstan analyze --generate-baseline
16+
```
17+
18+
PHP CS Fixer
19+
------------
20+
21+
```bash
22+
composer install --working-dir=tools/php-cs-fixer
23+
# Check what can be fixed
24+
PHP_CS_FIXER_IGNORE_ENV=1 tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --dry-run --diff
25+
# Fix them
26+
PHP_CS_FIXER_IGNORE_ENV=1 tools/php-cs-fixer/vendor/bin/php-cs-fixer fix --diff
27+
```
28+
29+
PHPUnit
30+
-------
31+
32+
```bash
33+
./vendor/bin/phpunit
34+
```

src/StimulusBundle/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# StimulusBundle: Symfony integration with Stimulus!
2+
3+
This bundle adds integration between Symfony, Stimulus and Symfony UX:
4+
5+
* A) Twig `stimulus_*` functions & filters to add Stimulus controllers, actions & targets in your templates.
6+
* B) Integration with Symfony UX & AssetMapper
7+
* C) A helper service to build the Stimulus data attributes and use them in your services.
8+
9+
[Read the documentation][1]
10+
11+
[1]: https://symfony.com/bundles/StimulusBundle/current/index.html
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This file is dynamically rewritten by StimulusBundle + AssetMapper.
2+
/** @type {Object<string, Controller>} */
3+
export const eagerControllers = {};
4+
/** @type {Object<string, string>} */
5+
6+
/** @type {Object<string, () => Promise<{default: Controller}>>} */
7+
export const lazyControllers = {};
8+
9+
export const isApplicationDebug = false;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Starts the Stimulus application and reads a map dump in the DOM to load controllers.
3+
*
4+
* Inspired by stimulus-loading.js from stimulus-rails.
5+
*/
6+
import { Application } from '@hotwired/stimulus';
7+
import { eagerControllers, lazyControllers, isApplicationDebug } from './controllers.js';
8+
9+
const controllerAttribute = 'data-controller';
10+
11+
export const loadControllers = (application) => {
12+
// loop over the controllers map and require each controller
13+
for (const name in eagerControllers) {
14+
registerController(name, eagerControllers[name], application);
15+
}
16+
17+
loadLazyControllers(application);
18+
};
19+
20+
export const startStimulusApp = () => {
21+
const application = Application.start();
22+
application.debug = isApplicationDebug;
23+
24+
loadControllers(application);
25+
26+
return application;
27+
};
28+
29+
function registerController(name, controller, application) {
30+
if (canRegisterController(name, application)) {
31+
application.register(name, controller)
32+
}
33+
}
34+
35+
function loadLazyControllers(application) {
36+
lazyLoadExistingControllers(application, document);
37+
lazyLoadNewControllers(application, document)
38+
}
39+
40+
function lazyLoadExistingControllers(application, element) {
41+
queryControllerNamesWithin(element).forEach(controllerName => loadController(controllerName, application))
42+
}
43+
function queryControllerNamesWithin(element) {
44+
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).map(extractControllerNamesFrom).flat()
45+
}
46+
function extractControllerNamesFrom(element) {
47+
return element.getAttribute(controllerAttribute).split(/\s+/).filter(content => content.length)
48+
}
49+
function lazyLoadNewControllers(application, element) {
50+
new MutationObserver((mutationsList) => {
51+
for (const { attributeName, target, type } of mutationsList) {
52+
switch (type) {
53+
case 'attributes': {
54+
if (attributeName === controllerAttribute && target.getAttribute(controllerAttribute)) {
55+
extractControllerNamesFrom(target).forEach(controllerName => loadController(controllerName, application))
56+
}
57+
}
58+
59+
case 'childList': {
60+
lazyLoadExistingControllers(application, target)
61+
}
62+
}
63+
}
64+
}).observe(element, { attributeFilter: [controllerAttribute], subtree: true, childList: true })
65+
}
66+
function canRegisterController(name, application){
67+
return !application.router.modulesByIdentifier.has(name)
68+
}
69+
70+
async function loadController(name, application) {
71+
if (canRegisterController(name, application)) {
72+
if (lazyControllers[name] === undefined) {
73+
console.error(`Failed to autoload controller: ${name}`);
74+
}
75+
76+
const controllerModule = await (lazyControllers[name]());
77+
78+
registerController(name, controllerModule.default, application);
79+
}
80+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@symfony/stimulus-bundle",
3+
"description": "Integration of @hotwired/stimulus into Symfony",
4+
"version": "1.0.0",
5+
"license": "MIT",
6+
"symfony": {
7+
"needsPackageAsADependency": false,
8+
"importmap": {
9+
"@hotwired/stimulus": "^3.0.0",
10+
"@symfony/stimulus-bundle": "path:dist/loader.js"
11+
}
12+
},
13+
"peerDependencies": {
14+
"@hotwired/stimulus": "^3.0.0",
15+
"@symfony/stimulus-bridge": "^3.2.0"
16+
}
17+
}

src/StimulusBundle/composer.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "symfony/stimulus-bundle",
3+
"description": "Integration with your Symfony app & Stimulus!",
4+
"keywords": ["symfony-ux"],
5+
"license": "MIT",
6+
"type": "symfony-bundle",
7+
"authors": [
8+
{
9+
"name": "Symfony Community",
10+
"homepage": "https://symfony.com/contributors"
11+
}
12+
],
13+
"require": {
14+
"php": ">=8.1",
15+
"symfony/config": "^5.4 || ^6.0",
16+
"symfony/dependency-injection": "^5.4 || ^6.0",
17+
"symfony/finder": "^5.4 || ^6.0",
18+
"symfony/http-kernel": "^5.4 || ^6.0",
19+
"twig/twig": "^2.15.3 || ^3.4.3"
20+
},
21+
"require-dev": {
22+
"symfony/asset-mapper": "^6.3",
23+
"symfony/framework-bundle": "^5.4 || ^6.0",
24+
"symfony/phpunit-bridge": "^5.4 || ^6.0",
25+
"symfony/twig-bundle": "^5.4 || ^6.0",
26+
"zenstruck/browser": "^1.4"
27+
},
28+
"minimum-stability": "dev",
29+
"autoload": {
30+
"psr-4": {
31+
"Symfony\\UX\\StimulusBundle\\": "src"
32+
}
33+
},
34+
"autoload-dev": {
35+
"psr-4": {
36+
"Symfony\\UX\\StimulusBundle\\Tests\\": "tests/"
37+
}
38+
}
39+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator;
7+
use Symfony\UX\StimulusBundle\AssetMapper\StimulusLoaderJavaScriptCompiler;
8+
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
9+
use Symfony\UX\StimulusBundle\Twig\StimulusTwigExtension;
10+
use Symfony\UX\StimulusBundle\Ux\UxPackageReader;
11+
use Twig\Environment;
12+
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
13+
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
14+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
15+
16+
return static function (ContainerConfigurator $container): void {
17+
$container->services()
18+
->set('stimulus.helper', StimulusHelper::class)
19+
->arg('$twig', service(Environment::class)->nullOnInvalid())
20+
21+
->set('stimulus.twig_extension', StimulusTwigExtension::class)
22+
->tag('twig.extension')
23+
24+
->set('stimulus.asset_mapper.controllers_map_generator', ControllersMapGenerator::class)
25+
->args([
26+
service('asset_mapper'),
27+
service('stimulus.asset_mapper.ux_package_reader'),
28+
abstract_arg('controller paths'),
29+
abstract_arg('controllers_json_path'),
30+
])
31+
32+
->set('stimulus.asset_mapper.ux_package_reader', UxPackageReader::class)
33+
->args([
34+
param('kernel.project_dir'),
35+
])
36+
37+
->set('stimulus.asset_mapper.loader_javascript_compiler', StimulusLoaderJavaScriptCompiler::class)
38+
->args([
39+
service('stimulus.asset_mapper.controllers_map_generator'),
40+
param('kernel.debug'),
41+
])
42+
->tag('asset_mapper.compiler', ['priority' => 100])
43+
;
44+
};

0 commit comments

Comments
 (0)