Intro

This is the fourth installment in the series. In the first chapter, we implemented core business logic and rules with Entities. The second post was dedicated to setting them into motion with Services. In the previous installment, we build a bridge between Vue/Vuex infrastructure and core logic by setting up the Store: Actions and Mutations, to work with Services.

In this post, we are going to wrap everything up. We will create Vue components and teach them to get/save data from/to the Store.

Requirements Analysis

Photo by Isaac Smith on Unsplash

Even though we did a lot of work, we didn’t finish the stories yet. Let’s remind ourselves what Users expect from the App:

As a User, I should be able to see all the Articles on the Home page

As a User, I should be able to navigate from there to a page that represents one particular Article and see full Article

As a User, I should be able to leave a comment on this page

Let me transform these stories to the technical tasks:

create and setup UI for Home Page

create and setup UI for a list of Articles on the Home page

create and setup UI for Full Article Page

create and setup UI for Comment form on the Full Article Page

Setup

The repo and a the demo are at your service. Feel free to switch to the “store” branch that contains completed previous tutorials.

In this tutorial, we will focus mostly on /src/ui folder.

You can start the App by running:

npm run serve

And open it in the browser (by default, the link is http://localhost:8080, see details in your terminal). You should see a welcoming Home Page:

You can verify that tests are set up correctly by running

npm t

The homepage is partially already setup. You can find the View for the router in the

/src/ui/pages

Article Component

First, we should create a component that can render short information about the Article on the list. Create these files following the same old structure we did in the previous chapters:

/src/ui/components/article/article.tsx

/src/ui/components/article/article.spec.ts

/src/ui/components/article/article.types.ts

/src/ui/components/article/index.ts

And re-export everything within barrel file:

Barrel files

Tests first!

As in any other chapter, we will follow the TDD approach here. I will start with defining a boilerplate setup for the test:

Article spec

and set up an empty component:

Empty Article Component

Now it is time to make tests a bit more meaningful. I am going to use Vuetify’s card component to render an Article. I also expect that Article data to be passed as a prop to the component. And finally, I want to make sure that component renders at least title, a short text, and a link to the full Article:

Article Spec checks render

Note, how effectively we use Entity mock! We don’t have to know about all the structure of it. We care only about the title, id, and short content: precisely what we are going to use in the component. In other words, we decoupled the component test from Entity structure.

Component

The test is falling now, which is what we were after. Let’s fill it the component with the necessary logic. We determined that Article Component requires Article entity to work correctly. So, let’s define an interface for the props to set up a compile-time check:

Article Types

And utilize it in the component itself, plus secure a runtime-check:

Article Component props set up

Note: Please refer to my post about combining JSX and interfaces for details.

Now nothing stops us from rendering the Article:

Render Article

Note ‘this: ArticleComponent’ in the render argument list. TS may not know at the compile time the context of the execution of the function. This context is defined when a function is actually called. But if we know for sure the context, we can provide “this” argument to help TypeScript analyze the code. In this case, we are confident that render is called in the context of a component.

One little piece that is missing is URL to the full Article page. I will define a computed property for it, mostly to give an example of how it can be done with this setup:

getUrl computed prop

At this point, the test should pass. Great job!

Home Page

We’ve been working for a while without seeing any results on the screen. The time has come to change it.

Navigate to /src/ui/pages/home/home.tsx

We need the data: all available Articles. If you recall, this data should be deposited in the Store. After we injected the Storage into the Vue, it’s effortless to access it by simply calling ‘this.$storage.’ Also, thanks to TypeScript and the clear API we built with Storage, we don’t have to guess what actions/getters/mutations the Store supports and what payload expects. We can simply grab the data like this:

Get these Articles!

Now we can render Article Component for every Article found in the Store:

Render List of Articles

And finally, load the data:

Load Articles when Component mounts

If you run the App and open the browser, you should see Home Page filled with Articles:

Homepage renders Articles

But the test is falling. The reason is that the mounted component has no clue about $storage. It is fairly simple to fix that:

Mocking the Storage

The test is still falling, but the reason is different. Now, ‘getAllArticles’ is just a mock function of the mocked storage and returns ‘undefined’. It should return an array of Articles instead:

Mocking getters

At this point, your App should compile without errors, and all tests should pass. Victory!

Full Article Component

Ok, so we’re halfway through. The next stop is the Full Article Component. This Vue element has to handle three major things:

render Article (title and full description at least)

render a list of Comments for this Article

render comment form and process its submission

I will start by defining regular placeholder-files:

/src/ui/components/full/full.spec.ts

/src/ui/components/full/full.tsx

/src/ui/components/full/full.types.ts

/src/ui/components/full/index.ts

and setting up barrel files:

Barrel files

Tests first!

As always! The spec placeholder should be familiar:

Test placeholder

Usually, the primary purpose of the UI component is to display something. Full Article component should expect Article Entity as a prop and render the title and full content using Vuetify’s card:

Testing render

Then, the component should render a list of Comments:

Testing rendered List of Comments

Note: I ensured that mocked Article has at least two comments by manually pushing in their mocks

And finally, the form. According to Comment Entity, there are three required fields: Title, Author, and Content. It means the form must have these three inputs.

And of course, when a user submits the form, the component should dispatch “createComment” action, sending form data and Article’s id:

Testing Form

Component

Feels like test covers all we need. We can focus on the actual Component now. As we established earlier, it expects Article as a prop:

Full Component and Types

Code now compiles, but tests predictably fail. No worries, we are on our way to make them pass!

Let’s render the Article and the list of Comments first:

Render Comment’s List

Tests are getting better! But before we implement the form and wrap up this component, let’s take a little detour and display what we have already done on the screen. To do that, we need a new route and the View.

Full Article Page

Navigate to /src/ui/pages directory and start by defining familiar placeholders:

/src/ui/pages/article/article.tsx

/src/ui/pages/article/article.spec.ts

/src/ui/pages/article/index.ts

with updated barrel files:

Barrel files

Now, all this page has to do is to get Article by id, using router params, and then render Full Article component. The test is simple:

Full Article Page test

And here is the Component itself:

Ful Article Page

And now let’s attach this page to the router:

Set up router

Code should compile without the errors, and if you open the browser and click the Article preview on the home page, you should be navigated to the full Article page:

Great job!

Comment Form

The final piece of the puzzle is the comment form. This form must ensure the user provides proper data by performing validation. However, as we discussed many times, a UI component must not control these rules, only utilize them. These rules come from Entities. You may recall COMMENTS_MAX_AUTHOR_LENGTH, COMMENTS_MAX_TITLE_LENGTH, etc., that we created in the very first post in the series. It is now time to use them:

Form: Validation rules

Note, we define the properties “titleRules”, “contentRules”, “authorRules” in the way suitable for Vuetify. If you use another UI library, the way you employ validation rules may be different. But core idea remains: UI doesn’t own the rules, only uses it.

It would be useful to preserve data provided by a user, so we can apply it later on when the form is submitted:

Form: preserving values

And submission itself is merely grabbing that data and dispatching the action:

Form: Submit Handler

Now we are ready to provide markup for the form:

Rendering form

Now, all tests should pass, code should compile without the errors. And if you open any article, you should see a fully-functioning form:

The final piece: Comment form

Conclusion

We completed our journey. Let’s stop for a second and appreciate the path we have passed.

Each element in the Dependency circle of our application has its responsibility. Entities contain core business logic, Services utilize and enforce them. The Store only holds the data and supports UI Components with global access to it. Lastly, Vue components handle the presentation of data and interaction with a User. Business rules are not dependant on Vue infrastructure. The code has good test coverage. And last but not least, it is easy to delegate the implementation of different parts of the system to different people/teams: one team can handle UI components while others focus on the core logic.

What’s next?

I’d urge you to play around the repo to understand the approach better. Try to change things and see what part of the architecture has been affected by that change. Try to add new functionality (like creating a post or removing the comment, for example?). And of course, if you have any feedback, I would greatly appreciate it.

Cheers!