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

RFC / WIP - html_attributes function #4405

Draft
wants to merge 1 commit into
base: 3.x
Choose a base branch
from

Conversation

leevigraham
Copy link

@leevigraham leevigraham commented Oct 22, 2024

An alternative implementation to #3930

Addresses:

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:

  1. skips null attribute values
  2. filters and implodes class, style and data-controller attribute values
  3. coerces aria-* boolean attribute values to 'true' 'false' strings.
  4. json encodes data-* array values
  5. skips remaining false values (see aria-* coercion above)
  6. returns attribute name for true boolean values
  7. 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:

  1. The attributes to be merged
  2. 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 as aria-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 as data-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

@leevigraham leevigraham changed the title RFC / WIP - html_attributes function DRAFT: RFC / WIP - html_attributes function Oct 22, 2024
@leevigraham leevigraham marked this pull request as draft October 22, 2024 23:40
@leevigraham leevigraham changed the title DRAFT: RFC / WIP - html_attributes function RFC / WIP - html_attributes function Oct 22, 2024
@leevigraham leevigraham marked this pull request as ready for review October 22, 2024 23:40
@leevigraham leevigraham marked this pull request as draft October 22, 2024 23:40
@leevigraham leevigraham changed the title RFC / WIP - html_attributes function RFC / WIP - html_attributes function Oct 23, 2024
@leevigraham leevigraham changed the title RFC / WIP - html_attributes function RFC / WIP - html_attributes function Oct 23, 2024
@leevigraham
Copy link
Author

@fabpot
Copy link
Contributor

fabpot commented Oct 26, 2024

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

@leevigraham
Copy link
Author

@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 mergingdata-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.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants