Model, Vue, View Model

Hello there
Obi Wan Kenobi Hello GIF By Star Wars via Giphy

It’s been a while, and honestly for good reason. I’ve been super busy working on my masters, dealing with work, and working on some side gigs. Needless to say, life has been hectic. Work is sort of the reason why I’m here writing today.

I’m not going to go into all the details; however, there’s a library that I’ve been working with. This library tightly couples the View and the ViewModel. This library has nigh immutable state, using its state management library. This library has moderate issues with testing due to its focus on functional components as the stylistic standard. None of these things are bad… I’m lying they’re terrible.

This got me pondering. Is there a way to have a highly decouple, two-way data-bound, master class in front-end software architecture?

Let Me Think
Looking Let Me Think GIF By TipsyElves.Com via Giphy

Of course, there is, or else I wouldn’t be writing this post. I mean, you read the title, you have to be knowing at least a little bit where we’re going with this. If you don’t know what MVVM is or the difference between MVVM and MVC here’s a link: pachow.

A Brief Summary of Where We’re Going and Why

A summary is that the MVVM breaks apart the View (the, in this case, HTML) and the ViewModel (where we handle events and other code) into different concerns. By making our code more modular we increase its maintainability, its resiliency, and its dependability (through testing). The Model in this section is your application’s state AND your application’s backend DB and code; however, we’ll touch on that MUCH later. Like another post entirely. Maybe I’ll finish that full-stack tutorial with a different stack.

We’re doing this because, as I said above, it makes your code: easier to read, easier to test, easier to maintain, etc. But now I’m going to explain why.

You Shouldn’t Be Testing The “View” Anyway

That’s what I said. Yeah. Since I started in this profession I’ve found that a lot of people are testing the actual mark up itself. They’re checking to see when a button is clicked if it does something. And I know, some people think that you should test it, but honestly, the button doesn’t do anything.

Think about it. If you didn’t have an event tied to that button what would it do? Nothing. So why are you testing the button and not just the event tied to it? The event is testable, does do something, and does have testable results. So… yeah. If you have any questions on that feel free to shoot me a message or something I guess.

Bottomline. UI elements don’t have inherent functionality and shouldn’t be tested. It makes sense to break them away from the code that does things. Stop coupling your View and View Model. Cool? Cool.

Leaving James Harden
Leaving James Harden GIF By Farmers Insurance via Giphy

By Separating the View and View Model One Can Change without The Other

It’s a rather long title to explain the following. So let’s say we have MyViewModel and MyComponent. The Component has a text field and a button. Right? Our initial ViewModel uses a GetCount() for the text field and an Increment() for the button. However, we can change this without having to change the View. We could, without changing the View change the GetCount() to return a string and Increment() to randomly change the string.

Here’s an example of what I mean

Example of Count version

<template>
    <div>
        <p>{{viewModel.GetTextFieldValue()}}</p>
        <input type="button" value="Click Me" @click="viewModel.ButtonOneClicked()" />
    </div>
</template>

<script lang="ts">
import {Vue, Component, InjectReactive} from "vue-property-decorators";
import IMyViewModel from "./path/to/my/view/model";

@Component
export default MyComponent extends Vue
{
    @InjectReactive("MyViewModel") viewModel!: IMyviewModel;
}
export default class MyViewModel implements IMyViewModel
{
    private _count: number = 0;
    public GetTextFieldValue(): number
    {
        return this._count;
    }
 
    public ButtonOneClicked(): void
    {
        this._count++
    }
}

In the above, the View Model is providing the View values and methods that it has NO IDEA ABOUT. This is good, this means it’s decoupled. As of right now, all the View knows is that it’s displaying the value and handling a button click. That View is displaying a count.

We can change this simply by changing the View Model and that’s it. We don’t have to touch the View at all. Simply by changing the following, we change the entire functionality of the small application entirely.

Change to View Model for RandomString Implementation

export default class MyViewModel implements IMyViewModel 
{ 
    private _randomString = ""; 

    public GetTextFieldValue(): number 
    { 
        return this.randomString; 
    } 

    public ButtonOneClicked(): void 
    {
        this._randomString = GenerateRandomString();
    } 

    private GenerateRandomString(): string
    {
        // code to generate a random string
        // that's not what this article is about
        // look it up
    }
}

Simply by changing the above the entirety of the component changes, we never touched the HTML. This is great. Having modular code is a good thing. Please understand that if there’s a bigger change, that you’re likely going to have to change both files; however, if you keep things generic, keep your components clean and tested, all should go well.

I’m Sure There Are More Benefits…

There has to be more that I’m missing at this time. Think about it this way. You can put all your components into a components folder. Those components can share the same Model Interface with different implementations. The View files would be just the view, broken out by their function. I really don’t know why more people aren’t doing this… the benefits man… the benefits…

Always Sunny
Always Sunny Fx GIF via Giphy

Let’s Make A Thing

Alright, with all of that out of the way, I’m going to guide you through doing this architecture. If I just gave you some paragraphs and examples it wouldn’t be very helpful to you. Especially if you’ve never thought about this kind of thing. Which isn’t a “you” thing. I think it’s JavaScript.

So let’s look at a diagram really of what we’re going to be doing:

Diagram of Our App Architecture
Diagram of our Architecture

Okay, in the above you can see the following. One the client is interacting with the View. Everything in there is the View. There is no TypeScript code, save for the injects and provides, in any of those files. The View is bound to the ViewModel. This is the part of the code that does… well… ‘the thing’. Finally, we have Vuex, which is our Model.

If this looks simple that’s because, well, it is.

Setting Up That Environment

Listen, I’m lazy. No, you don’t understand, really lazy. So we’re going to be using a TON of third-party dependencies. We’re gonna use Vuetify because I don’t wanna deal with the flex-grid myself. We’re going to be using vuex-class-modules, because… they’re amazing, and some other stuff down the line. When I hit you with that, “but wait, there’s more”.

So let’s go ahead and just install all the dependencies that we’re gon’ need. Open your command line, navigate to the directory with your coding projects, and type:

vue create my-mvvm-app

Okay if you don’t know what this is and you don’t have this command ‘vue‘. You should go install it here. Once you have that you should be able to do the above. You’re going to pick the following choices:

First, pick your settings manually. I’m not going to give you a code example of this. It’s on your screen. It has the word manually in it. You got this.

At this point, you should see some options. Please choose the following:

? Please pick a preset: Manually select features
? Check the features needed for your project: 
 (*) Choose Vue version
 (*) Babel
 (*) TypeScript
 ( ) Progressive Web App (PWA) Support        
 (*) Router
 (*) Vuex
 ( ) CSS Pre-processors
 (*) Linter / Formatter
>(*) Unit Testing
 ( ) E2E Testing

Press enter… Next, you’ll be presented with a set of questions. Just answer them the same way I did below. I’m going to explain a lot of this in a minute.

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 2.x
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save   
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N)

Okay hit enter again, and we’ll talk while it’s building your boilerplate project… So the above options are for the boilerplate. These options are as follows. You’ve picked your base options: Vue version, Babel, TypeScript, Router, Vuex, etc. The second set of options is to specify things within that.

We’re choosing Vue 2.x because Vue 3 is really new. Not a lot of things support it yet. This can mean problems with some of our code moving forward if we can’t trust that a dependency is going to support Vue 3. So… we erred on the side of safety.

The next bit on the choices is about class-style components. Absolutely yes. Do you have any idea how much I hate that JSON style object of the other style component? It’s so gross. Trust me, you’ll like this better.

We’re not using history mode because we don’t have a backend server for this to configure to use history mode. We just don’t need it. I’m too lazy to write it out. There’s a whole thing on the vue-router documentation. Have fun.

Finally, because the type of linter you use doesn’t matter, we’re going to be using Jest instead of Mocha. This is just a personal preference thing. I like Jest because it’s easy to configure, has built-in HTML coverage collectors, and more. It’s a great tool. Check it out.

Is it done yet? Cool. We’re not. Next, go back to your command line and type:

vue add vuetify

You’re gonna be prompted with some options again, just pick the default ones. I’m too lazy to pick through the components we actually need. Wait for that to be done. We’re using Vuetify because it’s my favorite. If you have another material component library you like, you do you. No judgment here.

When that’s done we’re gonna do one final step and do some npm installs. These dependencies are going to be a big part of what we do later on and not necessarily important now; however, I don’t wanna re-type this when I have so much else to retype later. So… tough?

npm install reflect-metadata inversify-props vuex-class-modules --save

Again, most of the above are going to be used later. One; however, will be used sooner rather than later. vuex-class-modules is a way for us to use a Vuex module as a class style instead of the normal JSON notation. This will act as our Model for the project.

Clean up, Clean up…

Clean Up
Clean Up GIF By Originals via Giphy

Okay, so the boilerplate comes with a ton of crap that we don’t need. So we’re going to tear through it rather quickly and move on to the next step? Cool? Got it. Take the following files and replace the contents below them:

Oh, and as a note, we’re not going to be doing TDD for this. This is a tutorial, not production code. If you use this to write production code, test it; however, I’ll be showing you at the bottom how to test some of the trickier bits.

App. vue

<template>
  <v-app>
    <v-main>
      <router-view />
    </v-main>
  </v-app>
</template>

<script lang="ts">
import {Vue, Component, ProvideReactive} from "vue-property-decorator";

export default class App extends Vue
{
  
}
</script>

Home. vue

<template>
  <v-container>
  </v-container>
</template>

<script lang="ts">
import {Vue, Component} from "vue-property-decorator";

@Component
export default class Home extends Vue
{

}
</script>

About. vue

<template>
  <div class="about">
    <router-link to="/">Home</router-link>
  </div>
</template>

HelloWorld.vue

Black Ink
Black Ink Crew Spring Cleaning GIF By VH1 via Giphy

You don’t need this, remove it. We’ll come back around. We’ll replace it with something else later.

If you run this, it won’t render anything. This, by itself, isn’t terrible. Let’s move on and start doing a little bit more coding. Now we’re going to set up the structure of the app.

Building The MVVM App…

Let’s start by creating our View component for this example. Go into your src/components folder and create a file named ‘MyComponent.vue’. This is where we’re going to be doing the MVVM pattern in earnest.

MyComponent.vue

<template>
    <v-container>
        <v-row>
            <v-col cols="6"><p>{{myViewModel.GetTextFieldValue()}}</p></v-col>
            <v-col cols="6">
                <v-btn round @click="myViewModel.HandleClick()">Click Me</v-btn>
            </v-col>
        </v-row>
    </v-container>
</template>

<script lang="ts">
import {Vue, Component, InjectReactive} from "vue-property-decorator";
import IMyViewModel from "../ViewModels/MyViewModel/IMyViewModel";

@Component
export default class MyComponent extends Vue
{
    @InjectReactive("MyViewModel") private myViewModel!: IMyViewModel;
}
</script>

In the code above the ‘script‘ tag is literally only binding the ViewModel to the View. We’re not actually doing anything special here other than binding. Vue allows for two-way binding between the ViewModel and the View in this way.

Now we need to create out Interface and Concrete for the ViewModel. This is going to be put into a folder in your src with the following: src/ViewModels/MyViewModel. Place both of the following files in this directory.

IMyViewModel.ts

export default interface IMyViewModel
{
    GetTextFieldValue(): number;
    HandleClick(): void;
}

MyViewModel.ts

import IMyViewModel from "./IMyViewModel";
import {Count} from "../../store/CountModule";

export default class MyViewModel implements IMyViewModel
{
    public GetTextFieldValue(): number
    {
        return Count.Value;
    }

    public HandleClick(): void
    {
        Count.Increment();
    }
    
}

This is gonna error because you don’t have {Count}. So we have to make that. This is easy too. Inside of your src/store directory create the file “CountModule.ts

CountModule.ts

import {VuexModule, Module, Mutation} from "vuex-class-modules";
import store from "./index";

@Module
class CountModule extends VuexModule
{
    private _count: number = 0;

    public get Value(): number
    {
        return this._count;
    }

    @Mutation
    public Increment(): void 
    {
        this._count++;
    }
}

export const Count = new CountModule({store, name: "Count"});

Whew okay, we’re almost there. We only have a bit more to do before we’ll actually see something in the UI. This next step is fun. Next, we’re going to put MyComponent into the Home.vue and then we’re going to Provide the ViewModel from the App.vue.

Home.vue

<template>
  <v-container>
    <MyComponent />
  </v-container>
</template>

<script lang="ts">
import {Vue, Component} from "vue-property-decorator";
import MyComponent from "../components/MyComponent.vue"

@Component({components: {MyComponent}})
export default class Home extends Vue
{

}
</script>

App.vue

<template>
  <v-app>
    <v-main>
      <router-view />
    </v-main>
  </v-app>
</template>

<script lang="ts">
import {Vue, Component, ProvideReactive} from "vue-property-decorator";
import IMyViewModel from "./ViewModels/MyViewModel/IMyViewModel";
import MyViewModel from "./ViewModels/MyViewModel/MyViewModel";

@Component
export default class App extends Vue
{
  @ProvideReactive("MyViewModel") myViewModel: IMyViewModel = new MyViewModel();
}
</script>

If you run this, you’ll see the following:

Screen so far
Here’s the screen you should see at your localhost

BUT WAIT…

But Wait, There's MORE!

Guess what we can go even farther than just that. With the tools that we added in the past steps, we can leverage dependency injection to make sure that we never have to instantiate helper services, factories, etc ever again. Yeah, that’s right, we can make this EVEN CLEANER.

First, let’s create a new directory: src/Services/AddService. Inside that directory create two files: IAddService.ts and AddService.ts. Again, I never said this was a complicated app we’re making here. Anyway, add this code to the files respectively.

IAddService.ts

export default interface IAddService
{
    Add(x: number, y: number): void;
    GetValue(): number;
    IncrementByOne(): void;
}

AddService.ts

import IAddService from "./IAddService";
import {Count} from "../../store/CountModule";
import { injectable } from "inversify-props"
@injectable()
export default class AddService implements IAddService
{
    public Add(x: number, y: number): void
    {
        const num = parseInt(x.toString()) + parseInt(y.toString());
        Count.SetCount(num);
    }
 
    public GetValue(): number
    {
        return Count.Value;
    }
 
    public IncrementByOne(): void
    {
        Count.Increment();
    }
}

Cool! Now that we have these we’re gonna do a bit of refactoring to our ViewModel file. We also need to change up our Module file. so let’s go edit that too.

MyViewModel.ts

import IAddService from "@/Services/AddService/IAddService";
import { Inject } from "inversify-props";
import IMyViewModel from "./IMyViewModel";

export default class MyViewModel implements IMyViewModel
{
    @Inject("AddService") private _service!: IAddService;
    private firstNumber = 0;
    private secondNumber = 0;

    public GetTextFieldValue(): number
    {
        return this._service.GetValue();
    }

    public HandleClick(): void
    {
        this._service.IncrementByOne();
    }

    public AddTwoAndSetToCount(): void
    {
        this._service.Add(this.firstNumber, this.secondNumber);
    }

    public get FirstNumber(): number
    {
        return this.firstNumber;
    }

    public set FirstNumber(value: number)
    {
        this.firstNumber = value;
    }

    public get SecondNumber(): number
    {
        return this.secondNumber;
    }

    public set SecondNumber(value: number)
    {
        this.secondNumber = value;
    }
    
}

CountModule.ts

import {VuexModule, Module, Mutation} from "vuex-class-modules";
import store from "./index";

@Module
class CountModule extends VuexModule
{
    private _count = 0;

    public get Value(): number
    {
        return this._count;
    }

    @Mutation
    public SetCount(value: number): void
    {
        this._count = value;
    }

    @Mutation
    public Increment(): void 
    {
        this._count++;
    }
}

export const Count = new CountModule({store, name: "Count"});

As of right now, this code will ABSOLUTELY fail. There are no fields for the new numbers, nothing for the void method to add two numbers. Before we make those fields we need to make the container and then put the container in the main.ts.

So go ahead and create a file in the src root called app.container.ts. And write the following.

app.container.ts

import { container } from "inversify-props";
import AddService from "./Services/AddService/AddService";
import IAddService from "./Services/AddService/IAddService";

export default function buildDependencyInjectionContainer(): void {
    container.addTransient<IAddService>(AddService, "AddService");
}

Now add it to your main…

buildDependencyInjectionContainer()

All that’s left to do now is add the new fields to the Component. Let’s do that now with the following:

MyComponent.vue

<template>
    <v-container>
        <v-row>
            <v-col cols="12"><p>Add 1 to the current count</p></v-col>
            <v-col cols="6"><p>{{myViewModel.GetTextFieldValue()}}</p></v-col>
            <v-col cols="6">
                <v-btn @click="myViewModel.HandleClick()">Click Me</v-btn>
            </v-col>
            <v-col cols="12"><p>Or reset the value all together by adding two numbers</p></v-col>
            <v-col cols="4"><v-text-field type="number" v-model="myViewModel.FirstNumber"></v-text-field></v-col>
            <v-col cols="4"><p>+</p></v-col>
            <v-col cols="4"><v-text-field type="number" v-model="myViewModel.SecondNumber"></v-text-field></v-col>
            <v-col cols="12"><v-btn @click="myViewModel.AddTwoAndSetToCount()">Add Two Numbers</v-btn></v-col>
        </v-row>
    </v-container>
</template>

<script lang="ts">
import {Vue, Component, InjectReactive} from "vue-property-decorator";
import IMyViewModel from "../ViewModels/MyViewModel/IMyViewModel";

@Component
export default class MyComponent extends Vue
{
    @InjectReactive("MyViewModel") private myViewModel!: IMyViewModel;
}
</script>

Okay, there you go. It all works and should update the state as you go. MVVM Vue. But I’m not done yet. No, no. Time to show you how to test that private injected property, because, let’s be honest, that’s the only thing that seems like it’d be hard to test.

Fixing Our Bad Practice…

Property injection is typically VERY BAD. However, the cool thing about TypeScript is that at the end of the day, it’s just JavaScript. So, when you’re writing your unit test you should be able to do the following:

import IMyViewModel from "@/ViewModels/MyViewModel/IMyViewModel";
import MyViewModel from "@/ViewModels/MyViewModel/MyViewModel";
import myContainer from "./fixtures/test.container";

describe("MyViewModel", () => {
    let myViewModel: IMyViewModel;

    beforeEach(() => {
        myContainer()
        myViewModel = new MyViewModel();
    });

    test("It mocks the AddService", () => {
        expect(myViewModel.GetTextFieldValue()).toBe(1);
    });
     
});

If you see there, that…

myContainer()

That’s a test fixture container. You need to add it to test/unit/fixtures.

test.container.ts

import IAddService from "@/Services/AddService/IAddService";
import { container } from "inversify-props";
import AddService from "../__mocks__/AddService";

export default function myContainer(): void
{
    container.addTransient<IAddService>(AddService, "AddService");
}

In the code block above you can see that I can use mocks from my mock folder to give the container what I need. There’s likely a way to configure the container for each test too. I haven’t looked into it quite yet.

So go forth! Make clean code!

7
0

Related Posts