VueCommander

Vuejs application framework based on battle tested design patterns

Examples

Getting Started

The command pattern encapsulates a single unit of action. When you go to a restaurant and the server takes your order. Each slip of paper (what we'll call a command) contains specific instructions. You don't talk to the chef directly.

Once the chef receives your order, they may queue it up with the other orders. They'll read your order once they have finished their current ones. If the chef makes a mistake, they can easily undo that mistake by looking at the order and fixing it. The command pattern can easily undo. It can also queue up executions before making them.

In the same way that you wouldn't talk to the chef directly, your GUI should not access or run business logic directly.

With VueCommander your Vue component will dispatch notifications using a built in observer pattern. You'll name your events based on the command you'll be executing (not the UI interaction). e.g: hello.updatemessage is good, hello.messagebuttonclicked is bad.

With VueCommander you'll map your events to commands in your context.

context.js

import Vue from 'vue';
import VueCommander from 'vuecommander';
import {
  LikesCommand,
  RetweetsCommand,
  RepliesCommand
} from './CountCommand';

Vue.use(VueCommander);

export default new VueCommander.Context({
  'count.like': LikesCommand,
  'count.retweets': RetweetsCommand,
  'count.replies': RepliesCommand,
});
      

You'll import your context and it will be available to all components and child components.

app.js

import Vue from 'vue';
import context from './Context';
import Counter from './Counter.vue';

new Vue({
  el: '#app',
  context: context,
  render: h => h(Counter)
});
      

In your component, you'll dispatch an event when something happens.

Counter.vue (sample)

<template>
  ...
        <nav class="level is-mobile">
          <div class="level-left">
            <a class="level-item" aria-label="reply" @click="increaseReplies">
              <span class="icon is-small">
                <i class="fas fa-reply" aria-hidden="true"></i>
              </span>
            </a>
            <span class="level-item">{{replies}}</span>
            <a class="level-item" aria-label="retweet" @click="increaseRetweets">
              <span class="icon is-small">
                <i class="fas fa-retweet" aria-hidden="true"></i>
              </span>
            </a>
            <span class="level-item">{{retweets}}</span>
            <a class="level-item" aria-label="like" @click="increaseLikes">
              <span class="icon is-small">
                <i class="fas fa-heart" aria-hidden="true"></i>
              </span>
            </a>
            <span class="level-item">{{likes}}</span>
          </div>
        </nav>
  ...
</template>
<script>
import CountModel from './CountModel';
export default {
  data() {
    return CountModel;
  },
  methods: {
    increaseLikes() {
      this.$context.events.notify('count.like');
    },
    increaseRetweets() {
      this.$context.events.notify('count.retweets');
    },
    increaseReplies() {
      this.$context.events.notify('count.replies');
    }
  }
}
</script>
      
VueCommander will look through your context for that event. If you have mapped that event to a command, VueCommander will create a new instance of your command and run execute, which cannot take any parameters. Your command will also be instantiated with the event that was called so you can have the data related to event available within the command.

CountCommands.js

import CountModel from './CountModel';

export class LikesCommand {
  constructor(e) {
    this.event; // not needed here but `this.event.data` is the data from the event
  }

  execute() {
    CountModel.likes += 1;
  }
}
      

You'll create models which are singleton units of state.

CountModel.js

export default {
  likes: 0,
  retweets: 0,
  replies: 0,
}
      

Your models can be imported into your commands and you can modify them within your execute methods. Because Vuejs automatically tracks changes within objects you can set your model as part or all of your data attribute of your component.

Counter.vue (sample)

...
import CountModel from './CountModel';
export default {
  data() {
    return CountModel;
  },
...
      

Your component will no longer be tightly coupled to business logic.

Each execute call will be tracked in a history array. Because a command is a single unit of action, you can easily undo them. If you add a saveState method and a undo method to your commands, they can take care of undoing themselves. You'll write the code for saveState which will get called before execute. You'll also write the code to the undo method. Let's update our LikesCommand.

CountCommands

import CountModel from './CountModel';

export class LikesCommand {
  constructor() {
    this.backup;
  }

  execute() {
    CountModel.likes += 1;
  }

  saveState() {
    this.backup = CountModel.likes;
  }

  undo() {
    CountModel.likes = this.backup;
  }
}
      

Within your component you can now call `undo`. Although it's better to create a full undoCommand.

Counter.vue (sample)

methods: {
  increaseLikes() {
    this.$context.events.notify('count.like');
  },
  increaseRetweets() {
    this.$context.events.notify('count.retweets');
  },
  increaseReplies() {
    this.$context.events.notify('count.replies');
  },
  undo() {
    if(this.$context.history.length) {
      this.$context.history.pop().undo();
    }
  }
}
      

That's all for now. Go check out the examples for a few demo implementations of VueCommander. Also read up on the command and observer design patterns.