Logo credits to vuejs.org and golang.org

Social application with Vue.js and GO

Create and serve a twitter like application with vue.js and golang PART 6: Forms and data in VUEX

Ivano Dalmasso
Published in
9 min readMar 15, 2021

--

This is the sixth part of this serie. Check here all the parts:

In this chapter we are going to finally create some forms so we can use the store previously created and insert/update there some data. Note that at the end of this part we’ll have an application that have a client side in-memory storage, where we can add and update values. Also, we won’t have a persistent data layer, yet: for that, we’ll have to wait some more lessons.

Main target here is to update the frontend project, you can find the code here.

Updates to the store

Before inserting the actual forms, let’s make some last updates to the post store so that can accept the insertion of posts and also comments.

As first, remove all the post objects in the list in the state, so it will start as an empty array. Then, update the ADD_POST mutation to also have some logic to populate the id for the post (remember: when there will be a working backend this won’t be used anymore), and also add a mutation for adding a comment to a post, like this:

ADD_POST(state, post) {
if (state.posts.length < 1) {
post.id = 1;
} else {
const max = state.posts.reduce((prev, current) =>
prev.id > current.id ? prev : current
);
post.id = max.id + 1;
}
post.comments = [];
state.posts.push(post);
},
ADD_COMMENT(state, { postId, comment }) {
const post = state.posts.find(post => post.id === postId);
if (post.comments.length < 1) {
comment.id = 1;
} else {
const max = post.comments.reduce((prev, current) =>
prev.id > current.id ? prev : current
);
comment.id = max.id + 1;
}
post.comments.push(comment);
}

The code here is pretty straight forward. It assigns an id to every post inserted, incremental. And the second mutation does the same for the comments. In the actions, before calling the ADD_POST mutation also insert the date of the post, and also create an action that can do the same with a comment and call the respective mutation:

async addPost(context, post) {
post.date = getFormattedDate();
context.commit("ADD_POST", post);
},
async addComment(context, { postId, comment })
{
comment.date = getFormattedDate();
context.commit("ADD_COMMENT", { postId, comment });
}

The “getFormattedDate” function is actually a utility function I created before in the same file, just to add a nicely formatted date to both the posts and the comments.

Update the posts views

Now that we have a store that can add posts and comments, we have to actually use these new functionalities in the components.

Let’s add a new component that could be used for inputting a single text, and that could also be reusable. Add a new file AddTextForm.vue in the src/components folder, and fill it with the following:

<template>
<form>
<label v-if="showLabel" :for="'text' + this._uid">
{{ textRequest }}</label>
<input
:id="'text' + this._uid"
type="text"
:placeholder="textRequest"
v-model="textValue"
/>
<button @click.prevent="submitted">submit</button>
</form>
</template>

<script>
export default {
data() {
return {
textValue: ""
};
},
props: {
textRequest: { type: String, default: "" },
showLabel: { type: Boolean, default: false }
},
methods: {
submitted() {
this.$emit("text-added", this.textValue);
this.textValue = "";
}
},
emits: ["text-added"]
};
</script>

<style scoped>
label {
padding-right: 1rem;
display: block;
}
button {
margin-top: 1rem;
width: 10%;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
background-color: darksalmon;
padding: 8px;
}
input {
box-sizing: border-box;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
width: 80%;
padding: 8px 20px;
}
input:focus {
outline: 0;
background-color: wheat;
}
</style>

Here everything should be almost clear: we used a pair of props, one to decide if show the label in the form, the other to decide the placeholder of the textbox to be shown.

The only new thing we have in this file is actually the use of the “v-model” directive, that is used in conjunction with input fields to connect the value actually in the input to a variable in the data of the component, in a reactive way.

At the clicked event of the button, the method called emits an event “text-added” to any parent component that use this one. Also, note that we implemented a “emits” configuration parameters, to let the parent know, this is not mandatory but a nice best practice to have.

This event emitting give us the second communication way between components in vue, the first one being the “props”, so we can send data from parent to child and the other way around.

With this, any component that use the AddTextForm will be able to listen to the event “text-added” and will get also the data inserted.

So, right now, let’s use this form to add some posts in the Posts.vue component. Update the template, like this

<div class="posts">
<h1>All Posts</h1>
<add-text-form
textRequest="Add Post"
v-if="loggedIn"
:showLabel="true"
@text-added="addPost"
></add-text-form>
<post-list :posts="posts" title="" />
</div>

So, it is all like before, but we added a little title and the new component. We show this component only if the user is logged in, we show a label “Add Post” and on the event “text-added” we call the addPost method. This is actually pretty easy:

methods: {
addPost(text) {
this.$store.dispatch("posts/addPost", {
user: this.$store.getters["auth/currentUser"].username,
post: text
});
}
}

So, this just dispatch the addPost post, passing the actual authenticated user username and the text post. This will be used from the action, and add the post to the actual store.

Next natural step is to use the same add-text-form to also add the comments to a post. This can be done in two steps. First we have to change the BaseCard template, so it can also have some “actions” slot, where we can insert some other code. This is easily done adding this after the footer:

<div class="actions">
<slot name="actions"></slot>
</div>

and then we can use this slot in the SinglePost component adding the following lines after the footer just before the closing tag of the BaseCard:

<template v-if="loggedIn" v-slot:actions>
<add-text-form
textRequest="Add comment"
:showLabel="false"
@text-added="text => addComment(text, post)">
</add-text-form>
</template>

Note that for this we have to add a mapGetter to the loggedIn getter of the store to render the button only if the user is actually logged in.

On the emission of the event “text-added”, it’s called a function addComment, that we have to add to the methods like this:

addComment(text, post) {
this.$store.dispatch("posts/addComment",
{
postId: post.id,
comment:
{
user: this.currentUser.username,
post: text
}
});

This will obviously add the comment to the store, and so every other page will be able to load it when needed.

Add a login page

Right by now the login button is only swapping the login status of the actual user from “logged in” to “not logged in”. In the future the user will have to actually request a login via a user/password form, so just now let’s create a login page that will do this. For now it’s going to be a placeholder, with only a “login” button, but it will be soon completed with the other functionalities.

In the view folders, create a Login.vue file with thw following:

<template>
<div class="login">
<button @click="loginButtonClicked">LOGIN</button>
</div>
</template>

<script>
import { mapActions } from "vuex";
export default {
methods: {
...mapActions({ login: "auth/login" }),
loginButtonClicked() {
this.login().then(() => {
this.$router.push({ name: "Posts" });
});
}
}
};
</script>

<style scoped>
button {
margin-top: 1rem;
width: 20rem;
height: 5rem;
border-radius: 8px;
background-color: darksalmon;
padding: 8px;
}
</style>

Nothing new here, only note the “router.push” instruction that will redirect the browser to the Posts view after the login store action has been succesfully completed.

To complete this step, we have only to change the application bar now, that must navigate to the login view when the login button is pressed. To do so, just change a little the login link, from a “a” tag to a router-link that goes to the Login view:

<router-link
class="app-bar-item"
href="#"
v-if="!loggedIn"
@click.prevent :to="{ name: 'Login' }"LOGIN >LOGIN</router-link>

Also, to complete this update, we can make sure that when a user logs out of the application goes to the login page automatically, attaching this new method to the logout link clicked event:

logoutButtonClicked() {
this.logout().then(() => {
this.$router.push({ name: "Login" });
});
}

Last thing to do, update the router file to add the new “Login” route, like this:

import Login from "../views/Login.vue";...
{
path: "/login",
name: "Login",
component: Login
}

Add some navigation guards on login status

We would like the user to not being able to go in some pages if the user is not logged in (for example, the User page). So, as first thing, we can go to the application bar component, and show only some links if the user is not enabled: in the template change the actually present v-for=”link in links” to v-for=”link in activeLinks”, and also add the activeLinks computed method as:

activeLinks() {
return this.links.filter(
link => link.visibleIfLoggedOut || this.loggedIn
);
}

With this the links shown are only the ones that can be visible if the user is logged out, or if the user is logged in. We should also add the property “visibleIfLoggedOut” to the links, like this:

links: [
{
visibleIfLoggedOut: true,
name: "Posts",
to: { name: "Posts" }
},
{
visibleIfLoggedOut: false,
name: "User",
to: {
name: "User",
params: {
userid: this.$store.getters["auth/currentUser"].username
}
}
}

This code actually hides the link of “User” if the user is logged out, but this is actually not all we need to do. In fact, if a user puts the url of the User page in the address bar of the browser, the page will be seen. We need to make sure that the router makes some kind of processing to decide if a user can or cannot view some pages, in this case.

This is actually done via navigation guards: these are some configuration methods that can be used to instruct the router about things to do before and after a navigation event has called.

These can be set at route or at global level, in this case for example we are going to set a navigation guard on the User route, telling that if the user is not actually logged it should instead be routed to the login page. In the same way, we would like that an already logged in user should not go to the login page, and instead be redirected, for example, to the posts page.

In the router file, update the two routes like this:

import store from "@/store/index.js";
...
{
path: "/user/:userid",
name: "User",
component: User,
props: true,
beforeEnter: (to, from, next) => {
if (!store.getters["auth/isLoggedIn"]) {
next({ name: "Login" });
} else {
next();
}
}
},
{
path: "/login",
name: "Login",
component: Login,
//This is not needed right by now, because the store is refreshed on page refresh... Will be needed!
beforeEnter: (to, from, next) => {
if (store.getters["auth/isLoggedIn"]) {
next({ name: "Posts" });
} else {
next();
}
}
}

The beforeEnter configuration is actually a function that has as input three parameters:

  1. to: the target Route Object being navigated to.
  2. from: the current route being navigated away from
  3. next: a function that resolve the navigation. When it is called, depending on the parameter passed to it the resolution will be different: if no parameters will be passed, the resolution will be the normal one, if we pass it a route object, this will be pushed in the history of the browser (in our case, we will go there, instead of the normal route).

So, in our case, we can have the behaviour we wanted before for these two routes. Obviously, nothing block a malicious user to force a request, if he really want, so always remember to duplicate all “security” controls on backend too!

Now, we have reached an almost stop point in the frontend, and so next step is going to start adding a go backend that will serve some api with the actual posts, and authentication features that we will need.

--

--

Ivano Dalmasso

Always looking to learn new things, and loving see things work as I want