I had a chance earlier this month to convert a fairly generic CRUD-y application that I built in Laravel into something that a little bit more interactive. Since I already got all the interaction nailed down in Laravel including form validation and routing, I decided to use Turbolinks and Stimulus to make the application snappier and behave more like a single page application.

Since both Turbolinks and Stimulus come from the same developer, they pair quite nicely. However, to make them play nice with Laravel, few things need to be taken care of before.

Note: The rest of this article will be based on the default Laravel install, so you probably need to tweak it accordingly based on your application.

Prerequisites

First, install the required dependencies:

1
npm install turbolinks stimulus

Next, we’ll need to tweak the entry point of our Javascript. For the default install, this will be in resources/js/app.js. Add these lines:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Turbolinks from 'turbolinks';
Turbolinks.start();

import { Application } from 'stimulus';
import { definitionsFromContext } from 'stimulus/webpack-helpers';

const application = Application.start();
const context     = require.context('./controllers', true, /\.js$/);

application.load(definitionsFromContext(context));

Please note that all of our Stimulus controllers need to be created inside the resources/js/controllers directory.

One last step we need to do for the initial setup is to add support for the class property during our Babel compilation. Add this line into the .babelrc file. You might need to create one if it doesn’t exist.

1
2
3
{
    "plugins": ["@babel/plugin-proposal-class-properties"]
}

By now, Turbolinks is enabled and will start intercepting all requests by default.

Adding a global middleware to our Laravel application

As mentioned, all normal requests should now get intercepted by Turbolinks. However, it might not be able to handle our redirection properly if we define something like this in our routes/web.php file.

1
Route::redirect('/route-1', '/route-2');

To solve this issue, we can create a global web middleware that’ll get executed on every request. I created this app/Http/Middleware/SetTurbolinksHeader.php and set it to load in the web middleware group.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

// app/Http/Middleware/SetTurbolinksHeader.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Session;

class SetTurbolinksHeader
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        if ($request->ajax() && !$request->isMethod('GET') && $response instanceof RedirectResponse) {
            $response->setContent($response->getTargetUrl());
            $response->setStatusCode(202);
        } else {
            $response->header('TurboLinks-Location', $request->url());
        }

        return $response;
    }
}

And in our app/Http/Kernel.php file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
...
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\SetTurbolinksHeader::class,
    ],

    'api' => [
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];
...

I’ll explain about the line

1
if ($request->ajax() && !$request->isMethod('GET') && $response instanceof RedirectResponse) {

in the next section.

Adding a dedicated FormRequest based class

In my application, I make heavy use of Form Request validation for all of the form submissions. I created a TurbolinksFormRequest that extends the default Laravel’s FormRequest class that overrides the failedValidation method. The whole class is pretty simple, it creates a new response, with all the user inputs and any validation errors and throws a new ValidationException exception.

The content of the class is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

// app/Http/Requests/TurbolinksFormRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;

class TurbolinksFormRequest extends FormRequest
{
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];

    protected function failedValidation(Validator $validator)
    {
        $response = $this->redirector->to($this->getRedirectUrl())
            ->withInput($this->except($this->dontFlash))
            ->withErrors($validator->errors(), $this->errorBag);

        throw new ValidationException($validator, $response);
    }
}

There is no reliable way for me to get the array of $dontFlash from the app/Exception/Handler.php file so I decided to just copy the array again into this class. Now, all we need to do is to change any FormRequest instance that we have in our controllers with TurbolinksFormRequest and we’re good to go. The middleware that we have before will automatically set the content to the redirected URL and set the status code to 202.

For example, if we have something like this in our route file:

1
2
3
4
5
6
7
<?php

Route::post('/contact', function (FormRequest $request) {
    // send the email

    return redirect()->back();
});

We can convert it to:

1
2
3
4
5
6
7
<?php

Route::post('/contact', function (TurbolinksFormRequest $request) {
    // send the email

    return redirect()->back();
});

Stimulus-powered AJAX Form

The last step is to convert all of our forms to the Stimulus version of it. To begin, create a new controller inside the resources/js/controllers folder. I’ll name it ajaxform_controller.js to follow the naming convention mentioned in its handbook.

At its very basic form, the controller will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { Controller } from "stimulus"

export default class extends Controller {

    submitForm(e) {
        e.preventDefault();

        axios.post(this.element.action, new FormData(this.element))
            .then(function (res) {
                Turbolinks.clearCache();
                Turbolinks.visit(res.data);
            })
            .catch(function (error) {
                console.error(error);
            })
            .finally(function () {
            });
    }
}

Remember the line in custom middleware that we created previously?

1
if ($request->ajax() && !$request->isMethod('GET') && $response instanceof RedirectResponse) {

Now all AJAX requests will return a redirect URL in the response body. So all this class needs to do is to prevent default form submission and do a request using Axios library instead. We then can use the retuned URL to redirect form submission using the Turbolinks.visit(res.data). To use this controller in our existing forms, add the related attribute data-controller and data-action into our form.

For example, if we have a form like this:

1
2
3
4
5
6
<form action="/contact" method="post">
    @csrf

    ... form elements
</form>

Change it to be

1
2
3
4
5
<form action="/contact" method="post" data-controller="form" data-action="form#submitForm">
    @csrf

    ... form elements
</form>

Conclusion

Integrating Turbolinks and Stimulus into r Laravel application is fairly easy to do. With minimal changes in the existing codebase and the fact that they pair nicely together, it can be a powerful combination to use to make our app feels snappier and behaving more like single page application.

Reference