Skip to content

Commit 92529dc

Browse files
committed
WIP - html_attributes function
WIP
1 parent 34e874e commit 92529dc

File tree

2 files changed

+463
-0
lines changed

2 files changed

+463
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
namespace Twig\Extra\Html;
4+
5+
use Twig\Error\RuntimeError;
6+
7+
final class HtmlAttributes
8+
{
9+
/**
10+
* Merges multiple attribute group arrays into a single array.
11+
*
12+
* `HtmlAttributes::merge(['id' => 'a', 'disabled' => true], ['hidden' => true])` becomes
13+
* `['id' => 'a', 'disabled' => true, 'hidden' => true]`
14+
*
15+
* attributes override each other in the order they are provided.
16+
*
17+
* `HtmlAttributes::merge(['id' => 'a'], ['id' => 'b'])` becomes `['id' => 'b']`.
18+
*
19+
* However, `class` and `style` attributes are merged into an array so they can be concatenated in later processing.
20+
*
21+
* `HtmlAttributes::merge(['class' => 'a'], ['class' => 'b'], ['class' => 'c'])` becomes
22+
* `['class' => ['a' => true, 'b' => true, 'c' => true]]`.
23+
*
24+
* style attributes are also merged into an array so they can be concatenated in later processing.
25+
*
26+
* `HtmlAttributes::merge(['style' => 'color: red'], ['style' => ['background-color' => 'blue']])` becomes
27+
* `['style' => ['color: red;' => true, 'background-color: blue;' => true]]`.
28+
*
29+
* style attributes which are arrays with false and null values are also processed
30+
*
31+
* `HtmlAttributes::merge(['style' => ['color: red' => true]], ['style' => ['display: block' => false]]) becomes
32+
* `['style' => ['color: red;' => true, 'display: block;' => false]]`.
33+
*
34+
* attributes can be provided as an array of key, value where the value can be true, false or null.
35+
*
36+
* Example:
37+
* `HtmlAttributes::merge(['class' => ['a' => true, 'b' => false], ['class' => ['c' => null']])` becomes
38+
* `['class' => ['a' => true, 'b' => false, 'c' => null]]`.
39+
*
40+
* `aria` and `data` arrays are expanded into `aria-*` and `data-*` attributes before further processing.
41+
*
42+
* Example:
43+
*
44+
* `HtmlAttributes::merge([data' => ['count' => '1']])` becomes `['data-count' => '1']`.
45+
* `HtmlAttributes::merge(['aria' => ['hidden' => true]])` becomes `['aria-hidden' => true]`.
46+
*
47+
* @param ...$attributeGroup
48+
* @return array
49+
* @throws RuntimeError
50+
* @see ./Tests/HtmlAttributesTest.php for usage examples
51+
*
52+
*/
53+
public static function merge(...$attributeGroup): array
54+
{
55+
$result = [];
56+
57+
$attributeGroupCount = 0;
58+
59+
foreach ($attributeGroup as $attributes) {
60+
61+
$attributeGroupCount++;
62+
63+
// Skip empty attributes
64+
// Return early if no attributes are provided
65+
// This could be false or null when using the twig ternary operator
66+
if (!$attributes) {
67+
continue;
68+
}
69+
70+
if (!is_iterable($attributes)) {
71+
throw new RuntimeError(sprintf('"%s" only works with mappings or "Traversable", got "%s" for argument %d.', self::class, \gettype($attributes), $attributeGroupCount));
72+
}
73+
74+
// Alternative to is_iterable check above, cast the attributes to an array
75+
// This would produce weird results but would not throw an error
76+
// $attributes = (array)$attributes;
77+
78+
// data and aria arrays are expanded into data-* and aria-* attributes
79+
$expanded = [];
80+
foreach ($attributes as $key => $value) {
81+
if (in_array($key, ['data', 'aria'])) {
82+
$value = (array)$value;
83+
foreach ($value as $k => $v) {
84+
$k = $key . '-' . $k;
85+
$expanded[$k] = $v;
86+
}
87+
continue;
88+
}
89+
$expanded[$key] = $value;
90+
}
91+
92+
// Reset the attributes array to the flattened version
93+
$attributes = $expanded;
94+
95+
foreach ($attributes as $key => $value) {
96+
97+
// Treat class and data-controller attributes as arrays
98+
if (in_array($key, [
99+
'class',
100+
'data-controller',
101+
'data-action',
102+
'data-targets',
103+
])) {
104+
if (!array_key_exists($key, $result)) {
105+
$result[$key] = [];
106+
}
107+
$value = (array)$value;
108+
foreach ($value as $k => $v) {
109+
if (is_int($k)) {
110+
$classes = explode(' ', $v);
111+
foreach ($classes as $class) {
112+
$result[$key][$class] = true;
113+
}
114+
} else {
115+
$classes = explode(' ', $k);
116+
foreach ($classes as $class) {
117+
$result[$key][$class] = $v;
118+
}
119+
}
120+
}
121+
continue;
122+
}
123+
124+
if ($key === 'style') {
125+
if (!array_key_exists('style', $result)) {
126+
$result['style'] = [];
127+
}
128+
$value = (array)$value;
129+
foreach ($value as $k => $v) {
130+
if (is_int($k)) {
131+
$styles = array_filter(explode(';', $v));
132+
foreach ($styles as $style) {
133+
$style = explode(':', $style);
134+
$sKey = trim($style[0]);
135+
$sValue = trim($style[1]);
136+
$result['style']["$sKey: $sValue;"] = true;
137+
}
138+
} elseif (is_bool($v) || is_null($v)) {
139+
$styles = array_filter(explode(';', $k));
140+
foreach ($styles as $style) {
141+
$style = explode(':', $style);
142+
$sKey = trim($style[0]);
143+
$sValue = trim($style[1]);
144+
$result['style']["$sKey: $sValue;"] = $v;
145+
}
146+
} else {
147+
$sKey = trim($k);
148+
$sValue = trim($v);
149+
$result['style']["$sKey: $sValue;"] = true;
150+
}
151+
}
152+
continue;
153+
}
154+
155+
$result[$key] = $value;
156+
}
157+
}
158+
159+
return $result;
160+
}
161+
162+
public static function renderAttributes($attributes): string
163+
{
164+
$return = [];
165+
166+
foreach ($attributes as $key => $value) {
167+
168+
// Skip null values regardless of attribute key
169+
if ($value === null) {
170+
continue;
171+
}
172+
173+
// Handle class, style, data-controller value coercion
174+
// array[] -> concatenate string
175+
if (in_array($key, ['class', 'style', 'data-controller'])) {
176+
$value = array_filter($value);
177+
$value = array_keys($value);
178+
$value = implode(' ', $value);
179+
}
180+
181+
// Handle aria-* value coercion
182+
// true -> 'true'
183+
// false -> 'false,
184+
// array[] -> concatenate string
185+
if (str_starts_with($key, 'aria-')) {
186+
if ($value === true) {
187+
$value = 'true';
188+
} elseif ($value === false) {
189+
$value = 'false';
190+
} elseif(is_array($value)) {
191+
$value = join(" ", array_filter($value));
192+
}
193+
194+
}
195+
196+
// Handle data-* value coercion
197+
// array[] -> json
198+
if (str_starts_with($key, 'data-')) {
199+
if(is_array($value)) {
200+
$value = json_encode($value);
201+
}
202+
}
203+
204+
// Skip false values
205+
if ($value === false) {
206+
continue;
207+
}
208+
209+
// Boolean attribute doesn't have a value
210+
if ($value === true) {
211+
$return[] = $key;
212+
continue;
213+
}
214+
215+
// Everything else gets added as an encoded value
216+
$return[] = $key . '="' . htmlspecialchars($value) . '"';
217+
}
218+
219+
return implode(' ', $return);
220+
}
221+
}

0 commit comments

Comments
 (0)