Skip to content

Conversation

NickSdot
Copy link
Contributor

@NickSdot NickSdot commented Oct 1, 2025

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.

@taylorotwell
Copy link
Member

taylorotwell commented Oct 8, 2025

Has anyone tried building this as a simple package that registers the directive as a Blade extension?

I think I do find the true / false behavior a bit unintuitive, but it may not be solvable for all use cases. The example that came to mind for me:

<div class="something something-else" @attribute('wire:poll', $shouldPoll)>
    ...
</div>

@NickSdot
Copy link
Contributor Author

NickSdot commented Oct 8, 2025

Has anyone tried building this as a simple package that registers the directive as a Blade extension?

I think I do find the true / false behavior a bit unintuitive, but it may not be solvable for all use cases. The example that came to mind for me:

<div class="something something-else" @attribute('wire:poll', $shouldPoll)>

    ...

</div>

Not as a package, but I have it in service providers in the hope to get rid of it when this gets merged.

The attribute you mention, by spec, is an enumeration attribute because it also allows other values like wire:poll="refreshSubscribers". Your example would render wire:poll="true".

You probably didn't read all the comments here. My personal conclusion was that we need @maybe and @attribute to cover both concepts, because they are diametrical to each other. This here, however, covers the majority cases that are not Livewire specific.

Think of ARIA attributes like `aria-hidden="true" that expect explicit tokens like true/false. Or what Tim brought up, he would expect an empty string to render. But we definitely don't want that with, for instance, the title/rel attributes which we all use all the time; including people that never touched Livewire.

By adding @maybe and @attribute we will be able to cover every single use case. Though, @attributes would be more complex than what I propose here. There were previous PR attempts, perhaps one of them could be retried after this here is merged and acceptance was signalled.

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.

9 participants