Advanced Frontend Webapp Architecture With Laravel and Vue JS

Advanced Frontend Webapp Architecture With Laravel and Vue JS
Advanced Frontend Webapp Architecture With Laravel and Vue JS

ATTENTION: Vue’s Vue-Router sub-project has come out with an update that fills most of the functionality in this article.

You can now use this.$router.app at any time inside a component to access the main component. Check it out here: http://router.vuejs.org/api/properties.html. And, Vue now has events built in. So I can say this.$dispatch(‘foo’) to run methods on other components. Haven’t played with it much but it looks really cool. http://vuejs.org/api/instance-methods.html#Events.

Vue JS 2 - The Complete Guide (incl. Vuex)

That said the rest of this article teaches important skills in front-end applications and is still a good read.

Do you use Laravel PHP and Vue JS? Do you wish there was a better way to structure your applications you build? This tutorial is just for you! In it, we set up simple two-way communication between the different components of your front-end app.

Would you like to get the results of this tutorial without reading through it? Check out Laravue, the brand new boilerplate repo for starting off!

This tutorial is inspired by a video by Laracasts that sets up the basic app architecture. You don’t have to watch it, but here’s a link if you’re curious: https://laracasts.com/series/learning-vuejs/episodes/9

To sum up the result of the video, you have two directories and one main file: resources/assets/js/components/ , resources/assets/js/views/ , and resources/assets/js/app.js. In the components folder you have two files for each component: Component.js and component.template.html . Component.js is the model while component.template.html is the view. You do the same for views/, with about.js and about.template.html . You export all of this using module.exports to package the functioanlity. If you use coffeescript, name them Component.coffee and view.coffee. Simple stuff.

In app.js, you include all of these into the components object of your main View-Model. The views are named like ‘name-view’ and the components like ‘name’. You also have a currentView variable in the data object, which represents the view that should be displayed. To display all that, you use .

Alright, now that you’ve finished that, you should have a great architecture to start off with. You have your main app.js file (which will now be called app), individual views, and components. However, there’s a few things that are missing:

  1. a standard way to communicate from app -> view
  2. a standard way to communicate from view -> app
  3. a standard way to communicate from components -> view
  4. a standard way to communicate from components -> app

There’s a few important things missing from basic Vue and Laravel integrations.

Why would we want these features? Suppose we want to change the currentView from within a view. Right now, there’s no easy way to do that. Using my setup, we can just run this.app.currentView = ‘awesome-view’; .

Another example is if you want to have one user object that can be accessed application-wide. Just add it to the data object of your main app and it can be accessed from views using this.app.user !

To make components easier to understand, let’s think of vue components just like classes in PHP: each view and component is like a class that is then instantiated into objects through ’s or a .

Think of Vue components just like classes in PHP.

A quick note on components: when I say it sometimes I mean a component in general, other times I mean non-view, new DOM element components. Like the ones in the components/ folder. It should be pretty clear when I’m talking about which one, contact me if you have any ways I can make it more clear.

Now that we understand the basics of components, let’s try and get a (even) better architecture for setting them up and addressing the problems above. Let’s think about Laravel: there’s a main IoC container, which is accessed through the App facade, which contains various smaller classes called Service Containers. This is starting to sound really similiar to the front end: we have a main app that contains our smaller components & views (which are really just glorified components).

In Laravel, each service provider can access the IoC container through $this->app. What if we could do the same thing in views, saying this.app? Well it turns out you can!

In Laravel, each service provider has direct access the IoC container, containing all the other services, why can’t we do the same thing in Vue?

With this technique, the app variable is passed easily into each view using two-way binding. This means you can change the master application from a view, and the changes are reflected application-wide on the app view-model itself, every view, and every component.

Part 1: using this.app in each view

Quick Note: In coffeescript, saying @var and this.var results in the same code. I prefer the first way, but you don’t have to.

First, let’s go to the main HTML file that contains the <component is=“{{ currentView}}”>. We need to add the app as a prop to that. So, change that to <component is=“{{currentView}}” app=“{{@ app }}”> .

If you’ve used Vue before you’ll be familiar with the mustache syntax, but the {{@ might leave you a little confused. Basically, it says pass the app into the component, but if the component makes a change to the app, reflect it in the scope that contains the component too. In other words it enforces two-way binding. If you didn’t have the @, you could do something to the app in the view and then nothing would happen anywhere else!

Of course, if you are in a .blade.php file, change that to <component is=“@{{currentView}}” app=“@{{@ app }}”> so Laravel doesn’t get confused and try to look for a currentView and app constant.

Also, in order to make things run better, you may want to add keep-alive to the end of the tag. It basically keeps the current view alive in the background instead of deleting it, so if you change currentView back to it Vue doesn’t have to do more work. However, if you want to get updated data anytime the user changes views, it would be very easy to just remove keep-alive so that the ready method is recalled on the component.

Next, we need to make sure that app is added a valid props array for each view. In the module.exports array, go add an array called props if it does not already exist. Next, add a string called ‘app’ to it. Here’s an example:

gistfile1.js

module.exports = {
    props: [
        'app'
    ],
    template: require('./analytics.template.html')
};

Finally, we need to configure our app.js file to have an app variable that points to itself. Add app: {} to its data object. Then, add a created method to the app. In it, say this.app = this; . You’re done! You now have access to the master app in each view.

Why a created method, instead of a ready method, you may ask? Because created is called before the DOM is evaluated, meaning Vue doesn’t even know the components have been instantiated yet. What this all amounts to is that the app variable is ready the moment the views are!

If you need access to the master app in a component, you need to pass it in as a prop. In the component’s module.exports, add ‘app’ to the props array. Then, just like before, whenever you instantiate the component using app=“{{@ app }}” . You can use this for any component.

Part 2: setting data for a particular view from other views or the main app

Since components are only loaded if you have component is set to them at one point in the app’s lifecycle, you can’t reliably access them directly from this.app. Instead, you can set data for them in this.app then pull it in the views. This is easier than it sounds! In app.js, add a new object to the data object, and call it viewData. In it add a name of each view you want to set data for and point it to an empty object. Example:

gistfile1.js

data: {
	currentView: 'edit-view',
	app: {},
	viewData: {
		'edit-view': {},
		'about-view': {}
	}
}

To set that data from a view, just go this.app.viewData[‘about-view’][‘foo’] = ‘bar’; . Then, to get it from the about-view, just go this.app.viewData[‘about-view’][‘foo’]. You could also shorten that by going into the ready method and adding: this.external = this.app.viewData[‘about-view’]. Be sure to add external as an empty object to the data function. Now you can just say this.external.foo to access it. Example:

gistfile1.js

module.exports = {
    data: function() {
        return {
            external: {}
        };
    },

    props: [
        'app'
    ],

    template: require('./analytics.template.html'),

    ready: function() {
        this.external = this.app.viewData['analytics-view'];

        //Access variable "downloads" that has been set in about-view
        alert(this.external.downloads);

        //Access the user variable from the main app
        alert(this.app.user.name);
    }
};

This part really is just an example of taking advantage of the global access to the app’s data, nothing more. So take it with a grain of salt, because the real power is in Part 1 and 3.

Part 3: calling view functions

This is undoubtedly the most involved yet most useful part of this tutorial. For example, if I have a search bar in my navbar component, yet want to call a function in the search-results view, there is no good way to do it until now.

We need to be able to call functions if a view has been instantiated and if it hasn’t been. If it has been, we can just say viewModelfunctionName. If it hasn’t, doing so will result in an error. So we need to account for both situations…

Let’s add an setting to each view’s data in app.js called ready. Make it to default to false. Then, in the ready() function in each view, say this.external.ready = true; . Next, add an array called funcs_to_call . If the view has not been instantiated yet, we can add the name of the function to the array so the view can call it later in its ready() method.

To call the function on another view, we can say t_his.app.call(‘search-view’, ‘search’);_ This will call search-view.search() . If the view is instantiated, we can just call it directly. Otherwise, we have a little bit of a harder job.

Let’s deal with the first scenario first. Each view in viewData we need to have ready, funcs_to_call, and model. In each view’s ready() function, we need to set ready to true and model to this. Example:

gistfile1.js

ready: function() {
    this.app.viewData['analytics-view'].ready = true;
    this.app.viewData['analytics-view'].model = this;
}

This allows us to call the functions easily. In app.js, lets add a new method to the methods object called call(). Look at the gist below to see how it works:

gistfile1.js

methods: {
    call: function(view, name) {
    	if(this.viewData[view].ready == true) {
    		this.viewData[view].model[name]();
    	}
    }	
}

Basically, it just tests if the view is ready. If it is, then it calls it using array syntax magic. Let’s do a test of our own: go to analytics-view.js and replace it to the following code:

gistfile1.js

module.exports = {
    data: function() {
        return {
            external: {}
        };
    },

    props: [
        'app'
    ],

    template: require('./analytics.template.html'),

    ready: function() {
        
        this.app.viewData['analytics-view'].ready = true;
        this.app.viewData['analytics-view'].model = this;

        this.app.call('analytics-view', 'al');
    },

    methods: {
        al: function() {
            alert('i got called!');
        }
    }
};

When you load the page, it should instantly pop up with ‘i got called!’ as soon as the view is set in currentView!

Now we need to figure out what to do if ready is set to false. First, we just need to add the method name to the array, right? Add an else statement to the call method in app.js to push the function name onto an array:

gistfile1.js

else {
	this.viewData[view].funcs_to_call.push(name);
}

Now we just need to check on ready() functions if there’s anything on the funcs_to_call array and call them if they exist. Add this to the end of the ready() function:

gistfile1.js

var methods = this.app.viewData[view].funcs_to_call;
for(var i in methods)
{
    this[methods[i]]();
}

Now, when the view is loaded, it runs the method that was called!!!

Closing

To close it up, here’s the two files we’ve been working on, completed and done.

gistfile1.html

...
<body id="mainApp">
	<component is="@{{ currentView }}" app="@{{@ app }}" keep-alive></component>
</body>
...
var Vue = require('vue');

var app = new Vue({
	el: 'body#mainApp',

	data: {
		currentView: 'loading-view',
		app: {},
		viewData: {
			'edit-view': {
				ready: false,
				funcs_to_call: [],
				model: {}
			},
			'analytics-view': {
				ready: false,
				funcs_to_call: [],
				model: {}
			}
		}
	},

	components: {
	    // Views
		'analytics-view': require('./views/analytics'),
		'edit-view': require('./views/edit'),
		
		// Components
		'piece': require('./components/Piece')
	},

	methods: {
		call: function(view, name) {
			if(this.viewData[view].ready == true) {
				this.viewData[view].model[name]();
			} else {
				this.viewData[view].funcs_to_call.push(name);
			}
		}	
	},

	ready: function() {
	this.app = this;
		
        this.app.call('analytics-view', 'al'); //Test caling the al function
	}
});

module.exports = {
    data: function() {
        return {
            external: {}
        };
    },

    props: [
        'app'
    ],

    template: require('./analytics.template.html'),

    ready: function() {
        var view = 'analytics-view';
        this.app.viewData[view].ready = true;
        this.app.viewData[view].model = this;

        var methods = this.app.viewData[view].funcs_to_call;
        for(var i in methods)
        {
            this[methods[i]]();
        }
    },

    methods: {
        al: function() {
            alert('i got called!');
        }
    }
};

Completed file structure. Here my view’s name is home instead of analytics, and I chose to use coffeescript instead of vanilla javascript, but the same principles apply.

You can use these two files as templates when using this design architecture in the future! I know I will.

To the left is the file structure I have for the Laravue repo. There are a couple of differences though: 1) I have a home view not an analytics view, and 2) I use coffeescript instead of vanilla JS. These aren’t biggies though!

What now?

In the future, I’ll no doubt be optimizing this and making it better. If you have any suggestions, just leave a response below! Your input is essential because the only way this can thrive is through community involvement.

The only way this can thrive is through community involvement.

In the future I may make a way to combine the ready and model variables in viewData functionally into one. But the additional code required may be more than I actually save, so I’m not sure on this one.

Something else I feel like this needs is a CLI. How about maybe php artisan make:view analytics and it will add all this boilerplate? Just an idea. Let me know if you’re interested in doing anything like this, because I think might be a bit beyond me.

For more info on how calling functions on objects using a string check out http://stackoverflow.com/questions/9854995/javascript-dynamically-invoke-object-method-from-string . It helped me out allot.

UPDATE: allow for arguments passed into view methods!

// app.js call method.
call: function(view, name, args = []) {
	if(this.viewData[view].ready == true) {
		this.viewData[view].model[name](args);
	} else {
		this.viewData[view].funcs_to_call.push({name: name, args: args});
	}
}

// updated for loop inside each view
for(var i in methods)
{
    this[methods[i].name](methods[i].args);
}

Now you can do this.app.call(‘foo’, ‘bar’, [‘arg1’, ‘arg2’, ‘arg3’]) and it will pass the array into the method. To retrieve them in a method just do args[0], args[1], args[2].

If you’re using coffeescript, it’s even easier! You can just use the … syntax for arguments. Yes, it’s technically possible without coffee, but it would look pretty ugly… Anyways here’s the coffeescript example:

call: (view, funcName, args...) ->
	if   @views[view].ready then @views[view]['model'][funcName].apply(null, args)
	else @views[view].funcs.push({name: funcName, args: args}) 

Any input is highly welcome!

30s ad

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

Build A Web App with VueJS, Spring Framework and MongoDB

Vue JS 2.0 - Mastering Web Apps

Vue JS 2: From Beginner to Professional (includes Vuex)

The Ultimate Vue JS 2 Developers Course

Suggest:

JavaScript Programming Tutorial Full Course for Beginners

Laravel Tutorial - Abusing Laravel

Web Development Tutorial - JavaScript, HTML, CSS

Learn JavaScript - Become a Zero to Hero

Javascript Project Tutorial: Budget App

E-Commerce JavaScript Tutorial - Shopping Cart from Scratch