Submitting a Form using classical post request will work but it does don’t give the user feedback while it’s submitting and overall not a great user experience. I find myself creating Laravel apps which uses Vue in many places, but if it’s not a complete SPA (without Vue router), to make forms more interactive I have created a directive v-ajax-submit
which I use religiously in my apps to turn boring forms more fun.
This directive takes care of all thing like disables and changes the text of submit button to something like Loading...
while ajax request goes through and once request completes it also handles if any validation error happens, and on success and it refreshes the page or you can use a callback to do whatever needs to be done.
Vue Directive for AJAX form Submit
Since most web apps use jQuery in one or other way I have used it to make ajax call and manipulate the DOM to mark input with errors.
Vue.directive('ajax-submit', {
bind: function (el, binding, vnode) {
// form element
var $el = $(el),
// Submit input button
submitBtn = $el.closest('form').find(':submit'),
// Submit input value
submitBtnText = submitBtn.val(),
// Loading text, use data-loading-text if found
loadingText = submitBtn.data('loading-text') || 'Submitting...',
// Form Method
method = $el.find('input[name=_method]').val() || $el.prop('method'),
// Action url for form
url = $el.prop('action');
// On form submit handler
$el.on('submit', function(e) {
// Prevent default action
e.preventDefault();
// Serialize the form data
var formData = $el.serialize();
// Disable the button and change the loading text
submitBtn.val(loadingText);
submitBtn.prop('disabled', true);
// make http call using jQuery
$.ajax({ url: url, method: method, data: formData })
.done(function(res) {
// Adding a body property to keep the same api
res.body = res;
// Remove highlights
removeErrorHighlight();
// Reset the form
$el[0].reset();
// check success handler is present
if( vnode.data.on && vnode.data.on.success ) {
vnode.data.on.success.fn.call(this, res);
} else {
// run default handler
responseSuccessHandler(res);
}
}).fail( function(err) {
// Adding a body property to keep the same api
err.body = err.responseJSON;
// check error handler is present
if( vnode.data.on && vnode.data.on.error ) {
vnode.data.on.error.fn.call(this, err);
} else {
// run default handler
responseErrorHandler(err);
}
}).always(function() {
// Re-enable button with old value
submitBtn.val(submitBtnText);
submitBtn.prop('disabled', false);
});
});
}
});
Main challenge was to develop a way to handle callbacks on success and error response, since in Vue directive it doesn’t work out of the box and even if you manage to emit event it can listen on parent component, but what I wanted to just add handler on the same Form, After digging a lot I found a way to make things work. So it turns out when you apply a event listener on element, in this case on a form like @success="submitted"
or @error="notSubmitted"
you can assess this on vnode passed as the third argument on bind
hook function of your directive and you can call it using vnode.data.on.error.fn.call(this, err)
that’s what I did on success
and fail
callback from $.ajax
with option to use a default handler if none was given to fit it in all situations and make it reusable.
vnode: The virtual node produced by Vue’s compiler. See the VNode API for full details.
I also wanted to have an option to change the text of the button to something else than Loading...
for some forms. Thanks to jQuery I used data attribute data-loading-text
on submit button to change the loading text if needed.
Vue Resource
If you already have integrated Vue resource or any other HTTP library, its very simple to swap the Ajax call mechanism to suite your need, I have also implemented it using $http
(Vue resource) see the repo for the code.
Handling Callback
In order to handle callbacks, you can just add success
and error
handler on the form like this.
<form @success="addComment" action="/api/comments" method="post" v-ajax-submit >
...
</form>
After that you can add a method on your Vue instance to handle the callback, by default I have omitted ()
parenthesis, this way you will get response from the server as the first argument, but if you need to pass something else you can do it by passing it explicitly @success="addComment(true)"
.
{
methods: {
addComment(res) {
this.comments.push(res.body);
}
}
}
Default Error handler
By default, this directive will get the validation error and show them below the form fields using bootstrap form-group
. You can easily tweak it to make it work for you. Optionally I have used sweet alert to display popups to make it look sweet.
// Default error response handler
function responseErrorHandler(response) {
// handle authorization error
if( response.status === 401 ) {
swal({
title: response.statusText,
text: response.body.msg,
timer: 1500
}, function(){
window.location.reload();
});
}
// any other error
if( response.status >= 400 ) {
if( response.status === 422 ) {
// validation error
swal("Validation Error!", getValidationError(response.body), 'error');
} else {
// handle other errors
var msg = response.body.msg || 'Unable to process your request.';
swal(response.statusText, msg, 'error');
}
}
}
Laravel Middleware for redirects response
In the case of return redirect from laravel controller I created this middleware which turns redirect to URL as a 200 response which is later handled by default response success handler of v-ajax-submit
directive to go to that URL.
namespace App\Http\Middleware;
use Closure;
class AjaxRedirect
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if($request->ajax() && ( $response->status() == 301 || $response->status() == 302 ) ) {
return response($response->getTargetUrl(), 200);
}
return $response;
}
}
You might argue that this doesn’t follow the Vue way of doing things, but what makes it different from the packs is that it is very tolerant, don’t expose a lot of rules to do things in a certain way, as always there are many ways to solve a particular problem in web development. I hope you have learned something new in this post, let me know in the comments if you have any question suggestion or a topic which should I cover. Feel free to grab the complete source code and see the demo of this directive below.