Scoping JavaScript Functionality to Specific Pages with Laravel and CakePHP

I regularly work on medium to large scale websites using both Laravel and CakePHP. A common part of the development process is building in some additional JavaScript functionality to improve the user experience. This can often be functionality that is only relevant to specific pages. For a example, on an ecommerce site the checkout process may consist of several steps each with functionality unique to that page; the basket step may need the ability to update quantities and totals whilst the payment screen may need to toggle fields depending on the card type.

While the unique JavaScript for each of these steps could be loaded in separately, a JS file for each page, it is often better to place it all in the site’s global JS file. This way we can keep the number of page requests low and take advantage of browser caching. This can be done by checking for the presence of particular DOM elements in order to trigger specific functionality (e.g. check for a form ID).

The problem with this is that for every page load the browser needs to run through each check to determine if each DOM element exists. So, for the last couple of years I’ve been taking a different, more efficient approach. As I am using MVC frameworks for my server-side code, I utilise the controller-action generating the page to initiate methods contained within a globally defined object client-side.

This article is an attempt by me to explain the process I use in hope that it is of interest to others. I’ll also show how I implement this approach in both Laravel and CakePHP, although it could easily be adapted for any other MVC framework.

The App Object

I begin each project with a basic object template that looks something like this:-

var App = {
	init: function(controller, action) {
		if (typeof this.actions[controller] !== 'undefined' && typeof this.actions[controller][action] !== 'undefined') {
			return App.actions[controller][action]();
		}
	},
	// Controller-action methods
	actions: {
		controllerName: {
			actionName: function() {
				// Do something here
			}
		}
	}
}

I call the object App, but it can be named anything appropriate that doesn’t conflict with any other names used in the project.

The object consists of two main parts: an init method for triggering the functionality and an actions sub-object that contains the controller-action methods; the controller-action methods can be either closures or function names to be called.

This object then gets compiled in with the rest of the JavaScript being used on a site so that I only need to serve a single JS resource to the end-user. I do this using either Gulp or more recently Webpack (which is used by Laravel Mix).

Webpack

A side note on using Webpack: it’s important to remember that the App object is correctly scoped so that it can be called from within the page. This can be simply achieved by scoping the object to the window by adding the following after defining the App object:-

window.App = App;

When using Laravel Mix this needs to be done in order to use the App object from the view templates.

Initiating the App Object

To initiate the functionality contained within the App object I include a call to the object’s init method at the end of my default layout view template; this needs to be called after the script defining the object has been included. I pass both the controllerName and actionName to the method:-

App.init(controllerName, actionName);

By including this on every page it will trigger the App object’s initialise function which checks if I’ve defined a method for the current controller-action pairing; if I have, it calls that function. Otherwise it will just do nothing. This way I can scope logic to specific pages.

Laravel

When using Laravel the controller and action need setting as variables that can be passed to the view templates. An easy way of achieving this is by adding the following to app/Providers/AppServiceProvider.php:-

public function boot()
{
    app('view')->composer('layouts.master', function ($view) {
        $action = app('request')->route()->getAction();
        $controller = class_basename($action['controller']);
        list($controller, $action) = explode('@', $controller);
        $view->with(compact('controller', 'action'));
    });
}

This ensures that the variables $controller and $action are available to all the view templates. Then in the view layout the App object can be initialised using:-

<script type="text/javascript">
App.init('{{ $controller }}', '{{ $action }}');
</script>

CakePHP

Implementing this in CakePHP is a bit simpler as the controller and action can be determined from $this->request in the view template:-

<script type="text/javascript">
App.init('<?= $this->request->controller ?>', '<?= $this->request->action ?>');
</script>

An Example App Object

Hopefully the above has explained the basics of my approach. To finish I’ll show an example skeleton of an App object that I might use for an ecommerce site. The code below doesn’t show the logic that would be run for each page, but should give you some idea of what the object could look like. It contains methods for the view action of a products controller and the basket and checkout actions of an orders controller.

var App = {
	init: function(controller, action) {
		if (typeof this.actions[controller] !== 'undefined' && typeof this.actions[controller][action] !== 'undefined') {
			return App.actions[controller][action]();
		}
	},
	// Controller-action methods
	actions: {
		products: {
			view: function() {
				// Only run for the view action of the Products controller
			}
		},
		orders: {
			basket: function() {
				// Only run for the basket action of the Orders controller
			},
			checkout: function() {
				// Only run for the checkout action of the Orders controller
			}
		}
	}
};

Some Final Comments

I’ve found that this approach works really well for scoping functionality to pages generated by a MVC framework. It avoids code being run where it shouldn’t and ensures that code is only run on the necessary pages.

It’s worth remembering with this approach that functionality that is site wide should continue to be defined globally outside the App object. However, scoped functionality can still be shared between controller actions by defining it in a function that can be triggered by multiple controller-action functions. It’s also important to not include large JS libraries that may only be required on a specific page globally, for example a WYSIWYG editor or Google Maps. Doing so would greatly increase the file size of the site-wide JS which would act against the benefits of caching; instead these libraries should only be loaded on the necessary pages.

So that’s my approach to scoping functionality in a site-wide JavaScript file. I hope that it’s been of interest.

Related Content

Published on