Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add anattr function to make outputting HTML attributes easier#3930

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
mpdude wants to merge8 commits intotwigphp:3.x
base:3.x
Choose a base branch
Loading
frommpdude:add-attr-function

Conversation

@mpdude
Copy link
Contributor

@mpdudempdude commentedDec 6, 2023
edited by fabpot
Loading

Closes#3907.

First, it adds aattr_merge filter. This filter is intended to be used with arrays that represent HTML attribute name-value pairs. Basically, it works like|merge, but for the special key namesclass,style anddata it performs merging on thevalue level.

This is intended for the use case where you'd like to add to the attributes for an HTML element based on conditions, e. g. multiple subsequent{% if ... %} blocks.

Example:

  • { class: 'foo' }|attr_merge({ class: 'bar' }) will be{ class: ['foo', 'bar'] }
  • { class: 'foo' }|attr_merge({ class: ['bar', 'baz'] }) will be{ class: ['foo', 'bar', 'baz'] }
  • { class: { special: 'foo' } }|attr_merge({ class: ['bar', 'baz'] })|attr_merge({ class: {special: 'qux' } }) will be{ class: { special: 'qux', 0: 'bar', 1: 'baz' } }

or in Twig code:

{%setattr=attr ?? {} %}{%ifcondition1 %}  {%setattr=attr|attr_merge({class:'foo',data: {condition1:'met' }) %}{%endif %}{%ifcondition2 %}  {%setattr =attr|attr_merge({class:'bar',data: {condition2:'met' }) %}{%endif %}

Second, it adds aattr function. This function takes one or multipleattr arrays like above, and print them as a series of HTML attribute markup. All values fromclass will be concatenated with spaces. Key/value pairs fromstyle will be treated as CSS property/value pairs. Fordata, keys will be used to constructdata-{keyname} attributes.

Example:

    {% set id = 'id value' %}    {% set href = 'href value' %}    {% set disabled = true %}    <div {{ attr(        { id, href },        disabled ? { 'aria-disabled': 'true' },        not disabled ? { 'aria-enabled' : true },        { class: ['zero', 'first'] },        { class: 'second' },        true ? { class: 'third' },        { style: { color: 'red' } },        { style: { 'background-color': 'green' } },        { style: { color: 'blue' } },        { 'data-test': 'some value' },        { data: { test: 'other value', bar: 'baz' }},        { 'dangerous=yes foo' : 'xss' },        { style: 'font-weight: bold' },        { style: ['text-decoration: underline'] },    ) }}></div>

will generate HTML markup:

<div href="href%20value" aria-disabled="true" data-test="other value" data-bar="baz" dangerous&#x3D;yes&#x20;foo="xss"></div>

TODO:

  • Get initial feedback
  • Add tests
  • Add documentation
  • Add docblocks

norkunas and leevigraham reacted with thumbs up emojiemnsen reacted with rocket emoji
Copy link
Contributor

@fabpotfabpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I like it a lot. Let's continue the work. Tell me of you need help@mpdude


$result ='';
foreach ($attras$name =>$value) {
$result .=twig_escape_filter($env,$name,'html_attr').'="'.htmlspecialchars($value).'"';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Let's considerfalse as a way to disable the attribute? Andtrue would not generate the value part?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I guess that makes sense. I'd havefalse omit the attribute output altogether. Fortrue, I'd use something likename="name" for backwards compat with (X)HTML.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

⚠️ "true" must be written "true" for some enumerated aria attributes.

Almost everywhere else, indeed,false should remove/hide the attribute.

"" should not happen, i guess, but should be dealt with attention, because the following have all the same meaning:

  • disabled=""
  • disabled
  • disabled="false"
  • disabled="disabled"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

For the aria cases,if you want, there is some classic cases + docs here

https://github.com/symfony/ux/blob/647532ab688f79acfbcb1c895e88a8b2f1a502f6/src/Icons/src/Icon.php#L139-L162

with some test cases if you need toohttps://github.com/symfony/ux/blob/647532ab688f79acfbcb1c895e88a8b2f1a502f6/src/Icons/tests/Unit/IconTest.php#L226

(other similar in the TwigComponent / ComponentAttributes)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Fortunately, Twig and PHP have separate types for strings and booleans. Passing the stringtrue orfalse will not trigger the special logic for boolean values.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@stof I guess what I was trying to say is that yes you could pass'true' or 'false' as parameters but this would require string coercion when setting the value.

The previous comment stated that:

Almost everywhere else, indeed, false should remove/hide the attribute.

I was pointing out thataria-* attributes do accept a string value of'false' and in those cases a coercion of boolfalse to string'false' might be warranted. This could also apply todata-* attributes.

IMO:

  • null value always removes the attribute from the final string rendering.
  • true andfalse values foraria-* attributes should coerce boolean to their string values'true' and'false'
  • true andfalse values for all other attributes should act as boolean html attributes. ie true renders the attribute, false removes the attribute (as per@fabpotcomment).

@mpdude
Copy link
ContributorAuthor

@fabpot Regarding tests: I suppose the fixture-based tests (*.test) are necessary to do integration testing, e. g. including the escaping mechanism? Should I go for such tests or write plain PHPUnit tests instead?

What's the best way to cover lots of scenarios in the fixture-style tests? It's easy to get lost when there are lots of cases but just one--TEMPLATE-- and one--EXPECT-- section.

@fabpot
Copy link
Contributor

@fabpot Regarding tests: I suppose the fixture-based tests (*.test) are necessary to do integration testing, e. g. including the escaping mechanism? Should I go for such tests or write plain PHPUnit tests instead?

You can write both, but.test tests are easier to write and read.

What's the best way to cover lots of scenarios in the fixture-style tests? It's easy to get lost when there are lots of cases but just one--TEMPLATE-- and one--EXPECT-- section.

That's indeed a current limitation.

returnnewCva($base,$variants,$compoundVariants,$defaultVariant);
}

publicstaticfunctionhtmlAttrMerge(...$arrays):array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We can maybe have a@param to better describe$arrays?

return$result;
}

publicstaticfunctionhtmlAttr(Environment$env, ...$args):string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Maybe a@param for$args?

{
$result = [];

foreach ($arraysas$argNumber =>$array) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Not guaranteed that we have a number here.


$value = CoreExtension::toArray($value);

$result[$deepMergeKey] =array_merge($result[$deepMergeKey] ?? [],$value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Can we use[... ] when we can ? As it's slightly more performant than array_merge

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@smnandre Like this?717f872

smnandre reacted with thumbs up emoji
Comment on lines +181 to +185
if (is_numeric($name)) {
$style .=$value.';';
}else {
$style .=$name.':'.$value.';';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm not sure why this specific case for style.. i may have missed the reason earlier in the discussion ?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In casestyle is something like['color: red', 'font-weight: bold'].

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I have two other questions then (sorry)

What if "aria" is something like['aria-disabled', 'aria-current'] ?

Does that mean i can do something like this ?

{{ html_attr(   {style: ['color: red;'] },   {style: {'color':'blue'} },   {style: {'color':'green'} },   {style: ['','color: black;'] },) }}

I'm shared between "i understand each of these usages" and "this could be very un-predictable when/if args are assembled from different layers / templates :|

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes, definetly many edge cases I haven't thought through. Make sure you don't miss the inital idea in#3907, fwiw.

I think we should aim for behaviour similar toarray_merge andnot try to do any kind of parsing/interpretation. For example:

  • html_attr_merge( { style: {'color': 'blue'} }, { style: {'color': 'green'} } } ) }} would be{ style: {'color': 'green'} }
  • html_attr_merge( { style: ['color: blue'] }, { style: { ['color: green']} } } ) }} would be{ style: ['color: blue', 'color: green'] } – if you want "real" overrides, use semantic keys
  • html_attr_merge( { aria: { 'aria-label': 'that' }, {'label': 'this'} } ) would be{ 'aria-label': 'this' } – I'd rewrite keys from thearia anddata sub-array to the "flat" values first and then merge those.

I hope I can come up with reasonable test cases for all those situations so we can discuss them before merge.

@mpdude
Copy link
ContributorAuthor

Added a first load of tests for thehtml_attr_merge function

leevigraham reacted with thumbs up emoji

@leevigraham
Copy link

leevigraham commentedOct 21, 2024
edited
Loading

For reference

Yii2 has a similar function:https://github.com/yiisoft/yii2/blob/master/framework/helpers/BaseHtml.php#L1966-L2046

TherenderTagAttributes method has the following rules:

  • Attributes whose values are of boolean type will be treated asboolean 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 anattr() twig method that implementsrenderTagAttributes. 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 forfalse valueshttps://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html. This aligns with "Attributes whose values are null will not be rendered." above.

}

if (!is_iterable($array)) {
thrownewRuntimeError(sprintf('The "attr_merge" filter only works with mappings or "Traversable", got "%s" for argument %d.',\gettype($array),$argNumber +1));
Copy link

@leevigrahamleevigrahamOct 22, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Should this be " Thehtml_attr_merge filter only…" . What if the exception is thrown when using thehtml_attr function.

@leevigraham
Copy link

leevigraham commentedOct 22, 2024
edited
Loading

How will merging of attributes that support multiple values work? Egaria-labelledby value is anID reference list.

Example:

<div{{attr({'aria-labelledby': 'id1 id2'},        {'aria-labelledby': 'id3'},    ) }}></div>

Should the result be:

  • First value set:{'aria-labelledby': 'id1 id2'}
  • Last value set:{'aria-labelledby': 'id3'}
  • Concatenated (like class):{'aria-labelledby': 'id1 id2 id3'}

This could also apply todata-controller which is used by stimulusjs and Symfony ux components.

@leevigraham
Copy link

@mpdude@fabpot@smnandre I've combined my comments above into a POC here:#4405

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@fabpotfabpotfabpot requested changes

+4 more reviewers

@leevigrahamleevigrahamleevigraham left review comments

@jdreesenjdreesenjdreesen left review comments

@stofstofstof left review comments

@smnandresmnandresmnandre left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Helper function to make dealing with HTML attributes easier

6 participants

@mpdude@fabpot@leevigraham@jdreesen@stof@smnandre

[8]ページ先頭

©2009-2025 Movatter.jp