Step 10: Styling & Navigation

Want to put some lipstick on this pig? Let's do it. Most of this update is CSS styling. Since that is outside the scope of this tutorial, I'm going to magically tackle it in one commit without boring you with the details here. Skim through the diff, though, because we do move around and change some HTML elements that might cause things to break if you don't notice them.

13a1e37

One last piece here - we still need to style the navigation. I'm going to start by making the main navigation its own Vue.js component, but with static content. Nothing worth discussing yet, since you already know how to create a component.

bbffd79

My first instinct is this static approach isn't good enough. The navigation component has to know about the existence of every view we add to the application. And when we add a view we have to come back to the navigation to add a new link. We might also want to show the user's accounts in the main navigation bar along with their respective balances. We could load that information from the shared Vuex state, which ties the navigation closely with the accounts module. If I make a change to how the accounts are stored I have to remember to go change the navigation. Components should be discrete when possible - I should be able to make a change to accounts without having to update other parts of my code.

I would prefer to have a navigation object that the accounts module can register itself with. Accounts would then tell the navigation object it wants to show the list of accounts with their balances, and is responsible for keeping the information up to date. This sounds similar to how the vue-router works. We could make this an independent reusable plugin, with our own global object for navigation. The developer using it would simply have to override the CSS for the nav bar, and if they needed more flexibility they could write their own HTML template.

This sounds like a great little project. However... part of a developer's responsibility is identifying when a task is outside the scope of work and deciding whether to take the simple, quick route that works in the short term versus the more flexible option which has long term benefits at the cost of time upfront. This is especially true when you are a solo developer and there is no one else to constrain you. We're shooting for a minimum, usable application here. A more complex navigation component might be on the long term vision board, but right now it will add more overhead than we're prepared to deal with.

And don't use this responsility as an excuse to write bad, hacky code!

For now, we'll simply loop through the accounts and display each one with its balance. Since we need the accounts information on the navigation we'll load them when the component is mounted. (Which is just one example of the close coupling we wanted to avoid.)

// /src/app/navigation/components/Navigation.vue
<template>
  <div id="navigation-view">
    <ul>
      <li>
        <router-link :to="{ name: 'accountsList' }">Accounts</router-link>
        <ul>
          <li
            v-for="account in accounts"
          >
            <router-link :to="{ name: 'updateAccount', params: { accountId: account.id } }">
              {{ account.name }} <span>${{ account.balance }}</span>
            </router-link>
          </li>
        </ul>
      </li>
      <li><router-link :to="{ name: 'budgetsList' }">Budgets</router-link></li>
    </ul>
  </div>
</template>

<script>
import { mapActions, mapState } from 'vuex';

export default {
  name: 'navigation',

  mounted () {
    this.loadAccounts();
  },

  methods: {
    ...mapActions(['loadAccounts'])
  },

  computed: {
    ...mapState({
      accounts: state => state.accounts.accounts
    })
  }
};
</script>

<style scoped lang='scss'>
  ul {
    margin-top: 50px;

    li {
      border-bottom: 2px solid rgb(31, 78, 93);
      font-size: 1.8em;
      padding-left: 20px;
      margin: 18px 20px;

      a {
        color: #ffffff;
      }

      ul {
        margin-top: 20px;

        li {
          font-size: 0.6em;
          border: none;

          span {
            float: right;
          }
        }
      }
    }
  }
</style>

Snapshot of the application navigation

This unfortunately exposes a bug in CreateUpdateAccount.vue. If you navigate directly between editing accounts you will notice that the URL changes, but the selected account gets "stuck" on the first one you edited. (Hello functional testing!)

Take a few minutes to look through the code and see if you can spot the problem before continuing.

We load our selected account in the mounted method of the component. This is part of the Vue lifecycle. Vue.js is pretty smart though. When the user navigates between two different routes using the same component Vue doesn't actually destroy the component and create a new one. It tries to be efficient by reusing the same Vue instance, thus none of the lifecycle hooks are called and our mounted code never loads the new account.

We could monitor the URL for changes and trigger the account loading code any time the route's accountId parameter changes. This would certainly work and wouldn't take much work, but it doesn't solve the root problem, which is that our component is closely coupled with vue-router. The better solution is to use the props feature that vue-router gave us in its 2.2 release. This allows us to pass route params to the component as props. We will receive the accountId as a prop, which we can watch for changes to reload the account. That should trigger the update any time the URL changes.

Why is this decoupled, where using this.$route.params is not? Props are a part of the Vue component itself rather than a value injected when the component is loaded via the router. They can be sent from vue-router or passed in if we directly embedded this component in another. Our component is no longer dependent on the usage of vue-router.

We'll also move the loading code into its own method so we can call it when the component is first mounted and when the URL changes.

...

export default {
  name: 'accounts-create-edit-view',
  props: ['accountId'],

  data: () => {
    return {
      categories: CATEGORIES,
      editing: false,
      selectedAccount: {}
    };
  },

  mounted () {
    if (this.accountId) {
      this.loadAccount();
    }
  },

  methods: {
    ...mapActions([
      'createAccount',
      'updateAccount',
      'loadAccounts'
    ]),

    resetAndGo () {
      this.selectedAccount = {};
      this.$router.push({ name: 'accountsList' });
    },

    saveNewAccount () {
      this.createAccount(this.selectedAccount).then(() => {
        this.resetAndGo();
      });
    },

    saveAccount () {
      this.updateAccount(this.selectedAccount).then(() => {
        this.resetAndGo();
      });
    },

    processSave () {
      this.editing ? this.saveAccount() : this.saveNewAccount();
    },

    loadAccount () {
      let vm = this;
      this.loadAccounts().then(() => {
        let selectedAccount = vm.getAccountById(vm.accountId);
        if (selectedAccount) {
          vm.editing = true;
          vm.selectedAccount = Object.assign({}, selectedAccount);
        }
      // TODO: the object does not exist, how do we handle this scenario?
      });
    }
  },

  computed: {
    ...mapGetters([
      'getAccountById'
    ])
  },

  watch: {
    accountId (newId) {
      if (newId) {
        this.loadAccount();
      }
      this.editing = false;
      this.selectedAccount = {};
    }
  }
};

...
...

{
    path: '/accounts/:accountId/update',
    component: components.CreateUpdateAccount,
    name: 'updateAccount',
    props: true
  }
];

You may have considered setting the selectedAccount as a computed property which changes based on the accountId prop. That's some great thinking! However, Vue.js computed properties must be synchronous. Since we must call loadAccounts and wait on local storage to retrieve the data we can't use a computed property here.

Now we have a fully functional, fully styled navigation bar!

ae09000

Continue to step 11, Finishing Budgets with Vue.js Dynamic Components


Originally published on


Sign up to receive updates for new articles.
And spam. Definitely lots of spam.