Vue.js directive to submit and server-side validatate forms using AJAX

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.

 

Source Code Demo

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.

Source Code Demo