From a6245eb355dbd733ed0c8b3b53cfc4c6a396e069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 14:34:39 +0100 Subject: [PATCH 01/12] Replace StimulusBridge with StimulusBundle in docblock --- templates/stimulus/Controller.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/stimulus/Controller.tpl.php b/templates/stimulus/Controller.tpl.php index 555e38da9..f85e2384d 100644 --- a/templates/stimulus/Controller.tpl.php +++ b/templates/stimulus/Controller.tpl.php @@ -2,7 +2,7 @@ /* * The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ /* stimulusFetch: 'lazy' */ export default class extends Controller { From 14570ee82930d8cf3a357fc4c3f16bff8591a50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:17:45 +0100 Subject: [PATCH 02/12] Add methods initialize, connect and disconnect with code comment --- templates/stimulus/Controller.tpl.php | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/templates/stimulus/Controller.tpl.php b/templates/stimulus/Controller.tpl.php index f85e2384d..a3c73c477 100644 --- a/templates/stimulus/Controller.tpl.php +++ b/templates/stimulus/Controller.tpl.php @@ -14,5 +14,32 @@ } - // ... + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } } From 49d28a66e41860761fde18a22aac81e77191d7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:19:25 +0100 Subject: [PATCH 03/12] Add line break before "stimulusFetch: lazy" to emphasis line and help UX parsers --- templates/stimulus/Controller.tpl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/stimulus/Controller.tpl.php b/templates/stimulus/Controller.tpl.php index a3c73c477..10fa4dfd1 100644 --- a/templates/stimulus/Controller.tpl.php +++ b/templates/stimulus/Controller.tpl.php @@ -4,6 +4,7 @@ * The following line makes this controller "lazy": it won't be downloaded until needed * See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ + /* stimulusFetch: 'lazy' */ export default class extends Controller { From 9915c2f2419a2120677175e277fe52c055c6f6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:24:49 +0100 Subject: [PATCH 04/12] Update tests for lazy doc changes --- tests/fixtures/make-stimulus-controller/with_targets.js | 2 +- tests/fixtures/make-stimulus-controller/without_targets.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/make-stimulus-controller/with_targets.js b/tests/fixtures/make-stimulus-controller/with_targets.js index d78f58f25..bbd3cb141 100644 --- a/tests/fixtures/make-stimulus-controller/with_targets.js +++ b/tests/fixtures/make-stimulus-controller/with_targets.js @@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus'; /* * The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ /* stimulusFetch: 'lazy' */ export default class extends Controller { diff --git a/tests/fixtures/make-stimulus-controller/without_targets.js b/tests/fixtures/make-stimulus-controller/without_targets.js index c07f69be3..1eef75793 100644 --- a/tests/fixtures/make-stimulus-controller/without_targets.js +++ b/tests/fixtures/make-stimulus-controller/without_targets.js @@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus'; /* * The following line makes this controller "lazy": it won't be downloaded until needed -* See https://github.com/symfony/stimulus-bridge#lazy-controllers +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ /* stimulusFetch: 'lazy' */ export default class extends Controller { From f6adf79fa6b55654ec4d11ba88ac551850bf4a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:25:13 +0100 Subject: [PATCH 05/12] Update tests for empty line before lazy --- tests/fixtures/make-stimulus-controller/with_targets.js | 1 + tests/fixtures/make-stimulus-controller/without_targets.js | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/fixtures/make-stimulus-controller/with_targets.js b/tests/fixtures/make-stimulus-controller/with_targets.js index bbd3cb141..c541b9208 100644 --- a/tests/fixtures/make-stimulus-controller/with_targets.js +++ b/tests/fixtures/make-stimulus-controller/with_targets.js @@ -4,6 +4,7 @@ import { Controller } from '@hotwired/stimulus'; * The following line makes this controller "lazy": it won't be downloaded until needed * See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ + /* stimulusFetch: 'lazy' */ export default class extends Controller { static targets = ['results', 'messages', 'errors'] diff --git a/tests/fixtures/make-stimulus-controller/without_targets.js b/tests/fixtures/make-stimulus-controller/without_targets.js index 1eef75793..6bc62a5e2 100644 --- a/tests/fixtures/make-stimulus-controller/without_targets.js +++ b/tests/fixtures/make-stimulus-controller/without_targets.js @@ -4,6 +4,7 @@ import { Controller } from '@hotwired/stimulus'; * The following line makes this controller "lazy": it won't be downloaded until needed * See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers */ + /* stimulusFetch: 'lazy' */ export default class extends Controller { // ... From e09a45a38ba60ec9d34f25325f0486754cd7045b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:27:39 +0100 Subject: [PATCH 06/12] Update tests with base methods --- .../make-stimulus-controller/with_targets.js | 29 ++++++++++++++++++- .../without_targets.js | 29 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/make-stimulus-controller/with_targets.js b/tests/fixtures/make-stimulus-controller/with_targets.js index c541b9208..6c80bb339 100644 --- a/tests/fixtures/make-stimulus-controller/with_targets.js +++ b/tests/fixtures/make-stimulus-controller/with_targets.js @@ -8,5 +8,32 @@ import { Controller } from '@hotwired/stimulus'; /* stimulusFetch: 'lazy' */ export default class extends Controller { static targets = ['results', 'messages', 'errors'] - // ... + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } } diff --git a/tests/fixtures/make-stimulus-controller/without_targets.js b/tests/fixtures/make-stimulus-controller/without_targets.js index 6bc62a5e2..2a74ae7d7 100644 --- a/tests/fixtures/make-stimulus-controller/without_targets.js +++ b/tests/fixtures/make-stimulus-controller/without_targets.js @@ -7,5 +7,32 @@ import { Controller } from '@hotwired/stimulus'; /* stimulusFetch: 'lazy' */ export default class extends Controller { - // ... + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } } From ac7e35af2d2bd1d5afd9c37c5339509546b066de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 15:42:03 +0100 Subject: [PATCH 07/12] Use 'JavaScript' as default extension --- src/Maker/MakeStimulusController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index 0610c7d3b..8e626d8d4 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -59,7 +59,8 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma [ 'js' => 'JavaScript', 'ts' => 'TypeScript', - ] + ], + 'js', ); $input->setArgument('extension', $chosenExtension); From a61b18e9e86e5f2c31d3a34d359ba262fe12a07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 17:00:10 +0100 Subject: [PATCH 08/12] Add `--typescript` / `--ts` (non interactive) option (default false) --- config/help/MakeStimulusController.txt | 14 +++++++-- src/Maker/MakeStimulusController.php | 24 ++++++++++----- tests/Maker/MakeStimulusControllerTest.php | 35 ++++++++++++++++++++-- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/config/help/MakeStimulusController.txt b/config/help/MakeStimulusController.txt index 0785f9733..33e021376 100644 --- a/config/help/MakeStimulusController.txt +++ b/config/help/MakeStimulusController.txt @@ -1,5 +1,15 @@ -The %command.name% command generates new Stimulus Controller. +The %command.name% command generates a new Stimulus controller. php %command.full_name% hello -If the argument is missing, the command will ask for the controller name interactively. \ No newline at end of file +If the argument is missing, the command will ask for the controller name interactively. + +To generate a TypeScript file (instead of a JavaScript file) use the --typescript +(or --ts) option: + +php %command.full_name% hello --typescript + +It will also interactively ask for values, targets, classes to add to the Stimulus +controller (optional). + +php %command.full_name% diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index 8e626d8d4..5763a4b66 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -19,6 +19,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Question\Question; use Symfony\UX\StimulusBundle\StimulusBundle; use Symfony\WebpackEncoreBundle\WebpackEncoreBundle; @@ -44,8 +45,11 @@ public function configureCommand(Command $command, InputConfiguration $inputConf { $command ->addArgument('name', InputArgument::REQUIRED, 'The name of the Stimulus controller (e.g. hello)') + ->addOption('typescript', 'ts', InputOption::VALUE_NONE, 'Create a TypeScript controller (default is JavaScript)') ->setHelp($this->getHelpFileContents('MakeStimulusController.txt')) ; + + $inputConfig->setArgumentAsNonInteractive('typescript'); } public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void @@ -54,16 +58,20 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $command->addArgument('targets', InputArgument::OPTIONAL); $command->addArgument('values', InputArgument::OPTIONAL); - $chosenExtension = $io->choice( + if ($input->getOption('typescript')) { + $input->setArgument('extension', 'ts'); + } else { + $chosenExtension = $io->choice( 'Language (JavaScript or TypeScript)', - [ - 'js' => 'JavaScript', - 'ts' => 'TypeScript', - ], - 'js', - ); + [ + 'js' => 'JavaScript', + 'ts' => 'TypeScript', + ], + 'js', + ); - $input->setArgument('extension', $chosenExtension); + $input->setArgument('extension', $chosenExtension); + } if ($io->confirm('Do you want to include targets?')) { $targets = []; diff --git a/tests/Maker/MakeStimulusControllerTest.php b/tests/Maker/MakeStimulusControllerTest.php index 69183d695..7b455e597 100644 --- a/tests/Maker/MakeStimulusControllerTest.php +++ b/tests/Maker/MakeStimulusControllerTest.php @@ -24,6 +24,19 @@ protected function getMakerClass(): string public function getTestDetails(): \Generator { + yield 'it_generates_stimulus_controller' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'default', // controller name + ], + ); + + $generatedFilePath = $runner->getPath('assets/controllers/default_controller.js'); + $this->assertFileExists($generatedFilePath); + }), + ]; + yield 'it_generates_stimulus_controller_with_targets' => [$this->createMakerTest() ->run(function (MakerTestRunner $runner) { $runner->runMaker( @@ -74,16 +87,34 @@ public function getTestDetails(): \Generator }), ]; - yield 'it_generates_typescript_stimulus_controller' => [$this->createMakerTest() + yield 'it_generates_typescript_stimulus_controller_interactively' => [$this->createMakerTest() ->run(function (MakerTestRunner $runner) { $runner->runMaker( [ 'typescript', // controller name 'ts', // controller language 'no', // do not add targets - ]); + ], + ); + + $this->assertFileExists($runner->getPath('assets/controllers/typescript_controller.ts')); + $this->assertFileDoesNotExist($runner->getPath('assets/controllers/typescript_controller.js')); + }), + ]; + + yield 'it_generates_typescript_stimulus_controller_when_option_is_set' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'typescript', // controller name + // '', // language is not asked interactively + 'no', // do not add targets + ], + ' --typescript' + ); $this->assertFileExists($runner->getPath('assets/controllers/typescript_controller.ts')); + $this->assertFileDoesNotExist($runner->getPath('assets/controllers/typescript_controller.js')); }), ]; } From 835d42b12f29ed6e7235a040bc17ba3cf76c998d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 17:07:56 +0100 Subject: [PATCH 09/12] Update Maker documentation link displayed after code generation --- src/Maker/MakeStimulusController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index 5763a4b66..20c23b6ce 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -137,7 +137,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $io->text([ 'Next:', \sprintf('- Open %s and add the code you need', $filePath), - 'Find the documentation at https://github.com/symfony/stimulus-bridge', + 'Find the documentation at https://symfony.com/bundles/StimulusBundle', ]); } From 86a47eaec1c40eabc36955950e6b49ba96d31a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 19:10:43 +0100 Subject: [PATCH 10/12] Define classes interactively --- src/Maker/MakeStimulusController.php | 50 +++++++++- templates/stimulus/Controller.tpl.php | 1 + tests/Maker/MakeStimulusControllerTest.php | 97 ++++++++++++++++++- .../make-stimulus-controller/with_classes.js | 39 ++++++++ .../with_targets_values_classes.js | 44 +++++++++ .../make-stimulus-controller/with_values.js | 42 ++++++++ 6 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/make-stimulus-controller/with_classes.js create mode 100644 tests/fixtures/make-stimulus-controller/with_targets_values_classes.js create mode 100644 tests/fixtures/make-stimulus-controller/with_values.js diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index 20c23b6ce..1d5968c6f 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -57,12 +57,13 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $command->addArgument('extension', InputArgument::OPTIONAL); $command->addArgument('targets', InputArgument::OPTIONAL); $command->addArgument('values', InputArgument::OPTIONAL); + $command->addArgument('classes', InputArgument::OPTIONAL); if ($input->getOption('typescript')) { $input->setArgument('extension', 'ts'); } else { $chosenExtension = $io->choice( - 'Language (JavaScript or TypeScript)', + 'Language (JavaScript or TypeScript)', [ 'js' => 'JavaScript', 'ts' => 'TypeScript', @@ -107,16 +108,35 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma $input->setArgument('values', $values); } + + if ($io->confirm('Do you want to add classes?', false)) { + $classes = []; + $isFirstClass = true; + + while (true) { + $newClass = $this->askForNextClass($io, $classes, $isFirstClass); + if (null === $newClass) { + break; + } + + $isFirstClass = false; + $classes[] = $newClass; + } + + $input->setArgument('classes', $classes); + } } public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void { $controllerName = Str::asSnakeCase($input->getArgument('name')); $chosenExtension = $input->getArgument('extension'); - $targets = $input->getArgument('targets'); - $values = $input->getArgument('values'); + $targets = $targetArgs = $input->getArgument('targets') ?? []; + $values = $valuesArg = $input->getArgument('values') ?? []; + $classes = $classesArgs = $input->getArgument('classes') ?? []; $targets = empty($targets) ? $targets : \sprintf("['%s']", implode("', '", $targets)); + $classes = $classes ? \sprintf("['%s']", implode("', '", $classes)) : null; $fileName = \sprintf('%s_controller.%s', $controllerName, $chosenExtension); $filePath = \sprintf('assets/controllers/%s', $fileName); @@ -127,6 +147,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen [ 'targets' => $targets, 'values' => $values, + 'classes' => $classes, ] ); @@ -224,6 +245,29 @@ private function askForNextValue(ConsoleStyle $io, array $values, bool $isFirstV return ['name' => $valueName, 'type' => $type]; } + /** @param string[] $classes */ + private function askForNextClass(ConsoleStyle $io, array $classes, bool $isFirstClass): ?string + { + $questionText = 'New class name (press to stop adding classes)'; + + if (!$isFirstClass) { + $questionText = 'Add another class? Enter the class name (or press to stop adding classes)'; + } + + $className = $io->ask($questionText, validator: function (?string $name) use ($classes) { + if (str_contains($name, ' ')) { + throw new \InvalidArgumentException('Class name cannot contain spaces.'); + } + if (\in_array($name, $classes, true)) { + throw new \InvalidArgumentException(\sprintf('The "%s" class already exists.', $name)); + } + + return $name; + }); + + return $className ?: null; + } + private function printAvailableTypes(ConsoleStyle $io): void { foreach ($this->getValuesTypes() as $type) { diff --git a/templates/stimulus/Controller.tpl.php b/templates/stimulus/Controller.tpl.php index 10fa4dfd1..a506ee445 100644 --- a/templates/stimulus/Controller.tpl.php +++ b/templates/stimulus/Controller.tpl.php @@ -15,6 +15,7 @@ } + initialize() { // Called once when the controller is first instantiated (per element) diff --git a/tests/Maker/MakeStimulusControllerTest.php b/tests/Maker/MakeStimulusControllerTest.php index 7b455e597..083458d52 100644 --- a/tests/Maker/MakeStimulusControllerTest.php +++ b/tests/Maker/MakeStimulusControllerTest.php @@ -36,7 +36,7 @@ public function getTestDetails(): \Generator $this->assertFileExists($generatedFilePath); }), ]; - + yield 'it_generates_stimulus_controller_with_targets' => [$this->createMakerTest() ->run(function (MakerTestRunner $runner) { $runner->runMaker( @@ -87,6 +87,99 @@ public function getTestDetails(): \Generator }), ]; + yield 'it_generates_stimulus_controller_with_values' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'with_values', // controller name + 'js', // controller language + 'no', // no targets + 'yes', // values + 'min', // first value + 'Number', // first value type + 'email', // second values + 'String', // second value type + '', // empty input to stop adding values + ]); + + $generatedFilePath = $runner->getPath('assets/controllers/with_values_controller.js'); + + $this->assertFileExists($generatedFilePath); + + $generatedFileContents = file_get_contents($generatedFilePath); + $expectedContents = file_get_contents(__DIR__.'/../fixtures/make-stimulus-controller/with_values.js'); + + $this->assertSame( + $expectedContents, + $generatedFileContents + ); + }), + ]; + + yield 'it_generates_stimulus_controller_with_classes' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'with_classes', // controller name + 'js', // use default extension (js) + 'no', // do not add targets + 'no', // do not add values + 'yes', // add classes + 'foo', // first class + 'bar', // second class + '', // empty input to stop adding classes + ]); + + $generatedFilePath = $runner->getPath('assets/controllers/with_classes_controller.js'); + + $this->assertFileExists($generatedFilePath); + + $generatedFileContents = file_get_contents($generatedFilePath); + $expectedContents = file_get_contents(__DIR__.'/../fixtures/make-stimulus-controller/with_classes.js'); + + $this->assertSame( + $expectedContents, + $generatedFileContents + ); + }), + ]; + + yield 'it_generates_stimulus_controller_with_targets_values_and_classes' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $runner->runMaker( + [ + 'with_targets_values_classes', + 'js', + 'yes', // add targets + 'aaa', + 'bbb', + '', // end + 'yes', // add values + 'ccc', + 'Number', + 'ddd', + 'String', + '', // end + 'yes', // add classes + 'eee', + 'fff', + '', // end + ]); + + $generatedFilePath = $runner->getPath('assets/controllers/with_targets_values_classes_controller.js'); + + $this->assertFileExists($generatedFilePath); + + $generatedFileContents = file_get_contents($generatedFilePath); + $expectedContents = file_get_contents(__DIR__.'/../fixtures/make-stimulus-controller/with_targets_values_classes.js'); + + $this->assertSame( + $expectedContents, + $generatedFileContents + ); + }), + ]; + yield 'it_generates_typescript_stimulus_controller_interactively' => [$this->createMakerTest() ->run(function (MakerTestRunner $runner) { $runner->runMaker( @@ -101,7 +194,7 @@ public function getTestDetails(): \Generator $this->assertFileDoesNotExist($runner->getPath('assets/controllers/typescript_controller.js')); }), ]; - + yield 'it_generates_typescript_stimulus_controller_when_option_is_set' => [$this->createMakerTest() ->run(function (MakerTestRunner $runner) { $runner->runMaker( diff --git a/tests/fixtures/make-stimulus-controller/with_classes.js b/tests/fixtures/make-stimulus-controller/with_classes.js new file mode 100644 index 000000000..7fd71e0bc --- /dev/null +++ b/tests/fixtures/make-stimulus-controller/with_classes.js @@ -0,0 +1,39 @@ +import { Controller } from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers +*/ + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static classes = ['foo', 'bar'] + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } +} diff --git a/tests/fixtures/make-stimulus-controller/with_targets_values_classes.js b/tests/fixtures/make-stimulus-controller/with_targets_values_classes.js new file mode 100644 index 000000000..5e2d9170c --- /dev/null +++ b/tests/fixtures/make-stimulus-controller/with_targets_values_classes.js @@ -0,0 +1,44 @@ +import { Controller } from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers +*/ + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static targets = ['aaa', 'bbb'] + static values = { + ccc: Number, + ddd: String, + } + static classes = ['eee', 'fff'] + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } +} diff --git a/tests/fixtures/make-stimulus-controller/with_values.js b/tests/fixtures/make-stimulus-controller/with_values.js new file mode 100644 index 000000000..a5c1c2561 --- /dev/null +++ b/tests/fixtures/make-stimulus-controller/with_values.js @@ -0,0 +1,42 @@ +import { Controller } from '@hotwired/stimulus'; + +/* +* The following line makes this controller "lazy": it won't be downloaded until needed +* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers +*/ + +/* stimulusFetch: 'lazy' */ +export default class extends Controller { + static values = { + min: Number, + email: String, + } + + initialize() { + // Called once when the controller is first instantiated (per element) + + // Here you can initialize variables, create scoped callables for event + // listeners, instantiate external libraries, etc. + // this._fooBar = this.fooBar.bind(this) + } + + connect() { + // Called every time the controller is connected to the DOM + // (on page load, when it's added to the DOM, moved in the DOM, etc.) + + // Here you can add event listeners on the element or target elements, + // add or remove classes, attributes, dispatch custom events, etc. + // this.fooTarget.addEventListener('click', this._fooBar) + } + + // Add custom controller actions here + // fooBar() { this.fooTarget.classList.toggle(this.bazClass) } + + disconnect() { + // Called anytime its element is disconnected from the DOM + // (on page change, when it's removed from or moved in the DOM, etc.) + + // Here you should remove all event listeners added in "connect()" + // this.fooTarget.removeEventListener('click', this._fooBar) + } +} From f838b8ccd6d40a3bf032954287103ce23feb9f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 19:11:05 +0100 Subject: [PATCH 11/12] Generate an example usage --- src/Maker/MakeStimulusController.php | 50 +++++++++++++++++ tests/Maker/MakeStimulusControllerTest.php | 65 ++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index 1d5968c6f..ab872a95c 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -158,6 +158,11 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $io->text([ 'Next:', \sprintf('- Open %s and add the code you need', $filePath), + '- Use the controller in your templates:', + ...array_map( + fn (string $line): string => " $line", + explode("\n", $this->generateUsageExample($controllerName, $targetArgs, $valuesArg, $classesArgs)), + ), 'Find the documentation at https://symfony.com/bundles/StimulusBundle', ]); } @@ -287,6 +292,51 @@ private function getValuesTypes(): array ]; } + /** + * @param array $targets + * @param array{name: string, type: string} $values + * @param array $classes + */ + private function generateUsageExample(string $name, array $targets, array $values, array $classes): string + { + $slugify = fn (string $name) => str_replace('_', '-', Str::asSnakeCase($name)); + $controller = $slugify($name); + + $htmlTargets = []; + foreach ($targets as $target) { + $htmlTargets[] = \sprintf('
', $controller, $target); + } + + $htmlValues = []; + foreach ($values as ['name' => $name, 'type' => $type]) { + $value = match ($type) { + 'Array' => '[]', + 'Boolean' => 'false', + 'Number' => '123', + 'Object' => '{}', + 'String' => 'abc', + default => '', + }; + $htmlValues[] = \sprintf('data-%s-%s-value="%s"', $controller, $slugify($name), $value); + } + + $htmlClasses = []; + foreach ($classes as $class) { + $value = Str::asLowerCamelCase($class); + $htmlClasses[] = \sprintf('data-%s-%s-class="%s"', $controller, $slugify($class), $value); + } + + return \sprintf( + '
%s%s
', + $controller, + $htmlValues ? ("\n ".implode("\n ", $htmlValues)) : '', + $htmlClasses ? ("\n ".implode("\n ", $htmlClasses)) : '', + $htmlValues || $htmlClasses ? "\n" : '', + $htmlTargets ? ("\n ".implode("\n ", $htmlTargets)) : '', + "\n \n", + ); + } + public function configureDependencies(DependencyBuilder $dependencies): void { // lower than 8.1, allow WebpackEncoreBundle diff --git a/tests/Maker/MakeStimulusControllerTest.php b/tests/Maker/MakeStimulusControllerTest.php index 083458d52..647864849 100644 --- a/tests/Maker/MakeStimulusControllerTest.php +++ b/tests/Maker/MakeStimulusControllerTest.php @@ -210,5 +210,70 @@ public function getTestDetails(): \Generator $this->assertFileDoesNotExist($runner->getPath('assets/controllers/typescript_controller.js')); }), ]; + + yield 'it_displays_controller_basic_usage_example' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $output = $runner->runMaker( + [ + 'fooBar', + 'js', + ], + ); + + $usageExample = << + + + HTML; + + $this->assertStringContainsString('- Use the controller in your templates:', $output); + foreach (explode("\n", $usageExample) as $line) { + $this->assertStringContainsString($line, $output); + } + }), + ]; + + yield 'it_displays_controller_complete_usage_example' => [$this->createMakerTest() + ->run(function (MakerTestRunner $runner) { + $output = $runner->runMaker( + [ + 'fooBar', + 'js', + 'yes', // add targets + 'firstOne', + 'secondOne', + '', + 'yes', // add values + 'minItems', + 'Number', + 'email', + 'String', + '', + 'yes', // add classes + 'isVisible', + 'hidden', + '', + ], + ); + + $usageExample = << +
+
+ + + HTML; + + $this->assertStringContainsString('- Use the controller in your templates:', $output); + foreach (explode("\n", $usageExample) as $line) { + $this->assertStringContainsString($line, $output); + } + }), + ]; } } From 04770d75356c2b8abd55a69ada1722c6144b4231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Wed, 25 Dec 2024 20:02:28 +0100 Subject: [PATCH 12/12] phpstan happiness --- src/Maker/MakeStimulusController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Maker/MakeStimulusController.php b/src/Maker/MakeStimulusController.php index ab872a95c..103ffaa60 100644 --- a/src/Maker/MakeStimulusController.php +++ b/src/Maker/MakeStimulusController.php @@ -293,9 +293,9 @@ private function getValuesTypes(): array } /** - * @param array $targets - * @param array{name: string, type: string} $values - * @param array $classes + * @param array $targets + * @param array $values + * @param array $classes */ private function generateUsageExample(string $name, array $targets, array $values, array $classes): string { @@ -331,7 +331,7 @@ private function generateUsageExample(string $name, array $targets, array $value $controller, $htmlValues ? ("\n ".implode("\n ", $htmlValues)) : '', $htmlClasses ? ("\n ".implode("\n ", $htmlClasses)) : '', - $htmlValues || $htmlClasses ? "\n" : '', + ($htmlValues || $htmlClasses) ? "\n" : '', $htmlTargets ? ("\n ".implode("\n ", $htmlTargets)) : '', "\n \n", );