Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Reem Jaghlit
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Reem Jaghlit

Vue.js Up And Running By Callum Macrae

By Ben Nadel on

In the last couple of months, there have been a few high-profile surveys about the state of JavaScript in 2018. And, on the various web-development podcasts that have discussed the results of said surveys, one datum that has been brought up several times is the relative growth of Vue.js. While Vue.js doesn't garner the absolute numbers that Angular and React do, its rate of adoption seems to be out-pacing the other front-end frameworks. As such, I thought it would be a fun Christmas treat to step outside of my Angular zone-of-comfort and take a look at Vue.js. To this end, I bought the book, Vue.js Up And Running: Building Accessible and Performant Web Apps by Callum Macrae. I read the book over the course of three leisurely days. And, as someone who has never looked at Vue.js before, I found Macrae's book easy to follow and quite enjoyable.


 
 
 

 
Vue.js Up and Running by Callum Macrae, review by Ben Nadel. 
 
 
 

While this was my first real exposure to Vue.js, many of the concepts discussed in the book are built on top of concepts that should be familiar to Angular developers. In fact, Vue.js is often described as being either, "What Angular 2 should have been"; or, "The happy medium between Angular.js and React." As such, it's hard for me to know how a framework-agnostic reader would experience the book. I can only say that Macrae's exposition let me quickly map my existing Angular mental model onto the new Vue.js syntax.

The one notable absence from the book is the concept of Dependency-Injection. To me, Dependency-Injection (DI) is one of the keystones of Angular and Angular.js. So, when I didn't see it in the book, I assumed that Vue.js - like React - decided to forgo such functionality. It was only after I Googled for it that I found Dependency-Injection listed under "Handling Edge Cases" in the official Vue.js documentation.

It seems odd to me - as an Angular developer - that DI is documented as an "edge-case" and not as a core tenant. Since I am not familiar with the Vue.js community, I hesitate to extrapolate too much from this. But, it does make me wonder if dependency-injection is not the "idiomatic Vue.js solution" for Service discovery?

That said, I don't want to conflate my feelings about Vue.js with my feelings about Macrae's book. The book is solid! And, by the time I was done reading it, I honestly felt like I could start building a Vue.js application. So, I wanted to see if I could do exactly that using just the book as my guide.

Well, with the exception of the build process. In the book, Macrae does talk about using Webpack and the "vue-loader" to get it working. But, as is always my experience with a build process, it never works the first time. And, in fact, it took me about 5-hours (over 2-days) and lots of Googling to get a build process working.

Part of the problem was that I tried to get TypeScript working first. I absolutely love TypeScript; but, I ran into a bunch of issues getting it to work in conjunction with "Class based" components. It was only after I backed-out of TypeScript, and went with Babel.js and Vue.extend() components, that I finally got something working.

And, of course, I had to look-up Dependency-Injection.

But, those two topics aside, the rest of the following code is based completely on the teachings in the book. Or, more accurately, on my novice interpretation of the book. So please, if you take issue with the code - or if I say something that is completely wrong - don't let that reflect poorly on the book - that's just my unfrozen caveman lawyer brain.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

As a simple "hello world" exploration, I created a list of friends that you could Add to and Delete from, respectively. And, in order to test a few Vue.js features, I encapsulated two parts of the user interface inside nested components:

  • Friend Form
  • Friend

These two components are so-called "presentation components" that operate solely on inputs and outputs. All logic and data is defined in the App component, which uses Dependency-Injection to locate a FriendService which, in turn, provides a synchronous look-up for the initial friend data.

First, let's look at the "main.js" file - this is then entry-point into the Vue.js application. It renders the root component into the document Body; and, it defines the name-based dependencies for subsequent dependency-injection:

  • // Import for side effects - we have to import this first so that the polyfills will
  • // be available for the rest of the code.
  • // --
  • // NOTE: I would normally include this as an Entry bundle; but, I couldn't get the
  • // HtmlWebpackPlugin to work properly if I did that (since I don't think it could
  • // implicitly determine the dependency order). In the future, I might be able to make
  • // this more dynamic (ie, use Webpack's import() syntax).
  • import "./main.polyfill";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // Import application classes.
  • import AppComponent from "./app.component.vue";
  • import { FriendService } from "./friend.service";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • new Vue({
  • el: "my-app",
  •  
  • // I setup the dependency-injection for the descendant components.
  • provide() {
  •  
  • return({
  • friendService: new FriendService()
  • });
  •  
  • },
  •  
  • // I render the root component of the application into the DOM.
  • render: ( createElement ) => {
  •  
  • return( createElement( AppComponent ) );
  •  
  • }
  • });

While Vue.js provides dependency-injection, Vue.js doesn't appear to manage the instantiation of injectables. As such, I am explicitly instantiating the FriendService class and making is available as "friendService" to my nested components.

The first component to be rendered is the AppComponent. The AppComponent manages the actual Friend data and renders the intake form and the list of Friend instances. The two nested components - FriendFormComponent and FriendComponent - are registered locally to the component. Meaning, they are only matched against the AppComponent's template, not the application at large.

  • <style scoped src="./app.component.less" />
  •  
  • <template>
  •  
  • <div class="app">
  •  
  • <h2 class="title">
  • You have {{ friends.length }} friend(s)!
  • </h2>
  •  
  • <FriendFormComponent
  • placeholder="Name..."
  • @add="addFriend( $event )">
  • </FriendFormComponent>
  •  
  • <!-- BEGIN: Friends List. -->
  • <template v-if="friends.length">
  •  
  • <ul class="items">
  • <li
  • v-for="friend in friends"
  • :key="friend.id"
  • class="items__item">
  •  
  • <FriendComponent
  • :friend="friend"
  • @delete="deleteFriend( $event )"
  • class="friend">
  • </FriendComponent>
  •  
  • </li>
  • </ul>
  •  
  • </template>
  •  
  • <template v-else>
  •  
  • <p class="no-data">
  • <em>Get your ass to Mars!</em>
  • </p>
  •  
  • </template>
  • <!-- END: Friends List. -->
  •  
  • </div>
  •  
  • </template>
  •  
  • <script>
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // Import application classes.
  • import FriendComponent from "./friend.component.vue";
  • import FriendFormComponent from "./friend-form.component.vue";
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • export default Vue.extend({
  • inject: [
  • "friendService"
  • ],
  • components: {
  • FriendComponent,
  • FriendFormComponent
  • },
  • data() {
  •  
  • return({
  • friends: []
  • });
  •  
  • },
  • // I get called once after the component has been created and the props and
  • // dependencies have been wired together.
  • created() {
  •  
  • this.friends = this.friendService.getFriends( "Sarah", "Kim" );
  •  
  • },
  • methods: {
  • // I add a new friend with the given name.
  • addFriend( name ) {
  •  
  • this.friends.push({
  • id: Date.now(),
  • name: name
  • });
  •  
  • },
  •  
  • // I delete the given friend.
  • deleteFriend( friend ) {
  •  
  • var index = this.friends.indexOf( friend );
  •  
  • if ( index >= 0 ) {
  •  
  • this.friends.splice( index, 1 );
  •  
  • }
  •  
  • }
  • }
  • });
  •  
  • </script>

As you can see, while I am using the "Single File Component" approach, I am saving my CSS (or, LESS CSS in this case) as an external file. It still compiles down to a single file when we build the application. But, since I don't think that the CSS adds much to a demo, I try to tuck it away when possible. This is also how I write much of my Angular code.

In fact, part of why it took me so long to get a "build process" working is because I spent a lot of time trying (and failing) to figure out how to stuff the Template and the Style tag(s) into the "@Component()" meta-data provided by the "vue-class-component" decorator.

One of the things that I love about Angular is how unopinionated it is. In an Angular component, you can put the Template and Styles anywhere you want: keep them in the one class file; or, break them out into multiple files. Vue.js is much more opinionated in this matter (or so it would seem to a novice developer).

ASIDE: One thing that is really cool about Vue.js is that the scoped-CSS appears to be defined at compile time. This allows the CSS to be extracted into an external CSS file.

Now, one of the very React-like features of Vue.js is the fact that each Component renders its own host element. Meaning, when I put "<FriendFormComponent/>" in my template, the rendered page doesn't include a "<FriendFormComponent>" element. Instead, the FriendFormComponent is responsible for rendering its own HTML host element (which is a "<form>" tag in this case).

This is a feature that rubs me the wrong way. At first, it feels like this approach provides more flexibility; and, it does. But, once you start to use it, this flexibility makes it harder to reason about what the page is actually doing (and about why it may not be rendering what you expect).

To see this, let's look at the FriendFormComponent which renders a "form" tag as its host tag:

  • <style scoped src="./friend-form.component.less" />
  •  
  • <template>
  •  
  • <form @submit.prevent="processForm()" class="form">
  • <input
  • type="text"
  • v-model.trim="name"
  • :placeholder="placeholder"
  • class="input"
  • />
  •  
  • <button type="submit" class="submit">
  • Add Friend
  • </button>
  • </form>
  •  
  • </template>
  •  
  • <script>
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • export default Vue.extend({
  • props: [
  • "placeholder"
  • ],
  • data() {
  •  
  • return({
  • name: ""
  • });
  •  
  • },
  • methods: {
  • // I emit the "add" event and clear the form.
  • processForm() {
  •  
  • if ( this.name ) {
  •  
  • this.$emit( "add", this.name );
  • this.name = "";
  •  
  • }
  •  
  • }
  • }
  • });
  •  
  • </script>

As you can see, this component renders a "form" tag as its host element. It then uses props and events to communicate with the AppComponent.

Similarly, the FriendComponent is also driven by props and events:

  • <style scoped src="./friend.component.less" />
  •  
  • <template>
  •  
  • <div class="friend">
  • <div class="name">
  • {{ friend.name }}
  • </div>
  •  
  • <div class="actions">
  • <a @click="handleDelete()" class="action">( delete )</a>
  • </div>
  • </div>
  •  
  • </template>
  •  
  • <script>
  •  
  • // Import core classes.
  • import Vue from "vue";
  •  
  • // ------------------------------------------------------------------------------- //
  • // ------------------------------------------------------------------------------- //
  •  
  • export default Vue.extend({
  • props: [
  • "friend"
  • ],
  • methods: {
  • handleDelete() {
  •  
  • this.$emit( "delete", this.friend );
  •  
  • }
  • }
  • });
  •  
  • </script>

As you can see, these components are fairly simple. And, if we run this code in the browser, add a few friends and delete a few friends, we get the following output:


 
 
 

 
 Vue.js hello world with webpack 
 
 
 

The total page-weight of this bundled and minified Vue.js application comes in at about 180Kb. This is less than half the size of a comparable Angular application - at least, with my fairly naive Webpack skills.

And, speaking of Webpack, here's my Webpack config, which I copied from my Angular project and then refactored for Vue.js:

  • // Load the core node modules.
  • var CleanWebpackPlugin = require( "clean-webpack-plugin" );
  • var HtmlWebpackPlugin = require( "html-webpack-plugin" );
  • var MiniCssExtractPlugin = require( "mini-css-extract-plugin" );
  • var path = require( "path" );
  • var VueLoaderPlugin = require( "vue-loader/lib/plugin" );
  • var webpack = require( "webpack" );
  •  
  • // We are exporting a Function instead of a configuration object so that we can
  • // dynamically define the configuration object based on the execution mode.
  • module.exports = ( env, argv ) => {
  •  
  • var isDevelopmentMode = ( argv.mode === "development" );
  •  
  • // Locally, we want robust source-maps. However, in production, we want something
  • // that can help with debugging without giving away all of the source-code. This
  • // production setting will give us proper file-names and line-numbers for debugging;
  • // but, without actually providing any code content.
  • var devtool = isDevelopmentMode
  • ? "eval-source-map"
  • : "nosources-source-map"
  • ;
  •  
  • // By default, each module is identified based on Webpack's internal ordering. This
  • // can cause issues for cache-busting and long-term browser caching as a localized
  • // change can create a rippling effect on module identifiers. As such, we want to
  • // identify modules based on a name that is order-independent. Both of the following
  • // plugins do roughly the same thing; only, the one in development provides a longer
  • // and more clear ID.
  • var moduleIdentifierPlugin = isDevelopmentMode
  • ? new webpack.NamedModulesPlugin()
  • : new webpack.HashedModuleIdsPlugin()
  • ;
  •  
  • // Locally, Vue will dyanmically inject a Style tag for each type of mounted
  • // component. This makes it easier to understand how the page is currently working.
  • // However, in production, it will be more efficient if we just extract the CSS and
  • // link it as a single external CSS file.
  • var vueStyleLoader = isDevelopmentMode
  • ? "vue-style-loader"
  • : MiniCssExtractPlugin.loader
  • ;
  •  
  • return({
  • // I define the base-bundles that will be generated.
  • // --
  • // NOTE: There is no explicit "vendor" bundle. With Webpack 4, that level of
  • // separation is handled by default. You just include your entry bundle and
  • // Webpack's splitChunks optimization DEFAULTS will automatically separate out
  • // modules that are in the "node_modules" folder.
  • entry: {
  • main: "./app/main.js"
  • // NOTE: I'm currently including the polyfill directly in the main.ts file.
  • // If I have it as an Entry, I get a "cyclic dependency" error since I had to
  • // ALSO change my "chunksSortMode" to "none" in order to get Lazy Loading
  • // modules to work.
  • // --
  • // polyfill: "./app/main.polyfill.ts",
  • },
  • // I define the bundle file-name scheme.
  • output: {
  • filename: "[name].[contenthash].js",
  • path: path.join( __dirname, "build" ),
  • publicPath: "build/"
  • },
  • devtool: devtool,
  • resolve: {
  • extensions: [ ".vue", ".js" ],
  • alias: {
  • "~/app": path.resolve( __dirname, "app" ),
  • "vue$": "vue/dist/vue.esm.js"
  • }
  • },
  • module: {
  • rules: [
  • {
  • test: /.vue$/,
  • loader: "vue-loader"
  • },
  • {
  • test: /\.js$/,
  • loader: "babel-loader",
  • exclude: /node_modules/
  • },
  • {
  • test: /\.css$/,
  • loaders: [
  • vueStyleLoader,
  • "css-loader"
  • ]
  • },
  • {
  • test: /\.less$/,
  • loaders: [
  • vueStyleLoader,
  • "css-loader",
  • "less-loader"
  • ]
  • }
  • ]
  • },
  • plugins: [
  • // I facilitate the vue-loader functionality.
  • new VueLoaderPlugin(),
  •  
  • // I clean the build directory before each build.
  • new CleanWebpackPlugin([
  • path.join( __dirname, "build/*.css" ),
  • path.join( __dirname, "build/*.css.map" ),
  • path.join( __dirname, "build/*.js" ),
  • path.join( __dirname, "build/*.js.map" )
  • ]),
  •  
  • // I generate the main "index" file and inject Script tags for the files emitted
  • // by the compilation process.
  • new HtmlWebpackPlugin({
  • // Notice that we are saving the index UP ONE DIRECTORY, so that it is output
  • // in the root of the demo.
  • filename: "../index.htm",
  • template: "./app/main.htm",
  • // CAUTION: I had to switch this to "none" when using Lazy Loading
  • // modules otherwise I was getting a "Cyclic dependency" error in the
  • // Toposort module in this plug-in. As a side-effect of this, I had to
  • // start including the Polyfill file directly in the main.ts (as opposed
  • // to including it as an entry point).
  • // --
  • // Read More: https://github.com/jantimon/html-webpack-plugin/issues/870
  • chunksSortMode: "none"
  • }),
  •  
  • // I extract the Vue CSS into an external file (if loader is enabled).
  • new MiniCssExtractPlugin({
  • filename: "[name].[contenthash].css"
  • }),
  •  
  • // I facilitate better caching for generated bundles.
  • moduleIdentifierPlugin
  • ],
  • optimization: {
  • splitChunks: {
  • // Apply optimizations to all chunks, even initial ones (not just the
  • // ones that are lazy-loaded).
  • chunks: "all"
  • },
  • // I pull the Webpack runtime out into its own bundle file so that the
  • // contentHash of each subsequent bundle will remain the same as long as the
  • // source code of said bundles remain the same.
  • runtimeChunk: "single"
  • }
  • });
  •  
  • };

To be clear - again - I suck at Webpack; but, this seems to compile and do the things.

I have no doubt that I made a lot of mistakes in this code. And, that I made decisions that would make a seasoned Vue.js developer cringe. But, considering that Vue.js Up And Running by Callum Macrae was my first exposure to Vue.js, I'm pretty excited that I got this far!

Tweet This Great article by @BenNadel - Vue.js Up And Running By Callum Macrae Woot woot — you rock the party that rocks the body!


Reader Comments

@All,

After reading this book, one of the Vue.js features that caught my eye was the "Event modifiers". For example, having click.self and click.once. I wanted to see if I could build this same concept into Angular:

https://www.bennadel.com/blog/3554-creating-a-vue-js-inspired-event-modifier-dom-plug-in-in-angular-7-1-4.htm

Since Angular uses a plug-in system for its DOM-events, this is actually a fairly straightforward -- but hella fun -- exercise.

Reply to this Comment

I had just been comparing Vue, React and Angular to decide which one I should learn. But with 10 years of jQuery experience it's not yet clear if Vue is a replacement for all the CSS and DOM manipulation that jQuery does so well. I think I need to understand more before buying a book on a specific framework. Sooo much to research!
Thanks for continuing to blog, Ben, always been a keen reader of yours.

Reply to this Comment

@Gary,

Personally, I still use jQuery on my traditional request/response type websites (where I render a page, make a request, and render a new page). Really, I'd only use a framework like Vue or Angular if I was creating a single-page application.

Of course, I'll caveat that I know nothing about server-side rendering with these frameworks; so, if you can server-side render, then your outlook might change. But, server-side rendering is still very confusing for me to even think about.

So, for things like this blog (as an example), I just use CSS and jQuery to do all the things. And it works fairly well.

Reply to this Comment

Happy New Year, Ben! Just a few quick observations:

First, you rag on your webpack-fu, but I'm totally cribbing your use of an arrow function in defining your module.exports in your webpack config so (YOINK!) thanks for that! (Also, props on using webpack, it's pretty sweet!)

Second, the Angular/JS take on dependency injection always bugged the dickens out of me as unnecessarily confusing, so when I switched to Vue & started writing all my components as self-contained modules I naturally found myself gravitating away from that. I'm glad you posted an example of Vue's provide/inject here, but it feels overly complicated. Maybe it's because I've just started exploring Vuex that it appears to me to be a better solution -- you define a Vuex.Store, attach the state (whatever libraries) to it and then attach it to your root Vue instance, and from that point on whatever you attached to your Vuex.Store is available to all child components off your Vue instance. Plus, you get a clear interface within Vuex for managing state mutations, time-machine debugging, etc. Vuex might arguably be too much solution for smaller use-cases and I was hesitant to mention this at all, but then I saw Vue's own guidance on this (https://vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection), specifically the block about the "downsides to dependency injection".

At any rate, thanks for the Vue.js book review!

Reply to this Comment

@Justin,

So, without dependency-injection, how do you locate the services that do things like interact with localStorage or make API calls to the remote server? Or, are you saying that is all handles at "actions" on the Vuex store? So, all of those would be actions you dispatch, and then the Vuex hooks deal with the external interactions and update the state?

To be fair, this is also the confusion that I have in React as well. I don't know how those devs get their services :D

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.