Contents

Implementing Model-View-Presenter Pattern With Vue

Background

For the past few weeks, my major task has been refactoring the old step-wise drafting process of our app into a much more simplified one. Instead of dividing the drafting and submission process into several steps like Drafting, Tagging, Feedback and Finalize, the goal is to have one unified interface where the features previously scattered across different steps can be integrated in one UI by a plug-in manner, similar to how VS Code plugins add new functionalities to the Activity Bar and Side Bar.

/implementing-mvp-pattern-with-vue/a-new-drafting-process.svg
New Drafting Process Design

The benefits are obvious: No duplicate event handlers implemented for shared components used across different pages; No need to care about updating data to the backend before navigating to other steps; The app feels more like an SPA and less like web pages; The new UI feels much more intuitive to new users, etc.

The Migration

Dynamic Component

Based on the requirements, it is pretty clear that we want to load the feature components in a dynamic fashion into the left and right panel. Vue provides support for this OOB via dynamic components.

Using dynamic components is easy. You only need to bind the component definition or name to Vue’s <component> using the is attribute:

1
2
3
<template>
  <component :is="currentRightComponent"></component>
</template>

Cool! Let’s try to dynamically bind the ExampleView to a <component> in right panel. Wait a minute… What about the props and event bindings?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
  <example-view
    :example="example"
    :visible="true"
    @next="showNextExample"
    @previous="showPreviousExample"
    @save="favoriteExample"
    @focus="hideTag"
  />
</template>

Lucky for us, all the Vue directives work in the dynamic <component> as well. So in a similar fashion, you can also pass props and bind event handlers to a dynamic component like so:

1
2
3
4
5
6
7
8
<template>
  <component
    v-if="currentRightComponent"
    :is="currentRightComponent"
    v-bind="currentRightComponentProps"
    v-on="currentRightComponentEvents"
  ></component>
</template>

Here currentRightComponentProps and currentRightComponentEvents are both local data properties. For a feature component ExampleView, these two values could look something like these:

1
2
3
4
5
6
7
exampleViewComponentProps() {
  const localExample = this.example;
  return {
    example: localExample,
    visible: true,
  };
}
1
2
3
4
5
6
7
8
exampleViewComponentEvents() {
  return {
      next: this.showNextExample,
      previous: this.showPreviousExample,
      save: this.favoriteExample,
      focus: this.hideTag,
  };
}

And with a click event handler on the corresponding item within the right sidenav that will trigger the dynamic loading, we have something like this (semi-pseudo code):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<template>
  <div class="draft-container">
    <div class="left-panel">
      <!-- Left Panel -->
    </div>
    <div class="editor-view">
      <!-- Editor View -->
    </div>
    <div class="right-panel">
      <drafting-side-bar class="sidebar">
        <img
          class="icon"
          src="/static/images/icons/examples.png"
          alt=""
          @click="loadRightComponent('ExampleView')"
        />
      </drafting-side-bar>
      <keep-alive>
        <component
          v-if="currentRightComponent"
          :is="currentRightComponent"
          v-bind="currentRightComponentProps"
          v-on="currentRightComponentEvents"
        ></component>
      </keep-alive>
    </div>
  </div>
</template>

<script>
import ExampleView from "shared/components/example-view/ExampleView.vue";

export default {
  data(){
    return {
      currentRightComponent: null,
      example:null,
    }
  }
  computed: {
    exampleViewComponentProps() {
      const localExample = this.example;
      return {
        example: localExample,
        visible: true,
      };
    },
    exampleViewComponentEvents() {
      return {
        next: this.showNextExample,
        previous: this.showPreviousExample,
        save: this.favoriteExample,
        focus: this.hideTag,
      };
    },
  },
  methods: {
    loadRightComponent(componentName) {
      if (componentName === this.currentRightComponent?.name) {
        this.currentRightComponent = null;
      } else {
        switch (componentName) {
          case "ExampleView":
            this.currentRightComponent = ExampleView;
            this.currentRightComponentProps = this.exampleViewComponentProps;
            this.currentRightComponentEvents = this.exampleViewComponentEvents;
            break;
          default:
            break;
        }
      }
    },
    showNextExample(){/*...*/},
    showPreviousExample(){/*...*/},
    favoriteExample(){/*...*/},
    hideTag(){/*...*/},
  },
};
</script>

Nice! We have a functional dynamic component setup and ExampleView works as expected! ✨ However, as you might have noticed we have introduced a big problem here. 🔍

That’s right! All these ExampleView’s implementation details (data, computed properties, event handlers, etc.) are actually living inside the Drafting component. Not only does it violate the single responsibility principle, but it will also hurt maintainability and extensibility down the road. As more “plugin” features are added to this interface, we can expect a 1000-line Vue component in the not-too-distant future. 😱 We definitely need a better structure to organize this, and that brings us to the meat of this blog post the Model-View-Presenter pattern.

MVP (Model-View-Presenter) Pattern

What is MVP pattern? In short, it’s a pattern derived from MVC and serve as a structure to better maintain separation of concern. There are a few layers:

  • Model - Where the data sources lives
  • View - Where the UI rendering happens
  • Presenter - Where the complex domain/UI logic lives

In the case of Vue (or any frontend framework), View takes care of rendering HTML elements and event handling, presenter takes care of data transformation according to domain or UI logic, and model takes care of providing and persisting data via various I/O options. Therefore, we can roughly map it to the following concepts in Vue:

  • Model ➡️ Smart (Container) Components w/ Vuex + Axios
  • View ➡️ Dumb (Presentational) Components
  • Presenter ➡️ Composables

There are some key points that worth emphasizing:

  • Only container components have access to Vuex store and various services to make API calls.
  • Presentational components have no idea of what other parts of the app are doing. They receive data by props and emit events. The only logic they have access to are presenters.
  • Presenters in this context are no just some random Vue composables. There should be no direct access to the store via something like useStore. They should not be making API calls either. In other words, they should be “dumb” as well.

If the guidelines above are violated, you are likely to end up with some components that violate Single Responsibility Principle (SRP) and hence, are less testable and maintainable.

Applying MVP to Drafting.vue

It might be easier to understand the benefits by seeing MVP in action. Following the above guideline, we can now refactor our Drafting component as such:

 1<template>
 2  <div class="draft-container">
 3    <div class="left-panel">
 4      <!-- Left Panel -->
 5    </div>
 6    <div class="editor-view">
 7      <!-- Editor View -->
 8    </div>
 9    <div class="right-panel">
10      <drafting-side-bar class="sidebar">
11        <img
12          class="icon"
13          src="/static/images/icons/examples.png"
14          alt=""
15          @click="loadRightComponent('ExampleView')"
16        />
17      </drafting-side-bar>
18      <keep-alive>
19        <!-- No more props and event binding on the dynamic component-->
20        <component
21          v-if="currentRightComponent"
22          :is="currentRightComponent"
23        ></component>
24      </keep-alive>
25    </div>
26  </div>
27</template>
28
29<script>
30export default {
31  data(){
32    return {
33      currentRightComponent: null,
34    }
35  }
36  // No more example-related data, computed properties and methods
37  methods: {
38    loadRightComponent(componentName) {
39      if (componentName === this.currentRightComponent?.name) {
40        this.currentRightComponent = null;
41      } else {
42        switch (componentName) {
43          case "ExampleView":
44            this.currentRightComponent = ExampleView;
45            break;
46          default:
47            break;
48        }
49      }
50    },
51  },
52};
53</script>

By taking a look at the larger picture, we can visualize the overall architecture in this diagram:

/implementing-mvp-pattern-with-vue/mvp-architecture-for-simplified-drafting-interface.svg
Simplified Drafting Interface - MVP Architecture

Essentially, we are turning the top-level Drafting component into a container component for three container components. It hosts or dynamically loads other container components (e.g. ExampleView) but does not concern itself with their implementation details.

Naming Convention
You might have noticed the XXXView pattern here. Yes, that’s the naming convention chosen to indicate container components. I picked it for our project but you can choose your own.

The container components, like ExampleView, can connect to Vuex store, make API calls, provide data and event handlers for the child presentational components. In simple cases, the presentational components render to HTML and that’s it. However, sometimes there can be complex business or UI logic and again, you should want extract it and separate it from the Vue framework whenever possible. There’s more than one way to skin a cat but the simplest solution nowadays would be a Vue composable via Composition API. You can also extract it into a library/module that you can extend and improve in the future.

An example of such presenter is the QuillEditor class from an internal editor library that empowers the QuillEditor Vue component, which in turn empowers the EmailEditor component. Inside this QuillEditor component, there is an line for editor initialization:

1
this.editor = new QuillEditor(initConfig);

where the definition of class QuillEditor looks something like this:

1
2
3
4
5
export class QuillEditor
  extends TypedEmitter<QuillEditorEvents>
  implements CoreEditor, TaggableExtension, HighlightableExtension {
  /* implementation */
}

As you can see, we extended the default rich text editor with tagging and colored highlighting feature, and such domain logic and UI behavior are nicely encapsulated inside this QuillEditor class.

So what are the benefits? There are, IMHO, quite a few:

  • Better code organization and separation of concern. You know the role of each component so when you need to add a new feature, you know exactly what type of logic should go where. The same also applies to debugging.
  • Looser coupling. Take the mentioned QuillEditor library for example: It’s loosely coupled with the main app. If we want to add new functionalities or even swap the underlying implementation (e.g. Quill ➡️ TipTap) in the future, we can simple refactor this presenter without worrying breaking other parts of the app as long as it implements those interfaces correctly.
  • Better testability. E2E/Integration testing can be brittle and hard to set up (I am not saying you should never perform them but it can be tricky!). By having a clear separation between container and presentational components, it would be much easier to test a subset of “dumb” components. Even better, by extracting the critical UI and business logic into composables and libraries, you can test the critical part more efficiently without involving a large UI framework that’s Vue.
  • Better maintainability. It is now easier to reason about your code, easier to test and get critical logic right, easier to locate certain code and easier to refactor old features and add new ones. Maintainability 🆙

Conclusion

There you have it! An example of how to refactor a complex component and apply MVP along the way to make our code better. In case you want to learn more about this pattern, there are a few articles that might interest you (despite being Angular-focused, the ideas introduced should be framework-agnostic):

Hope this post has been helpful and see you in the next one!