Skip to content

Conversation

@NickSdot
Copy link
Contributor

@NickSdot NickSdot commented Oct 1, 2025

Edit: Based on the discusstion here, I released an advanced version as a package for the time being:
https://github.com/NickSdot/blade-html-attributes


This PR adds a new @maybe directive that conditionally renders HTML attributes with values, complementing the existing boolean attribute directives like @checked, @selected, @disabled, @required, and @readonly.

Problem

While we have directives for boolean attributes, we still need verbose @if ... @endif blocks for attributes with values:

<a href="#" @if($title) title="{{ $title }}" @endif>Link</a>

We cannot keep adding specific directives for every possible attribute, so we need a dynamic solution.

Solution

The @maybe directive renders an attribute with a value only when the value is not null, not an empty string, and not whitespace-only:

<a href="#" @maybe('title', $title)>Link</a>

Before/After

{{-- before --}}
<a href="{{ $link->route }}" @if($link->title) title="{{ $link->title }} @endif" @if($link->rel) rel="{{ $link->rel }} @endif">
    {{ $link->label }}
</a>


{{-- after --}}
<a href="{{ $link->route }}" @maybe('title', $link->title) @maybe('rel', $link->rel)>
    {{ $link->label }}
</a>

{{-- before --}}
<img src="{{ $image->url }}" @if($image->alt) alt="{{ $image->alt }}" @endif @if($image->caption) data-caption="{{ $image->caption }}" @endif />

{{-- after --}}
<img src="{{ $image->url }}" @maybe('alt', $image->alt) @maybe('data-caption', $image->caption) />

Behaviour Matrix

The directive intentionally differs from a simple @if() check by treating 0 and false as valid values, since these are common in data attributes for counts, flags, and boolean states.

Value Renders
'foo' data-attribute="foo"
0 data-attribute="0"
false data-attribute="false"
true data-attribute="true"
'' (nothing)
null (nothing)
' ' (nothing)

Naming

I considered several alternatives: @when (too generic, likely better for other future use cases), @flag (implies boolean values only, whereas this handles strings, numbers, bools), @attribute and @optional (too long), @attr and @set (don’t make the conditional nature clear).

@has was tempting as it reads well: “has $title, then render title”. However, the parameter order would need reversing to @has($title, 'title'), which breaks the pattern of other Blade directives where the static value comes first.

@opt is appealingly terse but perhaps too cryptic.

@maybe has the right balance. It’s short, clearly conditional, and reads naturally with the attribute name first: “maybe render title if $title”.

@shaedrich
Copy link
Contributor

To me, the naming is not intuitive. I would call it @flag() or the like

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 1, 2025

To me, the naming is not intuitive. I would call it @flag() or the like

@flag would work well if this was exclusively for boolean data attributes, but since it handles any attribute with any value type, @maybe, @when or @optional and obviously @attribute (both too long, IMO) are more accurate. A title="" or target="" attribute aren't flags, though. Also flag doesn't really make it clear that it is conditional.

I appreciate the feedback, but I'll leave the naming to Taylor.

@shaedrich
Copy link
Contributor

It'd be fine with @optional

@faissaloux
Copy link
Contributor

Good one! One problem is the naming, I think @attribute would be better.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 1, 2025

I updated the naming section of the PR description.

@shaedrich
Copy link
Contributor

Thanks a lot 👍🏻

@imacrayon
Copy link
Contributor

I think this should mirror @class, there’s precedent with that directive:

<a href="{{ $link->route }}" @attr(['title' => $link->title, 'rel' => $link->rel])>
    {{ $link->label }}
</a>

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 2, 2025

I think this should mirror @class, there’s precedent with that directive:


<a href="{{ $link->route }}" @attr(['title' => $link->title, 'rel' => $link->rel])>

    {{ $link->label }}

</a>

It is different in the sense that the @class directive always has at least two entries.

For this use case here it makes it longer by 6 chars for the majority of situations where we have one data attribute. Don't really like it because the reason for the PR is to make things less verbose.

That said, I'll have a look if we can support both.

Edit:
@imacrayon it's easy to support both, passing as initially proposed here and as array. I think it's important to keep the initally proposed syntax too, because it's shorter for the majority case. As the below example shows the array syntax only really is adding it's marginal value >1 entries.

Happy to implement. For now I'll leave it to Taylor to decide first.

{{-- Attributes: 1 --}}
<a @if($link->title) title="{{ $link->title }} @endif">
<a @maybe('title', $link->title)>
<a @maybe(['title' => $link->title])>

{{-- Attributes: 2 --}}
<a @if($link->title) title="{{ $link->title }} @endif" @if($link->rel) rel="{{ $link->rel }} @endif>
<a @maybe('title', $link->title) @maybe('rel', $link->rel)>
<a @maybe(['title' => $link->title, 'rel' => $link->rel])>

{{-- Attributes: 3 --}}
<a @if($link->title) title="{{ $link->title }} @endif" @if($link->rel) rel="{{ $link->rel }} @endif  @if($link->clickId) data-tracker="{{ $link->clickId }} @endif">
<a @maybe('title', $link->title) @maybe('rel', $link->rel) @maybe('data-tracker', $link->clickId)>
<a @maybe(['title' => $link->title, 'rel' => $link->rel, 'data-tracker' => $link->clickId])>

@shaedrich
Copy link
Contributor

fyi, previous unsuccessful attempt at @attributes: #52783

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 2, 2025

fyi, previous unsuccessful attempt at @attributes: #52783

Similar. Mine is minus the complexity. Though, apparently high demand for a solution.

@hctorres02
Copy link

I tried using $attributes->merge([ ... ]), but there's no way to apply a condition to data-* attributes. The filter must be done elsewhere.

This directive is quite valid. It could be expanded to include conditions like the @class directive, but implementation requires care.

@timacdonald
Copy link
Member

I haven't dug deep into how this renders attributes, but I would expect the following to happen for these different attribute types.

[
    'crossorigin',                            // crossorigin
    'data-persistent-across-pages' => 'YES',  // data-persistent-across-pages="YES"
    'remove-me' => false,                     // [removed]
    'keep-me' => true,                        // keep-me
    'null' => null,                           // [removed]
    'empty-string' => '',                     // empty-string=""
    'spaced-string' => '   ',                 // empty-string="   "
    'zero' => 0,                              // zero="0"
    'one' => 1,                               // zero="1"
];
<div
    crossorigin
    data-persistent-across-pages="YES"
    keep-me
    empty-string=""
    spaced-string="   "
    zero="0"
    one="1"
/>

This will keep it inline with how Vite handles attribute types and values.

@timacdonald
Copy link
Member

See arbitrary attributes in #43442 for more on these decisions.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 7, 2025

I haven't dug deep into how this renders attributes, but I would expect the following to happen for these different attribute types.

[

    'crossorigin',                            // crossorigin

    'data-persistent-across-pages' => 'YES',  // data-persistent-across-pages="YES"

    'remove-me' => false,                     // [removed]

    'keep-me' => true,                        // keep-me

    'null' => null,                           // [removed]

    'empty-string' => '',                     // empty-string=""

    'spaced-string' => '   ',                 // empty-string="   "

    'zero' => 0,                              // zero="0"

    'one' => 1,                               // zero="1"

];
<div

    crossorigin

    data-persistent-across-pages="YES"

    keep-me

    empty-string=""

    spaced-string="   "

    zero="0"

    one="1"

/>

This will keep it inline with how Vite handles attribute types and values.

Hey Tim! I already read that in your comment to the PR linked above. But I kindly disagree. Personally I don't care what Vite does, what I care about is how I can add my own attributes in a non-verbose way.

This PR seeks to handle the majority case we deal with every single day, not to be "aligned" with the @class directive, nor be "unified" with Vite. Because it doesn't make sense for many situations.

Differences:

  • It intentionally will render false, and true as strings.
  • It will not render space only strings.
  • It doesn't have value-less attributes at all, because it's shorter to simply add it in your HTML. There is no value in having a always renderable attribute in the array even.
  • It doesn't support array syntax at all; I showed above how array syntax does not really add value compared to multiple @maybe.

As I mentioned in my PR description, I would name this @maybe to keep @attributes for future use cases (perhaps like yours).

Both concepts are valid, but they cannot be merged in one (one wants to render false, one doesn't). Hence, what you are asking for is unfortunately nothing for this PR. 🙏

@timacdonald
Copy link
Member

Appreciate you pushing back, @NickSdot! Always appreciated.

To clarify, when I say Vite, I mean what we do in Laravel itself. Having different attribute rendering mechanics for two Laravel features seems like a footgun.

But even taking the stand that we don't want to be inline with Laravel's attribute handling in the Vite space, I would still push back on the current rendering proposal. If nothing else, we should respect HTML itself. It explicitly mentions that true and false are not valid for boolean attributes.

Screenshot 2025-10-07 at 14 07 00

@timacdonald
Copy link
Member

timacdonald commented Oct 7, 2025

I think my brain goes to tooling like jQuery when I see that we don't render empty strings and whitespace only strings. I probably need to think on that some more to come up with a compelling argument as my brain is deep in other stuff right now.

None of this is a hill I wanna die on, btw. Just sharing a perspective and prior art in Laravel related to the feature to ensure we keep the framework cohesive when and where it makes sense.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 7, 2025

Having different attribute rendering mechanics for two Laravel features seems like a footgun.

@timacdonald well, as I mentioned above, there is no way to satisfy two completely contrary concepts in one solution. Below I make a point why both are valid (+ spec conform) and why we need two mechanisms.

But even taking the stand that we don't want to be inline with Laravel's attribute handling in the Vite space, I would still push back on the current rendering proposal. If nothing else, we should respect HTML itself. It explicitly mentions that true and false are not valid for boolean attributes.

Screenshot 2025-10-07 at 14 07 00

I knew that this will be the next argument. :) For the follwing reasons I need to push back again:

1) There are more than one relevant spec here.

The ARIA spec for instance. Accessibility APIs expect explicit tokens. E.g:

aria-hidden="false"
aria-expanded="false"

2) Only valid for presence indicators (boolean attributes)

What you are quoting is a separate concept with explicit behaviour. This, however, does not mean that I cannot use true/false in enumerated attributes (next section after the one on your screenshot: 2.3.3 Keywords and enumerated attributes). True/False are not forbidden values for enumerated attributes. We can do whatever we want, just cannot expect "boolean attributes" behaviour from it.

To bring a simple example; you will agree that the folowing is neater

foo.active = foo.active === 'true' ? 'false' : 'true';

than

if (foo.hasAttribute('data-active')) {
  foo.removeAttribute('data-active');
} else {
  foo.setAttribute('data-active', '');
}

Enumerated attributes allow us excactly that. And here we are also back to the previous point: ARIA attributes are enumerated attributes, not boolean ones. Both ways are HTML spec conform, even if we ignore that ARIA is a separate spec.

3) Third Party Expectations

I am all for following specs. And I also have proven above that we are aligned with the spec. However, I still would like to throw in third party stuff. If a third party expects explicit true/false I cannot change that. You mentioned jQuery, JQuery Mobile expects it. The same is true for Bootstrap in many cases.

I think my brain goes to tooling like jQuery when I see that we don't render empty strings and whitespace only strings.

Yes, again, I don't object. Both concepts have their place. That's why we need two solutions for it. I believe we shouldn't try too hard to unify something that cannot be unified. It's like in the real world, we have a Slotted screwdriver and a Phillips screwdriver. Both aren't footguns, but solutions for different problems.

4) Last but not least
We are pretty focused on custom attributes right now. But please don't forget, I should totally be able to decide on my own if I want to have title="false" to show "false" in a little tooltip in a, for instance, in classifier interface. And I should be able to hide a title tooltip on something if, for whatever reason, the value in title is .

You get the point: this is not only for custom data attributes.


I probably need to think on that some more to come up with a compelling argument as my brain is deep in other stuff right now.

None of this is a hill I wanna die on, btw. Just sharing a perspective and prior art in Laravel related to the feature to ensure we keep the framework cohesive when and where it makes sense.

Unfortunately, it (subjectively) feels like Taylor tends to close PRs when you hop in with objections. So if your objections are not fully thougt out... it's demotivating to be closed just because. No offense, of course! ❤️

I hope my arguments above are compelling enough to "book a win" here.

@timacdonald
Copy link
Member

timacdonald commented Oct 7, 2025

Unfortunately, it (subjectively) feels like Taylor tends to close PRs when you hop in with objections. So if your objections are not fully thougt out... it's demotivating to be closed just because. No offense, of course! ❤️

No offense taken. It is my job to offer opinions here and there. Sorry if I've put you off here or on other PRs.

I can see utility in a feature of this shape. FWIW, I hope some form of this gets merged.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 7, 2025

Sorry if I've put you off here or on other PRs.

No, it's also not like that. Wasn't put off myself. So far you luckily been supportive to all my PRs. Sorry if I didn't express myself clear enough. 🙏

@timacdonald
Copy link
Member

Awesome!

@willrowe
Copy link
Contributor

willrowe commented Oct 8, 2025

I fully agree with @timacdonald and would rather see a single @attributes directive that consistently handles this in a way that aligns with how you would expect it to when coming from the JavaScript/Vue/Vite side of things. Having too many ways to do essentially the same thing, but in slightly different ways makes it more difficult to learn. This feels too tailored to how one person may like to do things as opposed to something more general, powerful, and predictable.

@rodrigopedra
Copy link
Contributor

Adding to @willrowe's comment, the true and false as strings case can be easily handled by a user on their codebase:

@attrs([
    'aria-hidden' => $hidden ? 'true' : 'false',
    'aria-expanded' => $expanded ? 'true' : 'false',
])

One can easily add a helper to their code base if wanted. Or just use json_encode:

@attr([
    // json_encode will output booleans, numbers and null as a unquoted strings
    'aria-hidden' => json_encode($hidden), 
    'aria-expanded' => json_encode($expanded),
])

But attribute toggling, as @timacdonald described, would be very awkward to accomplish with the current proposal.

Also, calling the directive @maybe is a nay from me. Intent is unclear and confusing.

Mind that Blade's directives can also be used for non-HTML content, like markdown emails and Envoy tasks.

I'd prefer, if added, for it to be called something like @attr(), or anything that closely resembles its intent.

If different behavior due to aria- and data- attributes is desirable, why can't we have both?

Insert "Why not both?" meme here

We could add a @attr directive that behaves like @timacdonald described and like our Vite plugin already does, and a @aria or @dataset directive that behaves like this PR is proposing.

A @aria or @dataset directive could even auto-prefix its attributes and behave however it is needed for those cases.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 8, 2025

JavaScript/Vue

This is Blade. Just saying.


Y'all keep discussing things that this PR doesn't seek to solve. As we now already know there must be two different solutions because both concepts are diametrical to each other. This isn't "tailored" to one persons requirements, this is the majority use case. We all set title and rel attributes all the time.

The alternative examples proposed above are hilarious. You realise that they are longer than writing the actual control flows this PR attempts to get rid off?

About naming, I repeat, I leave that to Taylor.

Guys, keep on bike shedding unrelated stuff instead of working on a complementing PR to add the other missing piece. I am sure that's how we will get good things! ✌️❤️

Edit:
Imagine having json_encode in your Blade files.

Edit 2:

If different behavior due to aria- and data- attributes is desirable, why can't we have both?

And then a @title, @rel, @target etc. directives, right?

@rodrigopedra
Copy link
Contributor

And then a @title, @rel, @target etc. directives, right?

Of course not.

Those would be covered in the behavior everyone would expect a @attr directive to behave.

With sane rendering rules that follow the HTML spec, minus aria- or data- attributes, which were later additions.

Imagine having json_encode in your Blade files.

Sure, mate. It is such an odd case it got a custom directive, a wrapper class, and a section on docs.

https://laravel.com/docs/12.x/blade#rendering-json

This isn't "tailored" to one persons requirements, this is the majority use case. We all set title and rel attributes all the time.

Yes, it is. Or at least when one prioritizes aria- and data- attributes rules over all other HTML attributes.

The gripe is not on the value of the directive, as I, and I am sure others who commented out, like the proposed shorthand syntax in general.

The gripe is on the proposed esoteric rendering rules.

This argument doesn't make any sense on the title or rel attributes, as they would be fine if the rendering rules followed the HTML spec, as proposed by many commenters, and what we also already have for our Vite plugin.

But whatever, you do you.

Good luck with your PR. I like the idea, just not the oddities, such as treating booleans as strings (which is perplexing).

If not merged, consider publishing it as a package. I am sure many other developers would benefit from it for aria- and data- attributes.

Have a nice day =)

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 8, 2025

I answered the HTML spec question above in detail. This is spec conform. Read up enumeration attributes instead of ignoring it and liking comments which also got it wrong.

Enumeration example from ARIA:
true; false; undefined

Enumeration example from app:
true; false; unsure

These have nothing to do with boolean attributes.

Unfortunately, I cannot do more to help you to understand the difference.

I am sure many other developers would benefit from it for aria- and data- attributes.

Thanks for making the argument for getting this merged. And yet you don't want to see it merged. Because it isn't tailored to your use case? ;)

I wish you the same!

@rodrigopedra
Copy link
Contributor

rodrigopedra commented Oct 8, 2025

And yet you don't want to see it merged.

I do want to see it merged. Just not with such esoteric rendering rules.

I find the syntax great:

<a href="..." @attr('title', $title)>{{ $label }}</a>

Where the title attribute is not rendered at all if $title is null or empty or false, and thus does not overshadow the <a> tag's content for a screen reader with an empty string.

The proposed syntax is very handy. Just not your particular use case for rendering attribute values in such a manner no one would expect.

Because it isn't tailored to your use case? ;)

Not my use case. HTML attributes spec.

Imagine someone using Web Components opening issue after issue as they expect boolean attributes to behave conforming to the spec.

Enumeration attributes are another spec suitable to specific use cases. It is not the general use case. And as such, IMO, it could be subject to a future addition, as I believe the general use case would benefit more developers. Or even provided by a 3rd-party package.

I am sorry. I won't spend more of my time trying to help you bring this addition to the framework.

I wish you the best luck.

@Rizky92
Copy link
Contributor

Rizky92 commented Oct 17, 2025

HTML spec is too broad.
I think this needs to split, for attributes like disabled, checked, and the like, should be its own type of attribute.

While others can be functionally like @maybe('data-columntype', $isNumeric, 'integer', 'string').

@NickSdot
Copy link
Contributor Author

HTML spec is too broad. I think this needs to split, for attributes like disabled, checked, and the like, should be its own type of attribute.

Agree on this.

While others can be functionally like @maybe('data-columntype', $isNumeric, 'integer', 'string').

This defeats the goal of the PR because it would too long. Multiple directives solve that.

@browner12
Copy link
Contributor

It's not about what either 4 or 15 people want. It's about what's best and most maintainable for the framework as a whole. I'm making my argument for what I think is best, and you make your argument for what you think is best.

Then the BDFL comes in, passes judgement, and we all move on with our lives.

Of course 2 directives could exist. Or 3, or 4, or 10. But that adds a maintenance cost. If we choose to pay it, great. I've made my case for a single directive I believe best covers the use cases.

@shaedrich
Copy link
Contributor

But let me add one more proposal. How about the following?

@attr('foo', true) -> foo="true"
@attr('!foo', true) -> foo

@attr('foo', false) -> foo="false"
@attr('!foo', false) -> nothing

Now, that looks smart 😲 Very intriguing solution 🤔

I'm usually not in favor of multiple information within one string, but on one hand, it's pretty common in the framework already, and on the other, it's both quite short and addresses some problems from this discussion fairly well 👍🏻

@riyuk
Copy link
Contributor

riyuk commented Oct 17, 2025

Now, that looks smart 😲 Very intriguing solution 🤔

About the ! syntax: (as we all know) Tailwind uses ! for important-styles too. Though I'd flip it around - make @attr('foo', true) -> foo the default, and @attr('!foo', true) -> foo="true" for when you explicitly want the verbose output. Makes more sense to me since the ! would mean "verbose mode" instead of trying to follow spec behavior.

Actually strongly disagree at least as far as the current discussion goes. There are a tiny amount of people involved - and yet we have substantial disagreement and confusion about how this should even work.

I disagree with this take. From what I can see, the last PR that generated this level of discussion was at least 6 years ago. This PR is already in the top 10 most-commented PRs in the entire framework. That tells me the feature itself is valuable and people actually care about it.

@shaedrich
Copy link
Contributor

Now, that looks smart 😲 Very intriguing solution 🤔

About the ! syntax: (as we all know) Tailwind uses ! for important-styles too. Though I'd flip it around - make @attr('foo', true) -> foo the default, and @attr('!foo', true) -> foo="true" for when you explicitly want the verbose output. Makes more sense to me since the ! would mean "verbose mode" instead of trying to follow spec behavior.

I'd be totally down to that—good point 👍🏻

Actually strongly disagree at least as far as the current discussion goes. There are a tiny amount of people involved - and yet we have substantial disagreement and confusion about how this should even work.

I disagree with this take. From what I can see, the last PR that generated this level of discussion was at least 6 years ago. This PR is already in the top 10 most-commented PRs in the entire framework. That tells me the feature itself is valuable and people actually care about it.

And it's a discussion needed to make a PR result in well-implemented features 👍🏻

@NickSdot
Copy link
Contributor Author

@riyuk @shaedrich

Personally, I still believe the boolean case as the opposition here wants it is the minority; we use it less. I'd keep it as is and don't invert. However, I'd not die from it being inverted. If it would be decided to invert, I'd to say this would be more nice:

@attr('foo=', true) -> foo="true"
@attr('foo', true) -> foo

@attr('foo=', false) -> foo="false"
@attr('foo', false) -> nothing

Basically it says, if the = is passed, appending ='val' is forced.

@shaedrich
Copy link
Contributor

Makes sense 👍🏻

@AhmedAlaa4611
Copy link
Contributor

<x-card @if($condition) disabled @endif></x-card>

Since when are we mixing x-component syntax with @-syntax?

Since we start complicating things. 😂😂😂

@NickSdot
Copy link
Contributor Author

Released this as a package for the time being:
https://github.com/NickSdot/blade-html-attributes

The special ! and = operators might feel a bit akward at first, but I would like to remind that something similar was merged here lately.

@taylorotwell, I still believe such common functionality should be part of core.

But, one concern is I don't see a great path forward to solving all use cases. For example if we were to call this @attr and then have an @attributes that behaves slightly differently that feels confusing / annoying to me. 😕

The package takes all feedback from this discussion into account. Potentially, this is the "great path forward" you've been looking for. If that's the case I am keen to update the PR. Please lmk. Otherwise, just close -- no hard feelings. 🙏

@rodrigopedra
Copy link
Contributor

The special ! and = operators might feel a bit akward at first, but I would like to remind that something similar was merged here lately.

That PR was reverted here: #57151

The actual boolean arttibutes are the subset; so subset that they have their very own spec.

From this criterion, we can exclude the following classes of attributes from the discussion, as they would be "so subset" for having their own spec.

  1. Boolean attributes, as said by you, have their own section on spec:
  1. Enumerated attributes, as they also have their own section on spec:
  1. Custom data-* attributes, as they also have their own section on spec:
  1. WAI-ARIA attributes, more than their own section on spec, they have a full spec of their own:

With such a narrowed-down set of attributes, what is really left?

Regular HTML attributes?

I guess everyone agrees that the first example on the first post is one of the common goals:

Going from this:

<a href="#" @if($title) title="{{ $title }}" @endif>Link</a>

To this:

<a href="#" @attr('title', $title)>Link</a>

And I guess everyone would agree this outcome is undesirable:

<a href="#" title="false">Link</a>

How to prevent this from happening if the newly introduced directive gets merged?

In some part the discussion derailed from spelling out boolean values (e.g., spelling true and false as strings) to imagining this would only apply to boolean attributes.

Now that we agree boolean attributes are, in your own words, "are the subset; so subset that they have their very own spec.", along, by the same criteria, with enumerated attributes, custom data-* attributes, and WAI-ARIA attributes, we should look at the behavior of other attributes.

Such as the case of the title attribute outlined above.

Do we want booleans spelled out for these attributes that are not on a subset to have their very own spec?

Do we want rel, name, id, type, lang, and other attributes to be spelled out as attribute="false" when their dynamic variable value is falsy?

Or do we want these attributes to be hidden when they are provided with a dynamic variable with a falsy value?

Another thing that made me wonder is how common is the case to spell out boolean values, that they would be treated as first-class citizens?

Do we have any stats on how often we need to reach for these attributes?

Even if boolean attributes are to be considered a subset for having their own section on spec, what is more common: to toggle boolean attributes dynamically or to spell out boolean values on arbitrary attributes?

And we didn't touch on the issue of 3rd-party integration, such as

<div @attr('wire:poll', $shouldPoll)>...</div>

On the other hand, there is a very common case where we would not want booleans spelled out. When using the value attribute on radio buttons and checkboxes.

For example:

<input type="radio" @attr('value', $value)>

or

<input type="checkbox" @attr('value', $value)>

We wouldn't want them to be spelled out, as the Illuminate\Validation\Concerns\ValidatesAttributes@validateBoolean() method doesn't consider the strings "true" and "false" as valid boolean values.

From a maintenance perspective, imagine a user using the newly introduced @attr() directive to render a boolean value. Later it fails validation, and they open an issue. And another, and another.

To make it work as expected, a user would need to do something like this:

<input type="checkbox" @attr('value', $value ? 1 : 0)>

And that is also precisely what I think one should do if they need to spell out boolean attributes.

Of course, if we knew how common it is to need to spell out boolean values when using regular HTML attributes (not boolean attributes, not enumerated, not data-* and not aria), we could weigh in on the pros and cons.

But we don't know. At least no one provided a clue on this need. If anyone has an estimate, please share so we can enrich the discussion with actual data.

In summary:

  • We don't want to render attribute="false" for regular HTML attributes (non-subset ones that have a spec).
  • Spelled-out boolean values don't work well with the value attribute used in forms.
  • We have no clue how common it is to dynamically render spelled-out boolean values on regular HTML attributes (non-subset ones that have a spec) compared to the need to integrate with 3rd-party code that doesn't expect such values.
  • We care about maintainability.
  • It would be nice if different parts of the framework behaved similarly, such as what is already done with Vite.

Based on this, I still think spelling out boolean values by default is not the best option for any class of attributes, being a subset or not.

I also agree that usage on subset cases that need their very own spec, such as boolean attributes, enumerated attributes, custom data-* attributes, and WAI-ARIA attributes, should not be considered for the decision if spelling out boolean values should be default behavior.

@NickSdot
Copy link
Contributor Author

And I guess everyone would agree this outcome is undesirable

Bro is so focused on being against something that he didn't even realise that this isn't even the case anymore. I agree; changed my mind on it. It's fixed, sir.

(didn't read further after this)

@rodrigopedra, big fan of your contributions, code- and opinions-wise, here. I believe you are smart and that you add value. Now, however, I believe you maybe need to have a quick reset. 🤞🙏✌️

@NickSdot
Copy link
Contributor Author

That PR was reverted here: #57151

I know. Does that change the general acceptance of the syntax? No.

@timacdonald
Copy link
Member

timacdonald commented Oct 21, 2025

Hey folks, I have not been keeping up with the thread here, sorry. I very much appreciate the pushing, pulling, and challenging things. I can feel how much we all want thing in Laravel to be the best they can, even when we don't all see things the same way.

I was asked my take on the new proposal. I see that this new proposal with the key= approach might satisfy everything we have been chasing but something feels off about it to me. Just feels like a lot of overhead to work out what you want every time you are using the directive.

After taking all this in and seeing the back and forth, I've personally changed my mind on how I think this should work.

I know this is Blade but I think we should take inspiration from what React / Vue are doing. I reckon someone like Evan You, creator of Vue, has thought much more deeply and for a far longer time about these things than anyone in this thread and has likely thought of a lot of tradeoffs as well.

I'm sorry for the flip in opinions here, but strong opinions loosely held.

My original proposal

I compared my original proposal to some common frameworks in the Laravel space:

Screenshot 2025-10-21 at 10 25 20

My new proposal

I now feel our defaults should better match Vue and React which also start to align better with Nick's original proposal. Still has some deviations.

Screenshot 2025-10-21 at 10 29 18

What about booleans?

I think we should also take inspiration from Vue, React, etc. We maintain an internal list similar to how Vue handles things. If we encounter any of these attributes, we render the attribute as a HTML boolean attribute.

This list is based on the current specification.

  • allowfullscreen
  • alpha
  • async
  • autofocus
  • autoplay
  • checked
  • controls
  • default
  • defer
  • disabled
  • formnovalidate
  • hidden*
  • inert
  • ismap
  • itemscope
  • loop
  • multiple
  • muted
  • nomodule
  • novalidate
  • open
  • playsinline
  • readonly
  • required
  • reversed
  • selected
  • shadowrootclonable
  • shadowrootcustomelementregistry
  • shadowrootdelegatesfocus
  • shadowrootserializableamless

Those with a keen eye will see this does not match exactly what Vue has. I have removed a few attributes that are no longer part of the spec, including:

  • scoped
  • seamless

It also has some additional attributes that Vue has not yet included.

Unfortunately, rendering these boolean attributes is not consistent across frameworks. Here are how some popular libraries render boolean attributes:

Screenshot 2025-10-21 at 11 12 44

My feelings here are that we should do what Vue does. It feels closest from a PHP variable -> HTML boolean conversion.

I'm not really sure what Alpine is doing with those last couple; See this comment on MDN:

Screenshot 2025-10-21 at 11 18 32

What about non-standard attributes like wire:poll?

With my proposed solution, wire:poll is broken. I propose we offer a way for 3rd party libraries, such as Livewire, to hook in and modify which attributes are considered boolean.

Blade::compileAttribute('wire:poll', function ($value) {
    if (is_string($value)) {
        return 'wire:poll="'.$value.'"';
    }

    return 'wire:poll':
});

It may make most sense to use a regex pattern instead, as something like wire:poll has other variations, e.g., wire:poll.15s="foo"

Blade::compileAttribute('/^wire:poll/', function ($value, $key) {
    if (is_string($value)) {
        return $key.'="'.$value.'"';
    }

    return $key:
});

This leads up back to the original list of boolean attributes. You will notice the * on hidden. hidden was a boolean attribute but now has other values associated with it. That means we would use this API internally as well:

Blade::compileAttribute('/^hidden$/', function ($value) {
    if ($value === 'until-found') {
        return 'hidden="until-found"';
    }

    if ($value || $value === '') {
        return 'hidden=""';
    }

    return '';
});

See the PR to Vue to support this new hidden attribute overloading: vuejs/core#13125

If some app wanted special handling of certain attributes, they could also bind their own logic and then just use the directive in their templates.

I prefer all this because now I don't need to think about foo or foo= in the template, only in the service provider.

If I want special handling of all booleans, I can do that myself as needed.

Blade::compileAttribute('/.*/', function ($value, $key) {
    // ...
});

With all that in place, I would be tempted to then make the Vite handling align with this, as it correctly handles the HTML boolean attributes.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 21, 2025

@timacdonald thanks for the feedback!

One issue I see with not having the special syntax is that we would have no way to differentiate in situations where we want to not rendering instead of data-foo="false". Or no rendering instead of data-foo=' '. Both ways were requested by people here. If I am not mistaken we couldn't satistfy both with your proposed solution?

Just feels like a lot of overhead to work out what you want every time you are using the directive.

I'd argue that having Blade::compileAttribute() adds significanyly more overhead. 😅 Every package and every app could behave different for the same attributes; each time we get our hands on something we would first need to learn how things work there. There is also a risk of conflicts. I see that you want to keep things flexible; but is this really the right place for allowing flexibility? I am not convinced that this is better than adding a = or ! here and there. What do you think?

Did you see the behaviour matrix here and what directives I added? It allows for everything.

Value @attr @data @aria @flag
('foo', "bar") foo="bar" data-foo="bar" aria-foo="bar" foo
('foo', "1") foo="1" data-foo="1" aria-foo="1" foo
('foo', 1) foo="1" data-foo="1" aria-foo="1" foo
('foo', true) foo data-foo aria-foo="true" foo
('foo=', true) foo="true" data-foo="true" aria-foo="true" foo
('foo', false) (nothing) (nothing) aria-foo="false" (nothing)
('foo=', false) foo="false" data-foo="false" aria-foo="false" (nothing)
('!foo', false) (throws) (throws) (nothing) (throws)
('foo', "0") foo="0" data-foo="0" aria-foo="0" (nothing)
('foo', 0) foo="0" data-foo="0" aria-foo="0" (nothing)
('foo', '') (nothing) (nothing) (nothing) (nothing)
('foo=', '') foo="" data-foo="" (nothing) (nothing)
('foo', ' ') (nothing) (nothing) (nothing) (nothing)
('foo=', ' ') foo=" " data-foo=" " (nothing) (nothing)
('foo', null) (nothing) (nothing) (nothing) (nothing)

The behaviour surely could be adapted to what you propose here (which basically would be inverting the special syntax cases, I believe?), but should we really drop the special syntax in favour of having this pretty complex Blade::compileAttribute() concept? I'd love to think of attribute directives as something very simple; and standardised. What you propose feels heavy to me.

@timacdonald
Copy link
Member

timacdonald commented Oct 21, 2025

Personally, I'd like to push for a single way to do the thing that just works.

I don't love that the overhead exists while authoring the template, choosing between directives or !foo / foo=. I like my proposal (Taylor and others may disagree) as it means I can author without care for different types of attributes. One directive that just works as intended. It also opens the door to:

@attributes([
    'foo' => $v,
    'data-foo' => $v,
    'aria-foo' => $v,
    'wire:poll' => $v,
])

I'd argue that having Blade::compileAttribute() adds significantly more overhead

For this PR? Yes. For consuming applications? No. I wouldn't even imagine that we would document this hook.

I see Blade::compileAttribute as a low level package level hook. Not something that 99.9% of apps OR packages would need. There is probably a handful of packages that would register hooks across the entire ecosystem.

I love that the framework hides complexity in itself, 3rd party packages, or application service providers, to then surfaces a simple API to the main consuming user.

That is just my perspective, though, and I totally see there is no clear answer here and a lot of the suggestions here for API and values are valid.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 21, 2025

@timacdonald Ironically, you changed your mind to what I proposed initally and I changed mine to what you proposed initally. 🫠 Please note, inverting the behaviour would mean that this:

<a href="#" @attr('title', $title)>Link</a>

renders as

<a href="#" title="false">Link</a>

Rodrigo is IMO correct here, this is a rather undesirable outcome (at least in the majority of cases). Initially I was thinking that it is better, but I eventually changed my mind on it.

While I don't want to make this to 1:1 work like Blade component attributes, I think it is woth noting that

<x-phone :data-something="false">

Does not render the attribute.

<span>

While

<x-phone :data-something="true">

renders as

<span data-something="data-something">

Wouldn't it be confusing if @attr would do the exact opposite by default?

One directive that just works as intended.

I agree that one directive would be nice. But I really had to give up on it. There is no single "intended", unfortunately. Aria works slightly different than others, a dedicated directive can account for that.

Also, having @data and @aria allows for shorter code which I fancy.

<button @attr('aria-label', $label) @attr('aria-hidden', $hidden)></button>
<button @aria('label', $label) @aria('hidden', $hidden)></button>

The @flag attribute is IMO kinda useless; but since others wanted it it's there. I'd be fine to not have it. Personally, I believe having the others adds value.

I see Blade::compileAttribute as a low level package level hook. Not something that 99.9% of apps OR packages would need. There is probably a handful of packages that would register hooks across the entire ecosystem.

Yeah, that's probably right.


Alright, so the open questions are now:

  1. Do we want multiple directives or not
  2. Should the behaviour be inverted or not
  3. Keep the modifiers or not

@NickSdot
Copy link
Contributor Author

Here in comparison with Blade component behaviour.

Value @attr Blade Component
('foo', "bar") foo="bar" foo="bar"
('foo', "1") foo="1" foo="1"
('foo', 1) foo="1" foo="1"
('foo', true) foo foo="foo"
('foo', false) (nothing) (nothing)
('foo', "0") foo="0" foo="0"
('foo', 0) foo="0" foo="0"
('foo', '') (nothing) foo
('foo', ' ') (nothing) foo
('foo', null) (nothing) (nothing)

Since foo="foo" and foo are the same, the only difference (in default, non-modifier behaviour) are empty strings and whitespace-only strings. Personally, I don't like that these render the attribute in Blade components. Though, one could argue that the behaviour should be fully aligned.

So again, not sure if we really should do the opposite approach as you propose.

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 21, 2025

So, after tackling empty/whitespace strings (#57467) for a few hours, I am back to booleans now. I understand that Tim likes the Vue way better. I am also a big Vue fan and believe Evan is a giga brain. But, stricly speaking, Alpine is more correct (spec) here. And Blade Component attributes work in the same way as Alpine. However, I don't see a reason how the verbose way would make sense. Since Blade, as well as Alpine (somewhat), are close to Laravel I feel we should try to get (eg) allowfullscreen to render allowfullscreen instead of allowfullscreen="allowfullscreen" everywhere. How it is rendered now is not against the spec, but being verbose also does not add any extra value.

I am not very familiar with Alpine. Maybe someone could PR this to Alpine? I opened #57469 to handle Blade components side of things.

Edit: "more correct" would probably better be phrased "more clean". The verbose way is accepted, while the non-verbose way is the standard.

image

@NickSdot
Copy link
Contributor Author

A) Booleans
I am thinking back and forth about whether it makes sense to maintain a boolean list as suggested by Tim. I can't really come to a conclusion. I tend to say no, because it doesn't help us with non-standard attributes. While I understand where Tim is coming from, I believe this Blade::compileAttribute() beast doesn't do us a favour. We still have non-boolean stuff to deal with which means having it doesn't even make the =/!-syntax obsolete. Complicated.

B) Arrays
Tim, I didn't react to

@attributes([
    'foo' => $v,
    'data-foo' => $v,
    'aria-foo' => $v,
    'wire:poll' => $v,
])

at all yet. Yes, that is easy to add. I don't really see why we need it from a 'length'-wise point of view. But I assume you would want it because how other things in the framework work? Keen to add it when we are at a point where it was signaled that there is a chance to get this merged.

@taylorotwell
Copy link
Member

Personally I think we should just follow what @timacdonald suggested on this, while leaning towards Vue for behavior where there are differences. 👍

@timacdonald
Copy link
Member

Let me know if you would like a hand making any changes here. I've made a lot of noise here, so I'm happy to put my code where my mouth is, so to speak.

As a side note, regarding the accidental incorrect rendering of tags, one interesting thing the hook approach could provide is a 3rd party library that validates common attributes with known allowed values, e.g.,

Blade::compileAttribute('/^dir$/', function ($value) {
    if (empty($value)) {
        return '';
    }

    if (! in_array($value, ['ltr', 'rtl', 'auto'])) {
        Log::warning("Unexpected value [{$value}] for rtl attribute encountered.", [
            'url' => request()->fullUrl(),
        ]);

        return '';
    }

    return 'rtl="'.$value.'"';
});

There could be a hook that ensures title and alt tags are not true, false, null, or an empty string. It can be easy to accidentally have those rendering without realising.

Just a thought I had overnight while I was pondering on this one.

@shaedrich
Copy link
Contributor

shaedrich commented Oct 21, 2025

@timacdonald That reminds me of the example, I posted above:

One alternative to addressing both not having to have multiple directives as well as supporting multiple different notations might be the following:

enum BladeAttributeSpec // This would be the hook used in different packages in your example
{
    case Html; // Different versions might have their own spec: HTML 1 – 4, XHTML, HTML5
    case WaiAria;
    case Data;
    case Js;
    // …
}

Blade::directive('attr', function (string $name, mixed $value, BladeAttributeSpec $spec) {
    $attrRenderer = Blade::getAttributeRenderer($spec);
    return $attrRenderer->render($name, $value);
});

Seems to be something worth looking into. As you have shown, there's even more potential than meets the eye 👍🏻

@NickSdot
Copy link
Contributor Author

@timacdonald not sure you are talking to me or Taylor?


Just to make sure I understand correctly, we are going here with the exact opposite to what Blade Component attributes do (regarding booleans)?

@NickSdot
Copy link
Contributor Author

As a side note, regarding the accidental incorrect rendering of tags, one interesting thing the hook approach could provide is a 3rd party library that validates common attributes with known allowed values, e.g.,

You literally above argued that using the hook should be a very rare case to counter my concerns. Now you are saying the very standard case should be handled by a 3rd party lib. It doesn't make sense, I am afraid.

It also doesn't make sense that we here do the exact opposite of how Blade component attributes work.

The modifier solution I propose perhaps still isn't it. But what is proposed now is also not.

Given that my feedback to it, that is based on 19 days of very hard discussions, is completely ignored I am afraid I cannot support how this played out.

@NickSdot NickSdot closed this Oct 22, 2025
@timacdonald
Copy link
Member

Hey @NickSdot,

Thanks for the PR and the discussions it has initiated. Appreciate you and your time.

I'll follow up with Taylor and see if we are keen to pursue this feature in whatever form it may take.

Thanks again.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.