Page Based JavaScript in Laravel

I love Rails. Its great. Its fun. But sometimes I also like to use PHP (as crazy as that sounds.) When I spin up a PHP based project I more often than not use Laravel or Lumen. They are great.

The only gripe I have with them is how you cannot do page-based javascript like you do in Rails.

For example, when you run rails g controller UserController, Rails generates multiple files for you – controller, view, js, sass. On the other hand when you run php artisan make:controller User only a controller gets generated.

Maybe they are right to do this because after all the command run is saying to only generate a controller. But if this is the intended point then why does Laravel have the magic that Rails does?

Make it Magical

So how do I get around no JS files being generated for me? Well luckily, Laravel 5 comes with a new tool called Elixir.

My next step was to write a Gulpfile that attempts to emulate how the js gets magically compiled in Rails. It was quite easy actually.

Opening up your gulpfile.js, there is a base template already there for sass compilation. So lets change that around to work for our js.

var elixir = require('laravel-elixir');

elixir(function(mix) {
    mix.scripts([
        'pages/*.js'
    ], 'public/js/main.js', 'resources/javascript');
});

So whats going on here?

The above is all you need for your project to begin being magical like rails. Simple right? Very.

The mix.scripts function accepts 1 parameter with 2 optional. So something like mix.scripts(paths[, outFile, baseDirectory]). So lookin back to our code above, we are looking for all js files in the pages directory of the resources/javascript base diretory. Or even shorter, anything in resources/javascript/pages.

This is all great, but if you try and run it, you’re going to get an error. That’s because those directories more than likely don’t exist yet. So lets make them exist. Just run mkdir -p resources/javascript/pages from the base of your project. Next just add a file to that directory with touch resources/javascript/pages/foo.js. Add anything to that file and run gulp. If you go check in the public/js/, you should see main.js with the contents of foo.js in it. To do live compilation of the js as you work on your project, just run gulp watch. This watches for any changes in the resources directory by default.

Thats all! Super easy huh? These same concepts can be applied to sass or less or css in your project with a few extra lines of code.

Thats not all!

So you have all the files compiling now, but what next? Why is EVERYTHING running on EVERY page? That’s because all the javascript files are being concatenated into one large javascript file.

To prevent this, we can use the javascript module pattern and add some code to the Laravel AppServiceProvider class.

Before we get to that, lets create a simple javascript module. Open up the foo.js file you made and add this to it.

var BarController_show = (function() {
    var somePrivateFunction = function() {
        document.querySelectorAll('.js-title')[0].innerHTML = "It's definitely working!";
    };

    return {
        start: function() {
            somePrivateFunction();
        }
    };
});

It looks a little crazy but it will all make sense in a minute. Save that file and then lets generate a controller with some views.

Run php artisan make:controller BarController and open up BarController in app/Http/Controllers.

In that file, replace the contents with:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

class BarController extends Controller
{
    public function show()
    {
        return view('layout');
    }
}

Now create the view associated with that controller action with touch resources/views/layout.blade.php.

Open up that file and lets just add a simple html template:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Test!</title>
    </head>
    <body>
        <h1 class="js-title">It's working!</h1>
    </body>
</html>

Then modify app/Http/routes.php to include a route for the new view. Add something like Route::get('/foo', ['as' => 'foo', 'uses' => 'BarController@show']);. This is telling our app that the /foo route will use the show action (or function) of the BarController class. The show action then returns view('layout') which is the blade template from above.

Now spin up your app and navigate to /foo. You should see the text It’s Working!. If so, good job.

The next thing to do is figure out a way to load up the javascript files we made. To do that, modify the head tag of the bar.blade.php

Add the following to your header to include the javascript file that has been generated for our pages javascript.

<script type="text/javascript" src="{{ URL::asset('js/main.js') }}"></script>

Refresh your page and check the DOM, the file should now be loaded.

How does the javascript run?!

You might be wonder how to run the javascript based on the page, what this entire writeup is about. Well, we have to modify the app provider for our Laravel app to inject the current controller and action names into the page. To do that, open up AppServiceProvider.php and modify the boot function. Doing so should give you something like:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        /*
        Exposes the controller name and action in layout files.
         */
        app('view')->composer('layout', function($view) {
            $action = app('request')->route()->getAction();

            $controller = class_basename($action['controller']);

            list($controller, $action) = explode('@', $controller);

            $view->with(compact('controller', 'action'));
        });
    }
}

The above code will inject two variables, $controller and $action into any view with the name layout. This is why we named the view file about layout.blade.php.

Now lets go back to our layout.blade.php file and tell it to load up the correct function.

In the layout file right before the closing body tag, add the following:

<script type="text/javascript">
    (function(module) {
        module.start();
    })({{ $controller }}_{{ $action }});
</script>

Because we injected the controller and action names into the view, at page render, the following will be generated for our /foo route.

<script type="text/javascript">
    (function(module) {
        module.start();
    })(BarController_show);
</script>

It all makes sense now right? BarController_show is the name of the javascript module we wrote together earlier. If you refresh the page, the text should no longer say It’s Working! but rather It’s definitely working!.

That was a lot…

It is but once it’s up and running you’re smooth sailing.

So a simple recap: 1. Use Elixir to concat all js files in a directory 2. The js files should be written using modules and these modules should be named to match the name of the Controller and action the view will be called from 3. Inject the controller and action names into the layouts via the AppServiceProvider 4. Use a function closure with the variables to generate javascript to run the module 5. Profit

Hope this helped others because it took me a while to figure out a nice flow myself.