imacrayon / alpine-ajax

An Alpine.js plugin for building server-powered frontends.
https://alpine-ajax.js.org
MIT License
558 stars 11 forks source link

Redirect on successful form submission #79

Closed MikeHarrison closed 1 month ago

MikeHarrison commented 2 months ago

Hi,

I have a login form on my site. When submitted, using good old PHP, I have a redirect taking people to a new URL if the login is successful.

I have added Alpine AJAX to this form, so any error messages get displayed (e.g. incorrect password) without a whole page reload.

Now I have the issue where a person successfully logs in, my redirect isn't happening. What is the best way, using Alpine AJAX, to redirect someone on successful submission?

Thanks for any help

imacrayon commented 2 months ago

Hey Mike, thanks for reaching out.

If you're using v0.7 you can add a second x-target to your form like this: x-target.3xx="_self". That will ensure the page still redirects when any 3xx status code is returned from the server.

If you're using any version less than v0.7 adding the nofollow modifier to x-target was the old way to do this: x-target.nofollow="login_form_id".

MikeHarrison commented 1 month ago

Hi, Thanks for the reply, sorry it has taken so long to respond.

My form tag now looks like this:

<form x-init x-target x-target.3xx="_self" id="login-form" action="<?php echo $page->url() ?>" method="POST">

When I put in the correct credentials to log in, I am getting redirected as I would like. Which is great. But I am now not getting any of my error messages when login fails (e.g. if the wrong password is entered).

I may well be doing something wrong, or need to update my form handler in some way. If you have any insight or thoughts that would be great.

imacrayon commented 1 month ago

Ok, sounds like you're on the right track. So since you're using a naked x-target only the the form element (#login-form) will be changed when you submit the form. If the error messages live outside the <form> that could explain why they're not showing.

If that's that case, you could either target a different element that contains both your errors and the form, or you can specify multiple IDs in x-target like this x-target="login-form form-errors".

If that's not the case, I might need some more context, maybe what the page markup is like or how you're handling the form response server side.

gregmsanderson commented 1 month ago

I'm finding this same issue and not sure it can be (easily) fixed. It's not an issue with Alpine Ajax from what I can see.

It seems Laravel (I assume that is being used) returns a 302 status code both on a validation error and also on success (unlike with an SPA where you would return e.g a 422 or a 200). A validation error would throw a ValidatonException. That is caught and the user redirected back to the same page. With a ... 302. On success, generally you will also redirect (e.g to /dashboard). With a ... yep, 302.

Alpine Ajax can't know which it is by the status code alone.

What we'd need is for the 302 caused by the validation error to target the form element (and so not cause a full-page reload) but the 302 caused by a successful submit to use _self and so do a full page load.

I did wonder about sending a custom header (e.g X-Alpine) and check for that in the backend ... That would still allow a progressive enhancement since that custom header would only be sent if Alpine Ajax was making the request. That way the backend could return e.g a 422 and solve the problem. Since then Alpine Ajax would have a different status code to work with.

imacrayon commented 1 month ago

Bummer. Thanks for the clarification, I completely forgot about Laravel handling validation differently based on the request header (I was thinking it was always a 422). In my Laravel apps I tend to use a normal form if I need it to redirect, so I don’t use the _self trick much. I’d love to find a workaround that doesn’t require backend changes, but we might have to admit defeat here. I’ll think on it a bit, but yeah it wouldn’t be too difficult to add support for a X-Alpine-Redirect header that could trigger a client side redirect.

gregmsanderson commented 1 month ago

@imacrayon No problem. I had a look and when you throw a ValidationException on a normal form validation issue, this thing https://laravel.com/api/11.x/Illuminate/Validation/ValidationException.html does have a 422 (at that point). It's just that when it is handled, it returns a redirect ...

https://github.com/laravel/framework/blob/4672516314723023f61aa165acfc3b59c5cb9269/src/Illuminate/Foundation/Exceptions/Handler.php#L742

... and hence you get a 302, not a 422. Scrolling down that page, a JSON response would return a 422.

In theory that could be overridden but the other, simpler direction could be handling the success case: the return redirect()->route('home'). Being a redirect, that can't have a header added to it. Else that could have your idea of a X-Alpine-Redirect added. So I'd guess ... it would have to be a 200 (or something) and specify the URL for Alpine Ajax to redirect to in that header. Not sure what issues that could cause (cross-domain stuff). Perhaps its value must be a path, not a full URL, so e.g /dashboard. That would keep on the same domain X-Alpine-Redirect: dashboard.

imacrayon commented 1 month ago

@gregmsanderson Oh yeah - you’re right - a header isn’t much help in this case either. 😩

Ok, what if we use the location of the redirect instead? We add a new target modifier that handles this logic: “If this redirect response points to the current page, handle it without a full page reload, but trigger a full page reload on a redirect to any other URL”.

Maybe it doesn’t need to be a new modifier? I think that behavior might make sense as the default for _self. We could add a new special target value for cases where devs might actually want to redirect no matter what the location is _top could be a candidate.

gregmsanderson commented 1 month ago

@imacrayon That's even better! That would mean no backend change would be needed. I agree, I don't think it needs a new modifier. I think that makes more sense as the default behaviour. But yes, I guess also support a method for people that don't want that.

MikeHarrison commented 2 weeks ago

Hi!

Thanks for spending the time looking at and working on this. I just wanted to check in and say the updates you have made worked for me, and the form is now doing exactly what it needs to :)