Skip to content

Commit ea80d29

Browse files
committed
feature #102 Adding support for custom attributes on rendered script and link tags (weaverryan)
This PR was squashed before being merged into the main branch. Discussion ---------- Adding support for custom attributes on rendered script and link tags Fixes #10 - thanks to @dsech for the idea of relying on named Twig arguments (otherwise the new Twig attributes argument is the 4th argument... and rather inconvenient). This can be configured in *3* different ways: ## 1) Globally ``` webpack_encore: # ... script_attributes: defer: true ``` 2) when when using the Twig functions: ```twig {{ encore_entry_script_tags('my_entry', attributes={ defer: true }) }} ``` 3) Via a new `RenderAssetTagEvent` As stated at the bottom of #10 - #10 (comment) - there may be some additional options that would also be nice, but this probably covers the biggest use-case. NOTE: This also drops support for EOL Symfony versions. Cheers! Commits ------- b26cc85 Adding support for custom attributes on rendered script and link tags
2 parents c879bc5 + b26cc85 commit ea80d29

File tree

12 files changed

+283
-36
lines changed

12 files changed

+283
-36
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ matrix:
2020
env: deps=low
2121
- php: 7.4
2222
env: SYMFONY_PHPUNIT_VERSION=9.4
23-
- php: nightly
23+
- php: 8.0
2424
env: SYMFONY_PHPUNIT_VERSION=9.4
2525

2626
before_install:

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ webpack_encore:
2424
output_path: '%kernel.project_dir%/public/build'
2525
# If multiple builds are defined (as shown below), you can disable the default build:
2626
# output_path: false
27+
28+
# Set attributes that will be rendered on all script and link tags
29+
# script_attributes:
30+
# defer: true
31+
# referrerpolicy: origin
32+
# link_attributes:
33+
# referrerpolicy: origin
2734

2835
# if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
2936
# crossorigin: 'anonymous'
@@ -84,6 +91,13 @@ For example, to render all of the `script` and `link` tags for a specific
8491
{{ parent() }}
8592
8693
{{ encore_entry_script_tags('entry1') }}
94+
95+
{# or render a custom attribute #}
96+
{#
97+
{{ encore_entry_script_tags('entry1', attributes={
98+
defer: true
99+
}) }}
100+
#}
87101
{% endblock %}
88102
89103
{% block stylesheets %}
@@ -144,3 +158,44 @@ class SomeController
144158
If you have multiple builds, you can also autowire
145159
`Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface`
146160
and use it to get the `EntrypointLookupInterface` object for any build.
161+
162+
## Custom Attributes on script and link Tags
163+
164+
Custom attributes can be added to rendered `script` or `link` in 3
165+
different ways:
166+
167+
1. Via global config (`script_attributes` and `link_attributes`) - see the
168+
config example above.
169+
170+
1. When rendering in Twig - see the `attributes` option in the docs above.
171+
172+
1. By listening to the `Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent`
173+
event. For example:
174+
175+
```php
176+
<?php
177+
178+
namespace App\EventSubscriber;
179+
180+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
181+
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
182+
183+
class ScriptNonceSubscriber implements EventSubscriberInterface
184+
{
185+
public static function getSubscribedEvents()
186+
{
187+
return [
188+
RenderAssetTagEvent::class => 'onRenderAssetTag'
189+
];
190+
}
191+
192+
public function onRenderAssetTag(RenderAssetTagEvent $event)
193+
{
194+
if ($event->isScriptTag()) {
195+
$event->setAttribute('nonce', 'lookup nonce');
196+
}
197+
}
198+
}
199+
```
200+
201+
Ok, have fun!

composer.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@
2222
"minimum-stability": "dev",
2323
"require": {
2424
"php": ">=7.1.3",
25-
"symfony/asset": "^3.4 || ^4.0 || ^5.0",
26-
"symfony/config": "^3.4 || ^4.0 || ^5.0",
27-
"symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0",
28-
"symfony/http-kernel": "^3.4 || ^4.0 || ^5.0",
25+
"symfony/asset": "^4.4 || ^5.0",
26+
"symfony/config": "^4.4 || ^5.0",
27+
"symfony/dependency-injection": "^4.4 || ^5.0",
28+
"symfony/http-kernel": "^4.4 || ^5.0",
2929
"symfony/service-contracts": "^1.0 || ^2.0"
3030
},
3131
"require-dev": {
32-
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0",
33-
"symfony/phpunit-bridge": "^4.3.5 || ^5.0",
34-
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0",
35-
"symfony/web-link": "^3.4 || ^4.0 || ^5.0"
32+
"symfony/framework-bundle": "^4.4 || ^5.0",
33+
"symfony/phpunit-bridge": "^4.4 || ^5.0",
34+
"symfony/twig-bundle": "^4.4 || ^5.0",
35+
"symfony/web-link": "^4.4 || ^5.0"
3636
},
3737
"extra": {
3838
"thanks": {

src/Asset/TagRenderer.php

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,31 @@
1111

1212
use Symfony\Component\Asset\Packages;
1313
use Symfony\Component\DependencyInjection\ServiceLocator;
14+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
1415
use Symfony\Contracts\Service\ResetInterface;
16+
use Symfony\WebpackEncoreBundle\Event\RenderAssetTagEvent;
1517

1618
/**
1719
* @final
1820
*/
1921
class TagRenderer implements ResetInterface
2022
{
2123
private $entrypointLookupCollection;
22-
2324
private $packages;
24-
2525
private $defaultAttributes;
26+
private $defaultScriptAttributes;
27+
private $defaultLinkAttributes;
28+
private $eventDispatcher;
2629

2730
private $renderedFiles = [];
2831

2932
public function __construct(
3033
$entrypointLookupCollection,
3134
Packages $packages,
32-
array $defaultAttributes = []
35+
array $defaultAttributes = [],
36+
array $defaultScriptAttributes = [],
37+
array $defaultLinkAttributes = [],
38+
EventDispatcherInterface $eventDispatcher = null
3339
) {
3440
if ($entrypointLookupCollection instanceof EntrypointLookupInterface) {
3541
@trigger_error(sprintf('The "$entrypointLookupCollection" argument in method "%s()" must be an instance of EntrypointLookupCollection.', __METHOD__), E_USER_DEPRECATED);
@@ -47,24 +53,39 @@ public function __construct(
4753

4854
$this->packages = $packages;
4955
$this->defaultAttributes = $defaultAttributes;
56+
$this->defaultScriptAttributes = $defaultScriptAttributes;
57+
$this->defaultLinkAttributes = $defaultLinkAttributes;
58+
$this->eventDispatcher = $eventDispatcher;
5059

5160
$this->reset();
5261
}
5362

54-
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
63+
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = null, array $extraAttributes = []): string
5564
{
65+
$entrypointName = $entrypointName ?: '_default';
5666
$scriptTags = [];
5767
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
5868
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
5969

6070
foreach ($entryPointLookup->getJavaScriptFiles($entryName) as $filename) {
61-
$attributes = $this->defaultAttributes;
71+
$attributes = [];
6272
$attributes['src'] = $this->getAssetPath($filename, $packageName);
73+
$attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultScriptAttributes, $extraAttributes);
6374

6475
if (isset($integrityHashes[$filename])) {
6576
$attributes['integrity'] = $integrityHashes[$filename];
6677
}
6778

79+
$event = new RenderAssetTagEvent(
80+
RenderAssetTagEvent::TYPE_SCRIPT,
81+
$attributes['src'],
82+
$attributes
83+
);
84+
if (null !== $this->eventDispatcher) {
85+
$event = $this->eventDispatcher->dispatch($event);
86+
}
87+
$attributes = $event->getAttributes();
88+
6889
$scriptTags[] = sprintf(
6990
'<script %s></script>',
7091
$this->convertArrayToAttributes($attributes)
@@ -76,21 +97,33 @@ public function renderWebpackScriptTags(string $entryName, string $packageName =
7697
return implode('', $scriptTags);
7798
}
7899

79-
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
100+
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = null, array $extraAttributes = []): string
80101
{
102+
$entrypointName = $entrypointName ?: '_default';
81103
$scriptTags = [];
82104
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
83105
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
84106

85107
foreach ($entryPointLookup->getCssFiles($entryName) as $filename) {
86-
$attributes = $this->defaultAttributes;
108+
$attributes = [];
87109
$attributes['rel'] = 'stylesheet';
88110
$attributes['href'] = $this->getAssetPath($filename, $packageName);
111+
$attributes = array_merge($attributes, $this->defaultAttributes, $this->defaultLinkAttributes, $extraAttributes);
89112

90113
if (isset($integrityHashes[$filename])) {
91114
$attributes['integrity'] = $integrityHashes[$filename];
92115
}
93116

117+
$event = new RenderAssetTagEvent(
118+
RenderAssetTagEvent::TYPE_LINK,
119+
$attributes['href'],
120+
$attributes
121+
);
122+
if (null !== $this->eventDispatcher) {
123+
$this->eventDispatcher->dispatch($event);
124+
}
125+
$attributes = $event->getAttributes();
126+
94127
$scriptTags[] = sprintf(
95128
'<link %s>',
96129
$this->convertArrayToAttributes($attributes)
@@ -146,6 +179,10 @@ private function convertArrayToAttributes(array $attributesMap): string
146179
{
147180
return implode(' ', array_map(
148181
function ($key, $value) {
182+
// allows for things like defer: true to only render "defer"
183+
if ($value === true) {
184+
return $key;
185+
}
149186
return sprintf('%s="%s"', $key, htmlentities($value));
150187
},
151188
array_keys($attributesMap),

src/DependencyInjection/Configuration.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,31 @@ public function getConfigTreeBuilder()
5555
->useAttributeAsKey('name')
5656
->normalizeKeys(false)
5757
->scalarPrototype()
58-
->validate()
58+
->validate()
5959
->always(function ($values) {
6060
if (isset($values['_default'])) {
6161
throw new InvalidDefinitionException("Key '_default' can't be used as build name.");
6262
}
6363

6464
return $values;
6565
})
66+
->end()
6667
->end()
6768
->end()
69+
->arrayNode('script_attributes')
70+
->info('Key/value pair of attributes to render on all script tags')
71+
->example('{ defer: true, referrerpolicy: "origin" }')
72+
->useAttributeAsKey('name')
73+
->normalizeKeys(false)
74+
->scalarPrototype()->end()
75+
->end()
76+
->arrayNode('link_attributes')
77+
->info('Key/value pair of attributes to render on all CSS link tags')
78+
->example('{ referrerpolicy: "origin" }')
79+
->useAttributeAsKey('name')
80+
->normalizeKeys(false)
81+
->scalarPrototype()->end()
82+
->end()
6883
->end()
6984
;
7085

src/DependencyInjection/WebpackEncoreExtension.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ public function load(array $configs, ContainerBuilder $container)
6868
}
6969

7070
$container->getDefinition('webpack_encore.tag_renderer')
71-
->replaceArgument(2, $defaultAttributes);
71+
->replaceArgument(2, $defaultAttributes)
72+
->replaceArgument(3, $config['script_attributes'])
73+
->replaceArgument(4, $config['link_attributes']);
7274

7375
if ($config['preload']) {
7476
if (!class_exists(AddLinkHeaderListener::class)) {

src/Event/RenderAssetTagEvent.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Event;
11+
12+
/**
13+
* Dispatched each time a script or link tag is rendered.
14+
*/
15+
final class RenderAssetTagEvent
16+
{
17+
public const TYPE_SCRIPT = 'script';
18+
public const TYPE_LINK = 'link';
19+
20+
private $type;
21+
private $url;
22+
private $attributes;
23+
24+
public function __construct(string $type, string $url, array $attributes)
25+
{
26+
$this->type = $type;
27+
$this->url = $url;
28+
$this->attributes = $attributes;
29+
}
30+
31+
public function isScriptTag(): bool
32+
{
33+
return $this->type === self::TYPE_SCRIPT;
34+
}
35+
36+
public function isLinkTag(): bool
37+
{
38+
return $this->type === self::TYPE_LINK;
39+
}
40+
41+
public function getUrl(): string
42+
{
43+
return $this->url;
44+
}
45+
46+
public function getAttributes(): array
47+
{
48+
return $this->attributes;
49+
}
50+
51+
/**
52+
* @param string $name The attribute name
53+
* @param string|bool $value Value can be "true" to have an attribute without a value (e.g. "defer")
54+
*/
55+
public function setAttribute(string $name, $value): void
56+
{
57+
$this->attributes[$name] = $value;
58+
}
59+
60+
public function removeAttribute(string $name): void
61+
{
62+
unset($this->attributes[$name]);
63+
}
64+
}

src/Resources/config/services.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
<tag name="kernel.reset" method="reset" />
1818
<argument type="service" id="webpack_encore.entrypoint_lookup_collection" />
1919
<argument type="service" id="assets.packages" />
20-
<argument type="collection" />
20+
<argument type="collection"/> <!-- Default attributes-->
21+
<argument type="collection"/> <!-- Default script attributes -->
22+
<argument type="collection"/> <!-- Default link attributes -->
23+
<argument type="service" id="event_dispatcher" />
2124
</service>
2225

2326
<service id="webpack_encore.twig_entry_files_extension" class="Symfony\WebpackEncoreBundle\Twig\EntryFilesTwigExtension">

src/Twig/EntryFilesTwigExtension.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ public function getWebpackCssFiles(string $entryName, string $entrypointName = '
4646
->getCssFiles($entryName);
4747
}
4848

49-
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
49+
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default', array $attributes = []): string
5050
{
5151
return $this->getTagRenderer()
52-
->renderWebpackScriptTags($entryName, $packageName, $entrypointName);
52+
->renderWebpackScriptTags($entryName, $packageName, $entrypointName, $attributes);
5353
}
5454

55-
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
55+
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default', array $attributes = []): string
5656
{
5757
return $this->getTagRenderer()
58-
->renderWebpackLinkTags($entryName, $packageName, $entrypointName);
58+
->renderWebpackLinkTags($entryName, $packageName, $entrypointName, $attributes);
5959
}
6060

6161
private function getEntrypointLookup(string $entrypointName): EntrypointLookupInterface

0 commit comments

Comments
 (0)