-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5.8] Add @error blade directive #28062
[5.8] Add @error blade directive #28062
Conversation
Pretty neat! |
minor suggestion, switch to |
@calebporzio I think the suggestion by @browner12 is valid. Can you update the PR? |
Sounds like a good start to adding blade helpers to your awesome-helpers package, or an awesome-blade-helpers package. |
Problem with such helpers is that they are not flexible, they handle a very specific case. For example, you can't check if error does not exists, so in this case and many others you'll have to fallback to a regular if statement. Personally I try to keep my code consistent so I avoid using such helpers. |
Updated - thanks! |
I made a PR before like this and closed #27944 |
@mo7amed-3bdalla7 this is a different implementation. |
Problem I have with stuff like this is although on the face of it it looks nice - your still find yourself reaching for <div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<input type="email">
@error('email')
<span class="help-block">{{ $message }}</span>
@enderror
</div> ...developers may get confused (especially newcomers) as to why they need to use both Rendering errors is always something I've found a bit clumsy, so in previous projects I've experimented using a "wrapper" around the container for the field, something along these lines: @field('email', ['class' => 'flex']) <!-- <div class="flex form-group has-error"> -->
<input type="email">
{{ $error }} <!-- <span class="help-block">...</span> -->
@endField ...but even that style I've started to go off because, although it reduces amount of code, it's a bit cryptic in what it's actually going to render. It also creates a disparity between how you render errors in js e.g. Vue/React, to how you do it in blade. Purely for explicitness, I've started just writing it out in full, that way developers know exactly what it's going to render, how to render errors, why it's rendering that way etc. In summary, I've still not come up with a solution I've personally been that comfortable using in my apps, but this PR looks interesting as a way of reducing the amount of code for the "inner" message problem but doesn't solve the "outer" problem, if that makes sense. I still think there are better alternatives, so I personally would say for this reason it's maybe something best left in userland. |
Makes sense, what about this:
I kinda dig that. There could be a third parameter default too: I get just wanting to keep the original |
For reference: Here's my #noplanstomerge PR to 5.4 #20111 |
Does it handle named error bags? Something like? Blade::directive('isInvalid', function ($expression) {
$segments = explode(',', $expression);
$input = trim($segments[0]);
$errorBag = trim(Arr::get($segments, 1, 'default'), "'");
return '<?php echo $errors->'.$errorBag.'->has('.$input.') ? "is-invalid" : ""; ?>';
}); Why not two directives? // Bootstrap css classes
Blade::directive('validationMessage', function ($expression) {
$segments = explode(',', $expression);
$input = trim($segments[0]);
$errorBag = trim(Arr::get($segments, 1, 'default'), "'");
return '<?php if ($errors->'.$errorBag.'->has('.$input.')) { ?>
<div class="invalid-feedback d-block" role="alert">
<?php echo e($errors->'.$errorBag.'->first('.$input.')); ?>
</div>
<?php } ?>';
}); |
@ankurk91 - the goal of this feature is to cover (what I believe to be) the 90% of use cases for |
I'm still not convinced this is something that should be added to core. There are so many opinionated ways you could do error handling in views, getting involved in this logic is probably a recipe for disaster down the line. Style 1 - render second parameter if contains an error<div class="form-group @error('email', 'has-error')">
<input type="email">
@error('email')
<span class="help-block">{{ $message }}</span>
@enderror
</div> Style 2 - allow html as second parameter, to format error<div class="form-group @error('email', 'has-error')">
<input type="email">
@error('email', '<span class="help-block">:message</span>')
</div> Style 3 - using a custom helpers to return class/html formatted errors:<div class="form-group @errorClass('email')">
<input type="email">
@errorHtml('email') <!-- outputs <span class="help-block">.... if there is an error !-->
</div> Style 4 - using short Emmet-style syntax to reduce amount of typing of second parameter<div class="form-group @error('email', 'has-error')">
<input type="email">
@error('email', 'span.help-block') <!-- will be auto-closed and contain :message -->
</div> Style 5 - for multiple error bags.I give up. All of the above to avoid: Current:<div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<input type="email">
@if ($errors->has('email'))
<span class="help-block">{{ $errors->first('email') }}</span>
@endif
</div> Or: <div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<input type="email">
{!! $errors->first('email', '<span class="help-block">:message</span>') !!}
</div> |
"All of the above to avoid" - I don't think the mentioned "above" is a valid problem statement. I never proposed offering Emmet style syntax. Here is what I'm proposing: @error('first_name')
<span>{{ $message }}</span>
@enderror to avoid having to write @if ($errors->has('first_name'))
<span>{{ $errors->first('first_name') }}</span>
@endif This will cover most use cases. In the case of needing an inline style, you can do this: <input name="first_name" class="@error('first_name') has-error @enderror">
@error('first_name)
{{ $message }}
@enderror I think this is cleaner. I think this is prettier. A bunch of other people think so too. |
What about that ? https://twitter.com/michaeldyrynda/status/1115458350710321153?s=21 |
protected function compileEnderror($expression) | ||
{ | ||
return '<?php unset($message); | ||
if (isset($messageCache)) { $message = $messageCache; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong (didn't test it), but it seems this can produce side-effects if we include @error
directive inside another one - original $message
value will not be preserved:
$message = 'i use message in view';
...
@error('email')
...
@error('phone')
...
@enderror
@enderror
...
{{-- $message now contains 'email' rule message instead of original string value --}}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you're wrong. Should return to the previous $message
if one existed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you're wrong. Should return to the previous
$message
if one existed.
Believe is a strong word. Here is what I see:
-
Initial state:
$message: 'i use message in view', $messageCache: <unset>'
-
First
@error
call:
$message: 'email rule message', $messageCache: 'i use message in view'
-
Second
@error
call:
$message: 'phone rule message', $messageCache: 'email rule message'
-
First
@enderror
call:
$message: 'email rule message', $messageCache: 'email rule message'
-
Second
@enderror
call (and our end state):
$message: 'email rule message', $messageCache: 'email rule message'
$message
looses it's original value. Where am I wrong on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@d3jn You wouldn't want to nest @error
blocks with this PR, or forget to @enderror
because you'll get side effects, as you mention. It's one of the downsides to this implementation. If used once, you can still use $message
in your views, but not $messageCache
as that is used internally to remember the state of $message
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@garygreen I can definitely see a need to do nested checks (when certain fields are paired or have hierarchical relationship), so I just pointed out that this implementation will produce a side-effects in such situations. It definitely diminishes it's value if you need to actually take into account specifics of it's implementation when using it.
This type of side-effect can be resolved by using a stack (array) of cached $message
values and putting/popping values in/out of it. This will allow for a nesting support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you need to nest? You can use dot format to render deeply nested error messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I said "believe" not to be confrontational, but to express I don't actually know for sure. I meant "believe" to be a loose word, not strong lol.
I'm pretty indifferent on supporting nesting because I can't really see myself using it. But I agree it seems like the behavior is unexpected if what you're saying is true.
Can you provide the solution?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide the solution?
Just utilize array_pop
/array_push
and make $messageCache
into array instead. This will look something like this:
// For opening directive
return '<?php if ($errors->has('.$expression.')) :
if (! isset($messageCache)) { $messageCache = array(); }
array_push($messageCache, $message);
$message = $errors->first('.$expression.'); ?>';
...
// For closing one
return '<?php unset($message);
if (! empty($messageCache)) { $message = array_pop($messageCache); }
endif; ?>';
But make sure to cover it with separate test if you do decide to get rid of this problem and introduce nesting support without potential side-effects.
Yeah, Michael's example is nice. However, I am not a fan of having to use |
I'm not disagreeing behind the intentions of this PR - reducing the amount of code to write is always a good developer experience, but I personally think we should be careful adding new blade stuff like this without properly understanding why it's needed. This PR introduces side effects, by which I mean: what happens in the current PR you Most will solve the problem in userland by writing their own helpers which can fully format the error message to whatever framework format they are using without side effects, or use IDE helpers to write their own keyboard shortcut snippets. For example, you can write some very basic directives to do this kind of format, which IMO is even simpler and more declarative in usage than this PR. <div class="form-group @hasError('email')"> <!-- class="form-group has-error" -->
<input type="email" class="form-control">
@error('email') <!-- <span class="help-block">:message</span> or NULL -->
</div> \Blade::directive('hasError', function($expression) {
return "<?= \$errors->has($expression) ? 'has-error' : '' ?>";
});
\Blade::directive('error', function($expression) {
return "<?= \$errors->first($expression, '<span class=\"help-block\">:message</span>') ?>";
}); Again, I'm not totally against this PR, but I just question it's implementation and usefulness in real world applications when there are alternatives in userland. |
Agreed that this can be accomplished debatably better in userland, but that goes for most syntax like this. I would say that this is such a rediculously common use case, it makes sense to make it nicer. It's like Good point re onboarding newcomers. I think this could be the primary way of documenting and then, an example/note using the more verbose I believe it's useful for most applications using Blade. |
Maybe a paranoid concern but should we be concerned about merging this into 5.8 and stomping on someone's "macros"? Do macros take precedence over Blade defined directives? |
@taylorotwell I don't believe we need to take into account whatever anyone has added in user land. If that was the case than you couldn't add anymore new methods to classes etc because you'd "risk" it that someone else would have implemented that method. New functionality which doesn't breaks the public api should always be ok. |
@driesvints I believe the concern is if to merge into 5.8 or master |
@rellect yes, that's what I was addressing. |
Usually I'd agree that we shouldn't waste too much time worrying about userland conflicts with custom macros/helpers, however I believe "error" is so common that we should consider sending to master and noting it in the upgrade guide. I have to assume there are many apps using an "error" helper. I don't think this is such a pressing matter that we should risk breaking these apps on a point release. I'm not even sure which would take precedence, so it may not be an issue. |
@devcircus yeah, that's a good point actually. |
Just confirming that a userland directive takes precedence over a core directive. public function boot()
{
Blade::directive('php', function ($expression) {
return "<script>alert('You wanted PHP. You got JS. You\'re welcome.');</script>";
});
Blade::directive('csrf', function ($expression) {
return "<script>alert('CSRF');</script>";
});
}
|
If that's the case then I'm 👍🏻 |
🎉🍾🍻🎉 |
This broke our app in a few places! We have a lot of vue directives that use @error, ie.
I have just changed that to
For now... Is there a better way to escape the @ symbol? |
The quickest fix would probably be to just declare a custom directive to restore your original behaviour. public function boot()
{
Blade::directive('error', function ($expression) {
return '@error';
});
} |
@timacdonald thanks. I found something saying I would think this would be a breaking change given how many Laravel devs must use |
@jsiebach we can't account for every X language out there. Since nothing changed to the public API of Laravel this wasn't a breaking change. |
Just done the upgrade and ran into this exact problem 👎 |
This PR comes out of a popular tweet I wrote: https://twitter.com/calebporzio/status/1111366190444802048