Re-writing dcidr.org with Vue 2


I just finished re-writing my dcidr.org app again, this time with vue 2.

dcidr.org

It's my favorite pet project for learning new programming languages and libraries because:

I've re-built this app many different ways over the years: server-side web apps, command-line tools, mobile apps, spreadsheets, JS apps, etc. The latest generation was using Knockout circa 2014 so this time I decided to try out Vue with ES2015.

Why Vue?

Vue prides itself on having a path from simple use cases to more complex ones. All you really need to do in order to start using Vue is something like this:


<html>
    <body>
        <div id="app">{{ message }}</div>

        <script src="https://unpkg.com/vue"></script>
        <script>
            var app = new Vue({
                el: '#app',
                data: {
                    message: 'Hello Vue!'
                }
            });
        </script>
    </body>
</html>

When it's time to bring in a router or state management, you can use official plugins/libraries that work well with Vue or choose a 3rd party library. React and Knockout are same way (merely view libraries) and it's up to the developer to bring in whatever else they need. This is very valuable because sometimes I just need to add some basic interactivity to server-rendered pages and other times when I need a full-blown SPA.

Vue is very similar to React in that is uses a virtual DOM, is component-centric, and promotes unidirrectional data flow. The biggest difference is how it does data-binding and how templates work which we will dig into.

App Overview & Structure

The app has these key features:

I structured it like so:

dcidr.org diagram

Nothing innovative here! The central state manager has one Decision object. Components in the app trigger mutation events which the store listens for. The store synchronously updates its decision object and the updated state is passed down to all components who are listening.

The Router

I like delcarative routers and this one is very full-featured but my needs were simple. This is what my router looked like:


// imports omitted 
Vue.use(VueRouter);

const router = new VueRouter({
  routes : [
    { path: ROUTES.HOME, component: WelcomeComponent },
    { path: ROUTES.OPTIONS, component: OptionsComponent },
    { path: ROUTES.CRITERIA, component: CriteriaComponent },
    { path: ROUTES.OPTION_COMPARISONS, component: OptionComparisonListComponent },
    { path: ROUTES.CRITERIA_COMPARISONS, component: CriteriaComparisonListComponent },
    { path: ROUTES.REPORT, component: ReportComponent },
    { path: ROUTES.SAVE, component: SavePromptComponent },
    { path: ROUTES.ARCHIVE, component: ArchivedDecisionsComponent},
    { path: '*', redirect: '/' }  // default route
  ]
});

export default router;

My main app view model was just a container for the current route and the HTML for it was just like so:


<div id="app" v-cloak>
    <transition name="fade" mode="out-in" appear>
        <router-view></router-view>
    </transition> 
</div>

That <transition> element is vue's built-in transition support which I found to be extremely well-designed and documented. Simply wrap any element which would be dynamically added/removed via Vue (e.g. has a v-if binding) and vue will automatically provide support for CSS transitions and animations.

State Management

I probably could have found a way to re-write dcidr without central state management but my previous version used a message bus technique already so it was a natural fit. Vuex does state management very similarly to redux where components trigger synchronous mutations on a central state object which get propogated to listeners. Vuex provides "actions" which allows you do work with asynchronous operations, which I didn't need.

Vuex offers some helpful time-savers like getters which are a kind of shorthand for creating computed poperties based on your state.

Here's a sample of what my store looked like.


// imports omitted 

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    decision: null
  },
  mutations: {
    [MUTATIONS.NEW_DECISION] (state) {
      state.decision = new Decision();
    },
    [MUTATIONS.ADD_OPTION] (state, name) {
      state.decision.addOption(name);
    },
  },
  getters: {
    gates: state => {
      if(!state.decision) return null;
      return state.decision.gates;
    }
  }
});

export default store;

Data-Binding

Every property on your ViewModel you want to bind to must be defined in the .data property. Vue proxies these properties and allows you to optionally define some basic validations. You can also define .computed properties as functions. Here is an example:


const ViewModel = Vue.extend({
  data : {
    firstName: '',
    lastName: ''   
  },
  computed: {
      fullName: function(){
          return `${this.firstName} ${this.lastName}`;
      }
  }
});

Every property in .data is available on the root of the ViewModel object, as is every function in the .computed object. Notice how in the fullName() computed that it's referencing this.firstName instead of this.data.firstName.

This is very MVVM-ish if you ask me.

Vue components are slightly different in that .data is a factory function instead of an object and components can receive props (declarative bindings from parent components) which are treated as a one-way top-down data flow like in React.

Templates

View templates are essentially HTML which gets converted to a render function by the Vue engine (at design time or run time, your choice). You can write this HTML in whatever way works best for you. If your team wants to keep the component logic and templates in the same file, they can use single-file components or simply use string literals in the Vue component JS. If they want to keep the HTML separate from the JS, like me, they can use HTML references (el: '#app') or string literals sourced from html files and imported via webpack's html-loader.

Here are some quick examples of the Vue template syntax to get a taste.


<!-- handlebars are for text content, you can use filters in the expressions too -->
<div>{{ something | capitalize }}</div>

<!-- v-bind directive is for HTML attributes, shorthand notation is :href -->
<a v-bind.href="something">click me</a>

<!-- v-on directive is for events, shorthand notation is @click -->
<button v-on.click="myMethod">click me</button>

Each component you create becomes a "custom element" (e.g. <my-widget>). Components can also have one or more <slot> elements in their template for the purposes of content distribution, otherwise and infamously known in the Angular 1.x world as transclusion. Here is a quick example...


<!-- my-component template -->
<section>
    <slot>
</section>

<!-- my-app template -->
<my-component>
    <strong>Hello there!</strong>
</my-component>

<!-- rendered -->
<section>
    <strong>Hello there!</strong>
</section>

Tradeoffs & Conclusion

The key strengths for Vue IMO were:

The key downsides for me were:

Overall I enjoyed using Vue. I like some of the design choices (MVVM, component-based, "progressive", minimal lock-in) and the overall quality of the library. The dcidr code base is much better as a result and I'm confident I will understand it a year from now when I go to re-write it again. :)

[ Archive · Home ]