Skip to content

Commit 84a2d6b

Browse files
committed
Introduce stimulus_controller to ease Stimulus Values API usage
1 parent 4dc8ecb commit 84a2d6b

File tree

4 files changed

+149
-0
lines changed

4 files changed

+149
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony WebpackEncoreBundle package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\WebpackEncoreBundle\Exception;
11+
12+
class MissingPackageException extends \LogicException
13+
{
14+
}

src/Resources/config/services.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
</argument>
3737
</service>
3838

39+
<service id="webpack_encore.twig_stimulus_extension" class="Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension">
40+
<tag name="twig.extension" />
41+
</service>
42+
3943
<service id="webpack_encore.entrypoint_lookup.cache_warmer" class="Symfony\WebpackEncoreBundle\CacheWarmer\EntrypointCacheWarmer">
4044
<tag name="kernel.cache_warmer" />
4145
<argument /> <!-- build list of entrypoint paths -->

src/Twig/StimulusTwigExtension.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony WebpackEncoreBundle package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\WebpackEncoreBundle\Twig;
11+
12+
use Symfony\WebpackEncoreBundle\Exception\MissingPackageException;
13+
use Twig\Environment;
14+
use Twig\Extension\AbstractExtension;
15+
use Twig\TwigFunction;
16+
17+
final class StimulusTwigExtension extends AbstractExtension
18+
{
19+
public function getFunctions()
20+
{
21+
return [
22+
new TwigFunction('stimulus_controller', [$this, 'renderStimulusController'], ['needs_environment' => true, 'is_safe' => ['all']]),
23+
];
24+
}
25+
26+
public function renderStimulusController(Environment $env, array $data): string
27+
{
28+
if (!$data) {
29+
return '';
30+
}
31+
32+
$controllers = [];
33+
$values = [];
34+
35+
foreach ($data as $controllerName => $controllerValues) {
36+
$controllers[] = $controllerName;
37+
38+
foreach ($controllerValues as $key => $value) {
39+
if (!is_scalar($value)) {
40+
$value = json_encode($value);
41+
}
42+
43+
$value = twig_escape_filter($env, $value, 'html_attr');
44+
$values[] = 'data-'.$controllerName.'-'.$this->convertKeyCase($key).'-value="'.$value.'"';
45+
}
46+
}
47+
48+
return rtrim('data-controller="'.implode(' ', $controllers).'" '.implode(' ', $values));
49+
}
50+
51+
/**
52+
* Convert a Value key into the HTML equivalent for Stimulus ("kebab case").
53+
* Backport features from symfony/string.
54+
*
55+
* @param string $string
56+
* @return string
57+
*/
58+
private function convertKeyCase(string $str): string
59+
{
60+
// AbstractUnicodeString::camel
61+
$str = str_replace(' ', '', preg_replace_callback('/\b./u', static function ($m) use (&$i) {
62+
return 1 === ++$i ? ('İ' === $m[0] ? '' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
63+
}, preg_replace('/[^\pL0-9]++/u', ' ', $str)));
64+
65+
// AbstractUnicodeString::title
66+
$str = preg_replace_callback('/\b./u', static function (array $m): string {
67+
return mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8');
68+
}, $str, 1);
69+
70+
return mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1-\2', $str), 'UTF-8');
71+
}
72+
}

tests/IntegrationTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
2929
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
3030
use Symfony\WebpackEncoreBundle\CacheWarmer\EntrypointCacheWarmer;
31+
use Symfony\WebpackEncoreBundle\Twig\StimulusTwigExtension;
3132
use Symfony\WebpackEncoreBundle\WebpackEncoreBundle;
3233

3334
class IntegrationTest extends TestCase
@@ -178,6 +179,64 @@ public function testAutowireDefaultBuildArgument()
178179
$this->assertTrue(true);
179180
}
180181

182+
public function provideRenderStimulusController()
183+
{
184+
yield 'empty' => [
185+
'data' => [],
186+
'expected' => '',
187+
];
188+
189+
yield 'single-controller-no-data' => [
190+
'data' => [
191+
'my-controller' => [],
192+
],
193+
'expected' => 'data-controller="my-controller"',
194+
];
195+
196+
yield 'single-controller-scalar-data' => [
197+
'data' => [
198+
'my-controller' => [
199+
'myValue' => 'scalar-value',
200+
],
201+
],
202+
'expected' => 'data-controller="my-controller" data-my-controller-my-value-value="scalar-value"',
203+
];
204+
205+
yield 'single-controller-nested-data' => [
206+
'data' => [
207+
'my-controller' => [
208+
'myValue' => ['nested' => 'array'],
209+
],
210+
],
211+
'expected' => 'data-controller="my-controller" data-my-controller-my-value-value="&#x7B;&quot;nested&quot;&#x3A;&quot;array&quot;&#x7D;"',
212+
];
213+
214+
yield 'multiple-controllers-scalar-data' => [
215+
'data' => [
216+
'my-controller' => [
217+
'myValue' => 'scalar-value',
218+
],
219+
'another-controller' => [
220+
'anotherValue' => 'scalar-value 2',
221+
],
222+
],
223+
'expected' => 'data-controller="my-controller another-controller" data-my-controller-my-value-value="scalar-value" data-another-controller-another-value-value="scalar-value&#x20;2"',
224+
];
225+
}
226+
227+
/**
228+
* @dataProvider provideRenderStimulusController
229+
*/
230+
public function testRenderStimulusController(array $data, string $expected)
231+
{
232+
$kernel = new WebpackEncoreIntegrationTestKernel(true);
233+
$kernel->boot();
234+
$twig = $this->getTwigEnvironmentFromBootedKernel($kernel);
235+
236+
$extension = new StimulusTwigExtension();
237+
$this->assertSame($expected, $extension->renderStimulusController($twig, $data));
238+
}
239+
181240
private function getContainerFromBootedKernel(WebpackEncoreIntegrationTestKernel $kernel)
182241
{
183242
if ($kernel::VERSION_ID >= 40100) {

0 commit comments

Comments
 (0)