Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[make:stimulus-controller] Add classes support, generate usage code, fix doc, add tests #1631

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
14 changes: 12 additions & 2 deletions config/help/MakeStimulusController.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
The <info>%command.name%</info> command generates new Stimulus Controller.
The <info>%command.name%</info> command generates a new Stimulus controller.

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

If the argument is missing, the command will ask for the controller name interactively.
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 <info>--typescript</info>
(or <info>--ts</info>) option:

<info>php %command.full_name% hello --typescript</info>

It will also interactively ask for values, targets, classes to add to the Stimulus
controller (optional).

<info>php %command.full_name%</info>
127 changes: 115 additions & 12 deletions src/Maker/MakeStimulusController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,25 +45,34 @@ public function configureCommand(Command $command, InputConfiguration $inputConf
{
$command
->addArgument('name', InputArgument::REQUIRED, 'The name of the Stimulus controller (e.g. <fg=yellow>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
{
$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 (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)',
[
'js' => 'JavaScript',
'ts' => 'TypeScript',
],
'js',
);

$chosenExtension = $io->choice(
'Language (<fg=yellow>JavaScript</> or <fg=yellow>TypeScript</>)',
[
'js' => 'JavaScript',
'ts' => 'TypeScript',
]
);

$input->setArgument('extension', $chosenExtension);
$input->setArgument('extension', $chosenExtension);
}

if ($io->confirm('Do you want to include targets?')) {
$targets = [];
Expand Down Expand Up @@ -98,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);
Expand All @@ -118,6 +147,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
[
'targets' => $targets,
'values' => $values,
'classes' => $classes,
]
);

Expand All @@ -128,7 +158,12 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$io->text([
'Next:',
\sprintf('- Open <info>%s</info> and add the code you need', $filePath),
'Find the documentation at <fg=yellow>https://github.com/symfony/stimulus-bridge</>',
'- 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 <fg=yellow>https://symfony.com/bundles/StimulusBundle</>',
]);
}

Expand Down Expand Up @@ -215,6 +250,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 <return> to stop adding classes)';

if (!$isFirstClass) {
$questionText = 'Add another class? Enter the class name (or press <return> 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) {
Expand All @@ -234,6 +292,51 @@ private function getValuesTypes(): array
];
}

/**
* @param array<int, string> $targets
* @param array<array{name: string, type: string}> $values
* @param array<int, string> $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('<div data-%s-target="%s"></div>', $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(
'<div data-controller="%s"%s%s%s>%s%s</div>',
$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
Expand Down
33 changes: 31 additions & 2 deletions templates/stimulus/Controller.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

/*
* 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 {
<?= $targets ? " static targets = $targets\n" : "" ?>
Expand All @@ -14,5 +15,33 @@
<?php endforeach; ?>
}
<?php } ?>
// ...
<?= $classes ? " static classes = $classes\n" : '' ?>

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)
}
}
Loading
Loading