Framework: [BUG] [5.5] Component slot returns HtmlString, doesn't handle empty checks or isset properly

Created on 6 Feb 2018  ·  22Comments  ·  Source: laravel/framework

  • Laravel Version: v5.5.34
  • PHP Version: 7.2.0-1+ubuntu16.04.1+deb.sury.org+1
  • Database Driver & Version: N/A

Description:

EDIT: Clearer example from below comment.
home.blade.php:

@component('test')
    // Not actually passing in null, this is a placeholder for something like
    // "$myObj->getLink()", which sometimes returns null
    {{null}}
@endcomponent

test.blade.php:

<div class="test">
    @empty($slot)
        {{$slot}}
    @else
        Slot not empty!
    @endif
</div>

That will render the text, "Slot not empty!", when the value was originally null. This appears to be due to the slotted value being wrapped in an HtmlString instance, which is evaluated against (and returns false, since it is a valid instance of an object).

Steps To Reproduce:

  1. Create a component that has slots.
  2. Add an empty / isset / null check on one of the slot variables
  3. Pass that slot a null value
  4. Watch the magic

Most helpful comment

To work around this, I had to do the following inside our component:

@if(isset($link) && !empty($link->toHtml()))

All 22 comments

To work around this, I had to do the following inside our component:

@if(isset($link) && !empty($link->toHtml()))

5.4 is not supported anymore, please test on 5.5 and report if this is still an issue, many things were fixed between 5.4 and 5.5

Just tested on 5.5.28 (and then upgraded to 5.5.34) and confirmed it is still an issue.

home.blade.php:

@component('test')
    {{null}}
@endcomponent

test.blade.php:

<div class="test">
    @empty($slot)
        {{$slot}}
    @else
        Slot not empty!
    @endif
</div>

That will produce the value, "Slot not empty!"

Conversely, switching that @empty to @isset will make the slot variable render (which is an empty string or null). This is because when it checks that value, it's actually an instance of HtmlString, and therefore isn't considered empty or null.

I believe an easy fix here would be to override the magic methods on those classes to check their value for isset, or maybe even override the __toString method. That would resolve @isset, not positive about @empty.

(ignore accidental closing and reopening, misclick ;) )

This is indeed the normal behavior of php's empty() function.

empty — Determine whether a variable is empty

In other words, it will return true if the variable is an empty string, false, array(), NULL, “0", 0, and an unset variable.

You are looking for php's isset() function.

isset — Determine if a variable is set and is not NULL

In other words, it returns true only when the variable is not null.

Laravel provides you the @isset and @empty directives Blade Templating: Control Structures.

@isset($variable)
    // $variable is defined and is not null...
@endisset

@empty($variable)
    // $variable is "empty"...
@endempty

Replacing your condition with @isset($variable) will fix your bug, i guess.

A little bonus tip:

There is also a package called laravel-blade-directives, which provides you a collection of useful blade directives that might help you.

In this package you'll find the @isnotnull($variable) directive, which would also solve it.

Happy Coding :)

@brianwusu I believe you misunderstood. I am getting the opposite of what is supposed to happen.

I understand that if a variable is null, an empty string, empty array, false, etc., that empty will return true.

What is happening here, however, is that we have a value that is most certainly "empty" by that criteria being passed into a component, being passed into the @empty directive, and being evaluated as not-empty.
Please see my followup example above ☝️ in the comments.

EDIT: Updated my original post. Had a typo in my original description - woops! Should be a lot clearer now.

We know that {{null}} compiles to e(null) which outputs an empty string as can be easily with something along the lines

Route::get('/', function () {
    return e(null);
});

in routes/web.php. So this certainly is not the issue here.

But my guess is that the formatting of the

@component('test')
    {{null}}
@endcomponent

is the issue. To prove that, swap the content of test.blade.php with the following

@php
dd($slot);
@endphp

As a result you will see

HtmlString {#433 ▼
  #html: ""
}

in the browser. This tells us that $slot is not a plain string but an object and therefore not empty as already explained by @brianwusu. And it looks kind of intended.

It seems to be possible to work around that issue by using @empty(e($slot)) instead of @empty($slot). Not really sure if this may introduce other issues though.

@Namoshek I think we agree here? That's the issue I'm pointing out here. One would expect that slot would be considered empty, as it was a null value. However, because it is being wrapped in that HtmlString object, it is not considered empty or null. That's the issue here - I would expect that if I do @component('test'){{null}}@endcomponent and then did @empty($slot)Empty!@endif it would be considered "empty".

Actually, I don't agree, because I don't see a use case for {{null}} and an @empty($slot) check on it. The only use case I see for passing {{null}} would be if you had {{$someObj}} instead (where $someObj can be null). But here you quite certainly would use @isset($slot) and not @empty($slot). Or am I missing something?

But well, if you see a use case, you can still use the workaround I provided above anyways...


I would expect that if I do @component('test'){{null}}@endcomponent and then did @empty($slot)Empty!@endif it would be considered "empty".

That looks like a dirty hack and not how the template syntax is meant to be used.

@Namoshek I'm not passing null into a blade component, that was just an example. My blade component invocation ends up kinda looking like this:

@component('myComponent')
    @slot('link'){{$myObject->getLink()}}@endslot
    {{$myObject->getDescription()}}
@endcomponent

Then in the component, something like this:

<div class="my-component">
    @empty($link)
         {{$slot}}
    @else
        <a href="{{$link}}">{{$link}}</a>
    @endif
</div>

In the above example, $myObject->getLink() will sometimes be null (as it is optional). When it is null, I would expect the @empty directive to catch that, and show the slot instead. However, it instead will render an anchor tag with no href or content.

I understand that I shouldn't be passing null into a component directly - there'd be no valid reason to do that, it would make zero sense to do so. I definitely believe it's an issue that a value that is nullable / empty / empty is treated as not within a blade component, because it gets wrapped.

EDIT: I mention this as well in the OP, but using @isset or @empty both treat the value as not null / not empty. i.e. if the component is changed to:

<div class="my-component">
    @isset($link)
        <a href="{{$link}}">{{$link}}</a>
    @else
         {{$slot}}
    @endif
</div>

It'll render the anchor tag instead.

If you use

@component('myComponent')
    @slot('link', $myObject->getLink())
    {{$myObject->getDescription()}}
@endcomponent

you get your desired behavior. If you don't pass complex content to a slot, the content is passed as-is... so in this case as string.

An optional field is considered "complex content"? Are you being serious right now?

I definitely do not get the desired behavior - I don't want my HTML to have empty anchor tags.

Not the field is complex, but the content (may be). Using enclosing blade template tags expects the content to be HTML and handles it as such. If you pass the content directly as parameter to the single opening template tag, which is absolutely sufficient for a string (and also more readable in my opinion), it is passed without changes, because blade doesn't expect HTML there. Not sure what is unclear about that.

I don't want my HTML to have empty anchor tags.

Demanding changes in a framework because of personal code style taste is kind of weird.

I'll quit the discussion here now and let the maintainers make their choice.

Re: @slot('link', $myObject->getLink()), I've never seen this syntax before in the docs - a cursory glance shows the docs mention passing data in as part of the @component directive, but not into a slot like that. You are correct in that it appears to resolve my issue - Using that syntax the null is passed through without being coerced (and most importantly, handled properly).

I think the ☝️ syntax should be added to the docs, and I think it'd be a lot clearer to have a reference to why that alternate syntax is available. This doesn't seem like such a far-out use case, and I doubt I'm the only person who's scratched my head trying to figure out what's happening.

Facing same issue.

@mohd-isa Read the latest comments, they explain how to circumvent the "issue".

I'm facing this problem passing a timestamp string and converting it to a Carbon object in the view, so I had to force the $slot to return a string like this:

{{ Carbon\Carbon::createFromTimestamp(''.$slot.'')->format('...') }}

Why was this closed, is it not considered an issue? I'm still using https://github.com/laravel/framework/issues/23049#issuecomment-363546052 solution, but it's more of a workaround tbh.

Hi.

I'm using:

@if($slot->isEmpty())

I'ts one of the methods of Illuminate\SupportHtmlString

I had a similar issue (v. 5.8.*) - I resolved by using casting: @empty((string) $slot).

@if($slot->isEmpty()) sadly breaks if a slot is passed using <x-foo :slot-name="$bar" />, and possibly <x-foo slot-name="bar" /> although I have not checked.

The best way I can think of handling this right now is with a custom Htmlable (and possible DeferringDisplayableValue) aware is_empty() helper that resolves the actual string and then checks it. The advantage of this is that I have a place to trim the string before checking it, which is most often appropriate.

@stefanfisk You could try casting the $slot to a string with blank() or so:

@if(blank((string)$slot))
    <span>No slot</span>
@endif
Was this page helpful?
0 / 5 - 0 ratings

Related issues

JamborJan picture JamborJan  ·  3Comments

SachinAgarwal1337 picture SachinAgarwal1337  ·  3Comments

progmars picture progmars  ·  3Comments

Fuzzyma picture Fuzzyma  ·  3Comments

iivanov2 picture iivanov2  ·  3Comments