RFC / WIP - html_attributes function
An alternative implementation to #3930
Addresses:
- https://github.com/twigphp/Twig/issues/3907
- https://github.com/twigphp/html-extra/pull/4
- https://github.com/twigphp/Twig/pull/3760
This WIP pr demonstrates a html attribute merging strategy for html attributes, aria-attributes with special handling for class, style data and aria attributes.
I've currently focussed on the HtmlAttributes::merge function which returns the merged attributes array.
Examples
See ./Tests/HtmlAttributesTest.php for usage examples
// Basic attribute merging
HtmlAttributes::merge(
['id' => 'a', 'disabled' => true],
['hidden' => true]
)
===
['id' => 'a', 'disabled' => true, 'hidden' => true]
// Attribute overriding
HtmlAttributes::merge(
['id' => 'a'],
['id' => 'b']
)
===
['id' => 'b']
// class merging
// class attributes produce an array of key / values where the value is true, false, null.
// multiple classnames are split on spaces
HtmlAttributes::merge(
['class' => 'a'],
['class' => 'b'],
['class' => 'c'],
['class' => 'd e']
)
===
['class' => ['a' => true, 'b' => true, 'c' => true, 'd' => true, 'e' => true]]
// Style merging
// values provided as string, array of strings or key / value array
// styles are split on ":" and converted to key / value pairs
HtmlAttributes::merge(
['style' => 'color: red'],
['style' => ['color: green']],
['style' => ['background-color' => 'blue']],
['style' => ['display: block' => false]]
)
===
['style' => ['color: green;' => 'true', 'background-color: blue;' => 'true', 'display: block;' => false]]
// data expansion:
HtmlAttributes::merge(
[data' => ['count' => '1']]
)
===
['data-count' => '1']
// aria expansion:
HtmlAttributes::merge(
['aria' => ['hidden' => true]]
)
===
['aria-hidden' => true]
The return value of HtmlAttributes::merge should also be able to be used as an input for HtmlAttributes::merge as demonstrated here:
https://github.com/twigphp/Twig/pull/4405/files#diff-210291f849102679e6c0a1050d5bcd3076cf55b3cc9677c20fab26dcd15f543cR27-R75
Rendering Attributes
The HtmlAttributes::renderAttributes method that takes the output of HtmlAttributes::merge and creates the attribute string.
This method:
- skips
nullattribute values - filters and implodes
class,styleanddata-controllerattribute values - coerces
aria-*boolean attribute values to'true''false'strings. - json encodes
data-*array values - skips remaining
falsevalues (seearia-*coercion above) - returns attribute name for
trueboolean values - returns attribute name and encoded value for everything else
./Tests/HtmlAttributesTest.php demonstrate the return value of HtmlAttributes::merge and HtmlAttributes::renderAttributes
There was some consideration made to coerce data-* boolean values to string 'true' and 'false' but this was decided against. StimulusJs uses the following conditional to determine if a data-*-value attribute is true or false when internally coercing to a javascript Boolean.
// https://stimulus.hotwired.dev/reference/values#types
!(value == "0" || value == "false")
Illustrated here: https://codepen.io/leevigraham/pen/MWNOyLr
Challenges
The challenge with a html_attributes like function is that the merging strategy is arbitrary.
class and style attributes are usually merged together. Other attributes override the previous values.
Symfony UX twig components recommend Stimulus for interaction. Stimulus uses data-controller attributes for functionality. The data-controller value can be a space delimited list of strings. In this case should the multiple data-controller values be merged or overridden? For StimulusJs also applies to data-target and data-action
Alternative implementation ideas
Merge strategy options
Given the challenges with data-controller above maybe the method should take 2 arguments:
- The attributes to be merged
- An array which defines the merging strategy
HtmlAttributes::merge(
attributes: ['style' => ['background-color' => 'blue', 'color' => 'red']],
options: ['merge' => ['class', 'data-controller']]
)
In the example above the $options argument would be used to determine which values to merge. Other values would replace.
Given the return value of HtmlAttributes::merge can also be used as the $attributes argument of HtmlAttributes::merge the developer could call HtmlAttributes::merge multiple times which would be the equivalent of multiple attribute arrays / argument unpacking.
References in other platforms / frameworks:
Yii2 has a similar function: https://github.com/yiisoft/yii2/blob/master/framework/helpers/BaseHtml.php#L1966-L2046
The renderTagAttributes method has the following rules:
- Attributes whose values are of boolean type will be treated as boolean attributes.
- Attributes whose values are null will not be rendered.
- aria and data attributes get special handling when they are set to an array value. In these cases, the array will be "expanded" and a list of ARIA/data attributes will be rendered. For example,
'aria' => ['role' => 'checkbox', 'value' => 'true']would be rendered asaria-role="checkbox" aria-value="true". - If a nested data value is set to an array, it will be JSON-encoded. For example,
'data' => ['params' => ['id' => 1, 'name' => 'yii']]would be rendered asdata-params='{"id":1,"name":"yii"}'.
CraftCMS uses twig and provides an attr() twig method that implements renderTagAttributes. I've used this helper many times and the rules above are great. Especially the "Attributes whose values are null will not be rendered".
Vuejs v2 -> v3 also went through some changes for false values https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html. This aligns with "Attributes whose values are null will not be rendered." above.
TODO
- [ ] add twig filter function
- [ ] cleanup code
- [ ] write docs
- Attribute rendering added to the class and tests: https://github.com/twigphp/Twig/pull/4405/files#diff-210291f849102679e6c0a1050d5bcd3076cf55b3cc9677c20fab26dcd15f543cR100
- Changed style merging so that the array keys contain the case attribute and value
- Updated the original PR description.
I suppose that the implementation of html_classes would use this new function? https://github.com/twigphp/Twig/blob/3.x/extra/html-extra/HtmlExtension.php#L91-L113
@fabpot Yep it could do.
I still haven't fully considered the twig methods yet. Given the existing static methods the htmlClasses method could look like:
public static function htmlClasses(...$args): string
{
$attributes = HtmlAttributes::merge(['class' => $args]);
return HtmlAttributes::renderAttributes($attributes);
}
I'm also interested in your (and @stof) opinion on merging vs replacing on some attributes. class and style use merging (seems to be standard across other frameworks) but there are use cases for merging data-controller, data-action, some aria-* properties.
I thought using the CVA pattern here might work. ie… add a HtmlAttributes::__construct() method which takes a config argument (or null), and add a apply function which takes the object or attributes and returns the attribute string.
I still haven't fully considered the twig methods yet. Given the existing static methods the
htmlClassesmethod could look like:
no it cannot. html_classes returns only the value of the class attribute. It does not render the attribute name (which is totally different both in term of usage and of auto-escaping)