Skip to content

Commit 881ecf0

Browse files
authored
[make:stimulus-controller] Add classes support, generate usage code, fix doc, add tests (#1631)
* Replace StimulusBridge with StimulusBundle in docblock * Add methods initialize, connect and disconnect with code comment * Add line break before "stimulusFetch: lazy" to emphasis line and help UX parsers * Update tests for lazy doc changes * Update tests for empty line before lazy * Update tests with base methods * Use 'JavaScript' as default extension * Add `--typescript` / `--ts` (non interactive) option (default false) * Update Maker documentation link displayed after code generation * Define classes interactively * Generate an example usage * phpstan happiness
1 parent 468ff27 commit 881ecf0

File tree

9 files changed

+534
-22
lines changed

9 files changed

+534
-22
lines changed
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
The <info>%command.name%</info> command generates new Stimulus Controller.
1+
The <info>%command.name%</info> command generates a new Stimulus controller.
22

33
<info>php %command.full_name% hello</info>
44

5-
If the argument is missing, the command will ask for the controller name interactively.
5+
If the argument is missing, the command will ask for the controller name interactively.
6+
7+
To generate a TypeScript file (instead of a JavaScript file) use the <info>--typescript</info>
8+
(or <info>--ts</info>) option:
9+
10+
<info>php %command.full_name% hello --typescript</info>
11+
12+
It will also interactively ask for values, targets, classes to add to the Stimulus
13+
controller (optional).
14+
15+
<info>php %command.full_name%</info>

src/Maker/MakeStimulusController.php

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Console\Command\Command;
2020
use Symfony\Component\Console\Input\InputArgument;
2121
use Symfony\Component\Console\Input\InputInterface;
22+
use Symfony\Component\Console\Input\InputOption;
2223
use Symfony\Component\Console\Question\Question;
2324
use Symfony\UX\StimulusBundle\StimulusBundle;
2425
use Symfony\WebpackEncoreBundle\WebpackEncoreBundle;
@@ -44,25 +45,34 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
4445
{
4546
$command
4647
->addArgument('name', InputArgument::REQUIRED, 'The name of the Stimulus controller (e.g. <fg=yellow>hello</>)')
48+
->addOption('typescript', 'ts', InputOption::VALUE_NONE, 'Create a TypeScript controller (default is JavaScript)')
4749
->setHelp($this->getHelpFileContents('MakeStimulusController.txt'))
4850
;
51+
52+
$inputConfig->setArgumentAsNonInteractive('typescript');
4953
}
5054

5155
public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void
5256
{
5357
$command->addArgument('extension', InputArgument::OPTIONAL);
5458
$command->addArgument('targets', InputArgument::OPTIONAL);
5559
$command->addArgument('values', InputArgument::OPTIONAL);
60+
$command->addArgument('classes', InputArgument::OPTIONAL);
61+
62+
if ($input->getOption('typescript')) {
63+
$input->setArgument('extension', 'ts');
64+
} else {
65+
$chosenExtension = $io->choice(
66+
'Language (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)',
67+
[
68+
'js' => 'JavaScript',
69+
'ts' => 'TypeScript',
70+
],
71+
'js',
72+
);
5673

57-
$chosenExtension = $io->choice(
58-
'Language (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)',
59-
[
60-
'js' => 'JavaScript',
61-
'ts' => 'TypeScript',
62-
]
63-
);
64-
65-
$input->setArgument('extension', $chosenExtension);
74+
$input->setArgument('extension', $chosenExtension);
75+
}
6676

6777
if ($io->confirm('Do you want to include targets?')) {
6878
$targets = [];
@@ -98,16 +108,35 @@ public function interact(InputInterface $input, ConsoleStyle $io, Command $comma
98108

99109
$input->setArgument('values', $values);
100110
}
111+
112+
if ($io->confirm('Do you want to add classes?', false)) {
113+
$classes = [];
114+
$isFirstClass = true;
115+
116+
while (true) {
117+
$newClass = $this->askForNextClass($io, $classes, $isFirstClass);
118+
if (null === $newClass) {
119+
break;
120+
}
121+
122+
$isFirstClass = false;
123+
$classes[] = $newClass;
124+
}
125+
126+
$input->setArgument('classes', $classes);
127+
}
101128
}
102129

103130
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
104131
{
105132
$controllerName = Str::asSnakeCase($input->getArgument('name'));
106133
$chosenExtension = $input->getArgument('extension');
107-
$targets = $input->getArgument('targets');
108-
$values = $input->getArgument('values');
134+
$targets = $targetArgs = $input->getArgument('targets') ?? [];
135+
$values = $valuesArg = $input->getArgument('values') ?? [];
136+
$classes = $classesArgs = $input->getArgument('classes') ?? [];
109137

110138
$targets = empty($targets) ? $targets : \sprintf("['%s']", implode("', '", $targets));
139+
$classes = $classes ? \sprintf("['%s']", implode("', '", $classes)) : null;
111140

112141
$fileName = \sprintf('%s_controller.%s', $controllerName, $chosenExtension);
113142
$filePath = \sprintf('assets/controllers/%s', $fileName);
@@ -118,6 +147,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
118147
[
119148
'targets' => $targets,
120149
'values' => $values,
150+
'classes' => $classes,
121151
]
122152
);
123153

@@ -128,7 +158,12 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
128158
$io->text([
129159
'Next:',
130160
\sprintf('- Open <info>%s</info> and add the code you need', $filePath),
131-
'Find the documentation at <fg=yellow>https://github.com/symfony/stimulus-bridge</>',
161+
'- Use the controller in your templates:',
162+
...array_map(
163+
fn (string $line): string => " $line",
164+
explode("\n", $this->generateUsageExample($controllerName, $targetArgs, $valuesArg, $classesArgs)),
165+
),
166+
'Find the documentation at <fg=yellow>https://symfony.com/bundles/StimulusBundle</>',
132167
]);
133168
}
134169

@@ -215,6 +250,29 @@ private function askForNextValue(ConsoleStyle $io, array $values, bool $isFirstV
215250
return ['name' => $valueName, 'type' => $type];
216251
}
217252

253+
/** @param string[] $classes */
254+
private function askForNextClass(ConsoleStyle $io, array $classes, bool $isFirstClass): ?string
255+
{
256+
$questionText = 'New class name (press <return> to stop adding classes)';
257+
258+
if (!$isFirstClass) {
259+
$questionText = 'Add another class? Enter the class name (or press <return> to stop adding classes)';
260+
}
261+
262+
$className = $io->ask($questionText, validator: function (?string $name) use ($classes) {
263+
if (str_contains($name, ' ')) {
264+
throw new \InvalidArgumentException('Class name cannot contain spaces.');
265+
}
266+
if (\in_array($name, $classes, true)) {
267+
throw new \InvalidArgumentException(\sprintf('The "%s" class already exists.', $name));
268+
}
269+
270+
return $name;
271+
});
272+
273+
return $className ?: null;
274+
}
275+
218276
private function printAvailableTypes(ConsoleStyle $io): void
219277
{
220278
foreach ($this->getValuesTypes() as $type) {
@@ -234,6 +292,51 @@ private function getValuesTypes(): array
234292
];
235293
}
236294

295+
/**
296+
* @param array<int, string> $targets
297+
* @param array<array{name: string, type: string}> $values
298+
* @param array<int, string> $classes
299+
*/
300+
private function generateUsageExample(string $name, array $targets, array $values, array $classes): string
301+
{
302+
$slugify = fn (string $name) => str_replace('_', '-', Str::asSnakeCase($name));
303+
$controller = $slugify($name);
304+
305+
$htmlTargets = [];
306+
foreach ($targets as $target) {
307+
$htmlTargets[] = \sprintf('<div data-%s-target="%s"></div>', $controller, $target);
308+
}
309+
310+
$htmlValues = [];
311+
foreach ($values as ['name' => $name, 'type' => $type]) {
312+
$value = match ($type) {
313+
'Array' => '[]',
314+
'Boolean' => 'false',
315+
'Number' => '123',
316+
'Object' => '{}',
317+
'String' => 'abc',
318+
default => '',
319+
};
320+
$htmlValues[] = \sprintf('data-%s-%s-value="%s"', $controller, $slugify($name), $value);
321+
}
322+
323+
$htmlClasses = [];
324+
foreach ($classes as $class) {
325+
$value = Str::asLowerCamelCase($class);
326+
$htmlClasses[] = \sprintf('data-%s-%s-class="%s"', $controller, $slugify($class), $value);
327+
}
328+
329+
return \sprintf(
330+
'<div data-controller="%s"%s%s%s>%s%s</div>',
331+
$controller,
332+
$htmlValues ? ("\n ".implode("\n ", $htmlValues)) : '',
333+
$htmlClasses ? ("\n ".implode("\n ", $htmlClasses)) : '',
334+
($htmlValues || $htmlClasses) ? "\n" : '',
335+
$htmlTargets ? ("\n ".implode("\n ", $htmlTargets)) : '',
336+
"\n <!-- ... -->\n",
337+
);
338+
}
339+
237340
public function configureDependencies(DependencyBuilder $dependencies): void
238341
{
239342
// lower than 8.1, allow WebpackEncoreBundle

templates/stimulus/Controller.tpl.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
/*
44
* The following line makes this controller "lazy": it won't be downloaded until needed
5-
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
5+
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
66
*/
7+
78
/* stimulusFetch: 'lazy' */
89
export default class extends Controller {
910
<?= $targets ? " static targets = $targets\n" : "" ?>
@@ -14,5 +15,33 @@
1415
<?php endforeach; ?>
1516
}
1617
<?php } ?>
17-
// ...
18+
<?= $classes ? " static classes = $classes\n" : '' ?>
19+
20+
initialize() {
21+
// Called once when the controller is first instantiated (per element)
22+
23+
// Here you can initialize variables, create scoped callables for event
24+
// listeners, instantiate external libraries, etc.
25+
// this._fooBar = this.fooBar.bind(this)
26+
}
27+
28+
connect() {
29+
// Called every time the controller is connected to the DOM
30+
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
31+
32+
// Here you can add event listeners on the element or target elements,
33+
// add or remove classes, attributes, dispatch custom events, etc.
34+
// this.fooTarget.addEventListener('click', this._fooBar)
35+
}
36+
37+
// Add custom controller actions here
38+
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
39+
40+
disconnect() {
41+
// Called anytime its element is disconnected from the DOM
42+
// (on page change, when it's removed from or moved in the DOM, etc.)
43+
44+
// Here you should remove all event listeners added in "connect()"
45+
// this.fooTarget.removeEventListener('click', this._fooBar)
46+
}
1847
}

0 commit comments

Comments
 (0)