Categories
Vue Ionic

Mobile Development with Ionic and Vue — Badge, Button, and Card

If we know how to create Vue web apps but want to develop mobile apps, we can use the Ionic framework.

In this article, we’ll look at how to get started with mobile development with the Ionic framework with Vue.

Badge

We can add a badge with Ionic Vue.

For example, we can use the ion-badge component to add a badge:

<template>
  <ion-page>
    <ion-content>
      <ion-badge color="primary">11</ion-badge>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonBadge, IonItem, IonLabel } from "@ionic/vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { IonBadge, IonItem, IonLabel },
});
</script>

We can also put them in an ion-item container:

<template>
  <ion-page>
    <ion-content>
      <ion-item>
        <ion-badge slot="start">11</ion-badge>
        <ion-label>My Item</ion-label>
        <ion-badge slot="end">22</ion-badge>
      </ion-item>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonBadge, IonItem, IonLabel } from "@ionic/vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { IonBadge, IonItem, IonLabel },
});
</script>

The slot prop sets the position.

start is left and end is right.

Button

We can add a button with the ion-button component.

For example, we can write:

<template>
  <ion-page>
    <ion-content>
      <ion-button color="secondary">Secondary</ion-button>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonButton } from '@ionic/vue';
import { defineComponent } from 'vue';

export default defineComponent({
  components: { IonButton }
});
</script>

to add it.

The color prop has the color of the button.

We can disable it with the disabled prop.

And we can set the size with the size prop. We can set it to default , large , or small .

Ripple Effect

We can add a ripple effect when we click on something with the ion-ripple-effect component.

For example, we can write:

<template>
  <ion-page>
    <ion-content>
      <button class="ion-activatable ripple-parent">
        A button with an unbounded ripple effect
        <ion-ripple-effect type="unbounded"></ion-ripple-effect>
      </button>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonApp, IonContent, IonRippleEffect } from "@ionic/vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { IonApp, IonContent, IonRippleEffect },
});
</script>

to add the ripple effect.

Cards

We can add cards with the ion-card component.

For example, we can write:

<template>
  <ion-card>
    <ion-item href="#" class="ion-activated">
      <ion-icon :icon="wifi" slot="start"></ion-icon>
      <ion-label>Card Link Item 1 activated</ion-label>
    </ion-item>

    <ion-item href="#">
      <ion-icon :icon="wine" slot="start"></ion-icon>
      <ion-label>Card Link Item 2</ion-label>
    </ion-item>

    <ion-item class="ion-activated">
      <ion-icon :icon="warning" slot="start"></ion-icon>
      <ion-label>Card Button Item 1 activated</ion-label>
    </ion-item>
  </ion-card>
</template>

<script>
import {
  IonCard,
  IonCardContent,
  IonCardSubtitle,
  IonCardTitle,
  IonIcon,
  IonItem,
  IonLabel,
} from "@ionic/vue";
import { pin, walk, warning, wifi, wine } from "ionicons/icons";
import { defineComponent } from "vue";

export default defineComponent({
  components: {
    IonCard,
    IonCardContent,
    IonCardSubtitle,
    IonCardTitle,
    IonIcon,
    IonItem,
    IonLabel,
  },
  setup() {
    return { pin, walk, warning, wifi, wine };
  },
});
</script>

to add the card with the items.

ion-card is the container for the card.

ion-icon has the icon.

ion-label has the label.

We can also use the ion-card-content component to add the card content:

<template>
  <ion-card>
    <ion-card-header>
      <ion-card-subtitle>Card Subtitle</ion-card-subtitle>
      <ion-card-title>Card Title</ion-card-title>
    </ion-card-header>

    <ion-card-content>
      Keep close to Nature's heart.
    </ion-card-content>
  </ion-card>
</template>

<script>
import {
  IonCard,
  IonCardContent,
  IonCardSubtitle,
  IonCardTitle,
  IonIcon,
  IonItem,
  IonLabel,
} from "@ionic/vue";
import { pin, walk, warning, wifi, wine } from "ionicons/icons";
import { defineComponent } from "vue";

export default defineComponent({
  components: {
    IonCard,
    IonCardContent,
    IonCardSubtitle,
    IonCardTitle,
    IonIcon,
    IonItem,
    IonLabel,
  },
  setup() {
    return { pin, walk, warning, wifi, wine };
  },
});
</script>

ion-card-title has the card’s title, and ion-card-subtitle has the card’s subtitle.

Conclusion

We can add badges, buttons, and cards with Ionic Vue.

Categories
Vue Ionic

Mobile Development with Ionic and Vue — Action Sheets and Alerts

If we know how to create Vue web apps but want to develop mobile apps, we can use the Ionic framework.

In this article, we’ll look at how to get started with mobile development with the Ionic framework with Vue.

Action Sheet

We can display an action sheet with Ionic and Vue.

To do that, we can write:

<template>
  <ion-page>
    <ion-content>
      <ion-button @click="presentActionSheet">Show Action Sheet</ion-button>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonButton, actionSheetController } from "@ionic/vue";
import { defineComponent } from "vue";
import { caretForwardCircle, close, heart, trash, share } from "ionicons/icons";

export default defineComponent({
  components: { IonButton },
  methods: {
    async presentActionSheet() {
      const actionSheet = await actionSheetController.create({
        header: "Albums",
        cssClass: "my-custom-class",
        buttons: [
          {
            text: "Delete",
            role: "destructive",
            icon: trash,
            handler: () => {
              console.log("Delete clicked");
            },
          },
          {
            text: "Play (open modal)",
            icon: caretForwardCircle,
            handler: () => {
              console.log("Play clicked");
            },
          },
          {
            text: "Cancel",
            icon: close,
            role: "cancel",
            handler: () => {
              console.log("Cancel clicked");
            },
          },
        ],
      });
      return actionSheet.present();
    },
  },
});
</script>

We add the presentActionSheet method to show the action sheet with actionSheetController.create .

The object in the argument has the header with the action sheet header.

cssClass has the CSS class.

buttons has an array of buttons.

The objects in the array lets us set various things for the buttons.

text has the button text.

role has the aria-role.

icon has the icon.

handler has the function that’s run when we click on the button.

The actionSheet.present method shows the action sheet.

Alert

We can show alerts with the alertController .

For example, we can write:

<template>
  <ion-page>
    <ion-content>
      <ion-button @click="presentAlert">Show Alert</ion-button>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonButton, alertController } from "@ionic/vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { IonButton },
  methods: {
    async presentAlert() {
      const alert = await alertController.create({
        cssClass: "my-custom-class",
        header: "Alert",
        subHeader: "Subtitle",
        message: "This is an alert message.",
        buttons: ["OK"],
      });
      return alert.present();
    },
  },
});
</script>

to show the alert.

alertController.create creates the alert object.

cssClass has the CSS class.

header has the header.

subHeader has the subheader text.

message has the message text.

buttons has the button text.

We can customize the buttons more.

For example, we can write:

<template>
  <ion-page>
    <ion-content>
      <ion-button [@click](https://medium.com/r/?url=http%3A%2F%2Ftwitter.com%2Fclick "Twitter profile for @click")="presentAlert">Show Alert</ion-button>
    </ion-content>
  </ion-page>
</template>

<script>
import { IonButton, alertController } from "@ionic/vue";
import { defineComponent } from "vue";

export default defineComponent({
  components: { IonButton },
  methods: {
    async presentAlert() {
      const alert = await alertController.create({
        cssClass: "my-custom-class",
        header: "Alert",
        subHeader: "Subtitle",
        message: "This is an alert message.",
        buttons: [
          {
            text: "Cancel",
            role: "cancel",
            cssClass: "secondary",
            handler: (blah) => {
              console.log("Confirm Cancel:", blah);
            },
          },
          {
            text: "Okay",
            handler: () => {
              console.log("Confirm Okay");
            },
          },
        ],
      });
      return alert.present();
    },
  },
});
</script>

We add the buttons array and objects in the array.

The text has the button text.

role has aria-role.

cssClass has the CSS class.

handler has the function that’s run when we click the button.

Conclusion

We can add action sheets and alerts with Ionic Vue.

Categories
Vue Ionic

Getting Started with Mobile Development with Ionic and Vue

If we know how to create Vue web apps but want to develop mobile apps, we can use the Ionic framework.

In this article, we’ll look at how to get started with mobile development with the Ionic framework with Vue.

Getting Started

We can get started by installing a few things.

First, we install the Ionic CLI globally by running:

npm install -g @ionic/cli@testing

Next, we can create our Ionic app project by running:

ionic start ionic-vue-app blank --type vue --tag vue-beta

tabs adds tabs to the app.

type set to react means we’ll create a React project

--capacitor means we add Capacitor so we can run and build a native app from our project files.

To run our app with Genymotion and built a native app, we need to do more things.

Next, we add some scripts to our package.json file.

We write:

"ionic:serve": "vue-cli-service serve",
"ionic:build": "vue-cli-service build"

to into the scripts section.

Then, we run:

ionic build

to create the assets folder.

Then we run:

npx cap add android
npx cap sync

to add the Android dependencies.

Then we need to install Android Studio and Genymotion.

After we install Android Studio, we install the Genymotion plugin for Android Studio.

Once we did that, we run:

ionic capacitor run android --livereload --external --address=0.0.0.0

which runs our ionic:serve scripts with Genymotion.

We should see the app in Genymotion and any changes will be loaded automatically.

Creating a Camera App

We can create a camera with Ionic Vue by doing a few simple steps.

To write it, we add the following to views/Home.vue :

<template>
  <ion-page>
    <ion-content>
      <ion-grid>
        <ion-row>
          <ion-col>
            <ion-button @click='takePhoto'>take photo</ion-button>
          </ion-col>
        </ion-row>
        <ion-row>
          <ion-col size="6" :key="photo" v-for="photo in photos">
            <ion-img :src="photo.webviewPath"></ion-img>
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>
  </ion-page>
</template>

<script lang="ts">
import {
  IonContent,
  IonHeader,
  IonPage,
  IonGrid,
  IonRow,
  IonCol,
  IonImg,
  IonButton
} from "@ionic/vue";
import { defineComponent } from "vue";
import { ref, onMounted, watch } from "vue";
import {
  Plugins,
  CameraResultType,
  CameraSource,
  CameraPhoto,
  Capacitor,
  FilesystemDirectory,
} from "@capacitor/core";

interface Photo {
  filepath: string;
  webviewPath?: string;
}

function usePhotoGallery() {
  const { Camera } = Plugins;
  const photos = ref<Photo[]>([]);
  const takePhoto = async () => {
    const cameraPhoto = await Camera.getPhoto({
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
      quality: 100,
    });
    const fileName = new Date().getTime() + ".jpeg";
    const savedFileImage = {
      filepath: fileName,
      webviewPath: cameraPhoto.webPath,
    };
    photos.value = [savedFileImage, ...photos.value];
  };

  return {
    photos,
    takePhoto,
  };
}

export default defineComponent({
  name: "Home",
  components: {
    IonContent,
    IonHeader,
    IonPage,
    IonGrid,
    IonRow,
    IonCol,
    IonImg,
    IonButton
  },
  setup() {
    const { photos, takePhoto } = usePhotoGallery();
    return {
      takePhoto,
      photos,
      close,
    };
  },
});
</script>

We add the usePhotoGallery function that gets the camera.

Then we call Camera.getPhoto method to get the camera.

We get the CameraResuultType.Uri property to get the resule type for the camera.

quality has the quality to for the photos.

Then we set the photos.value propety with the new image adter taking the image with the camera, which is stored in the photo.value property.

Then we loop through the photos object to loop through the photos that are taken.

Conclusion

We can create mobile apps easily with Ionic and Vue.

Categories
Express MongoDB Nodejs Vue 3

Create a Full Stack Web App with the MEVN Stack

The MEVN stack is a set of technologies for full-stack apps.

MEVN stands for MongoDB, Express, Vue.js, and Node.js

In this article, we’ll take a look at how to create a simple todo app with authentication with the MEVN stack.

Setting Up the Project

The first step to create a full-stack MEVN app is to set up the project.

First, we create a project folder, then add the backend and frontend folders inside it.

backend has the Express app.

frontend has the Vue.js app. We’ll use Vue 3 for the front end.

Next, to create our Express app, we run the Express Generator to create the files.

To do this, go into the backend and run:

npx express-generator

We may need admin privileges to do this.

Next, we go into the frontend folder.

We install the latest version of the Vue CLI by running:

npm install -g @vue/cli

Vue CLI 4.5 or later can create Vue 3 projects.

Then in the frontend folder, we run:

vue create .

Then we choose the ‘Vue 3 (default)’ option.

Next, we have to install the packages.

We stay in the frontend folder and run:

npm i axios vue-router@4.0.0-beta.12

to install the Axios HTTP client and Vue Router 4.x, which is compatible with Vue 3.

In the backend folder, we run:

npm i cors momgoose jsonwebtoken

to install the CORS and Mongoose packages to enable cross-domain communication and work with MongoDB.

jsonwebtoken lets us add JSON web token authentication into our app.

Create the Back End

Now we’re ready to create the back end app to work with MongoDB and add authentication.

First, we create db.js in the backend folder and add:

const { Schema, createConnection } = require('mongoose');
const connection = createConnection('mongodb://localhost:27017/mevn-example', { useNewUrlParser: true });

const userSchema = new Schema({
  name: String,
  password: String
});

const User = connection.model('User', userSchema);

const todoSchema = new Schema({
  name: String,
  done: Boolean,
  user: { type: Schema.Types.ObjectId, ref: 'User' },
});

const Todo = connection.model('Todo', todoSchema);

module.exports = {
  User,
  Todo
}

We connect to the MongoDB database with Mongoose and create the models.

To create the models, we use the Schema constructor to create the schema.

We add the properties and their data types.

The user property references the Object ID from the User model so that we can link the todo entry to the user.

Then we call connection.model to use the schema to create the model.

Then, we create constants.js in the backend folder and write:

module.exports = {
  SECRET: 'secret'
}

We put the secret for the JSON web token into this file and use them in the route files.

Next, we go into the routes folder and add the files for the API routes.

We create todos.js in the routes folder and write:

var express = require('express');
var router = express.Router();
const { Todo } = require('../db');
const jwt = require('jsonwebtoken');
const { SECRET } = require('../constants');

const verifyToken = (req, res, next) => {
  try {
    req.user = jwt.verify(req.headers.authorization, SECRET);
    return next();
  } catch (err) {
    console.log(err)
    return res.status(401);
  }
}

router.get('/', verifyToken, async (req, res) => {
  const { _id } = req.user;
  const todos = await Todo.find({ user: _id })
  res.json(todos);
});

router.get('/:id', verifyToken, async (req, res) => {
  const { _id } = req.user;
  const { id } = req.params;
  const todo = await Todo.findOne({ _id: id, user: _id })
  res.json(todo);
});

router.post('/', verifyToken, async (req, res) => {
  const { name } = req.body;
  const { _id } = req.user;
  const todo = new Todo({ name, done: false, user: _id })
  await todo.save()
  res.json(todo);
});

router.put('/:id', verifyToken, async (req, res) => {
  const { name, done } = req.body;
  const { id } = req.params;
  const todo = await Todo.findOneAndUpdate({ _id: id }, { name, done })
  await todo.save();
  res.json(todo);
});

router.delete('/:id', verifyToken, async (req, res) => {
  const { id } = req.params;
  await Todo.deleteOne({ _id: id })
  res.status(200).send();
});

module.exports = router;

The verifyToken middleware has the jwt.verify method to verify the token with our SECRET key.

If it’s valid, we set the req.user to the decoded token.

Then we call next to call the router handlers.

Then we have the GET / route to get the user data from req.user and then call Todo.find to find the todo entries for the user.

Todo is the model we created earlier.

The GET /:id route gets the Todo entry by the ID.

findOne gets the first result in the todos collection.

req.params gets the URL parameters from the URL parameter.

Then POST / route gets the name from the req.body property, which has the request body daya.

And we get the _id property from the req.user property to get the user data.

We use both to create the todo entry.

user has the Object ID for the user.

The PUT /:id route is used to update the user.

We get the name and done from req.body , which has the request body.

req.params has the id property which has the ID of the todo entry.

We use the id to find the todo entry.

And update the name and done property to update the entry.

Then we call todo.save() to save the data.

The DELETE /:id route lets us delete a todo entry by its ID.

We call deleteOne with the _id to do that.

The verifyToken will make sure the token is valid before we run the route handlers.

req.headers.authorization has the auth token. req.headers has the HTTP request headers.

Next, we create users.js in the routes folder and write:

var express = require('express');
var router = express.Router();
const bcrypt = require('bcrypt');
const { User } = require('../db');
const { SECRET } = require('../constants');
const jwt = require('jsonwebtoken');
const saltRounds = 10;

router.post('/register', async (req, res) => {
  const { name, password } = req.body;
  const existingUser = await User.findOne({ name });
  if (existingUser) {
    return res.json({ err: 'user already exists' }).status(401);
  }
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  const user = new User({
    name,
    password: hashedPassword
  })
  await user.save();
  res.json(user).status(201);
});

router.post('/login', async (req, res) => {
  const { name, password } = req.body;
  const { _id, password: userPassword } = await User.findOne({ name });
  const match = await bcrypt.compare(password, userPassword);
  if (match) {
    const token = await jwt.sign({ name, _id }, SECRET);
    return res.json({ token });
  }
  res.status(401);
});

module.exports = router;

It’s similar to the todos.js .

We have the POST /register route to get the name and password properties from the JSON request body.

Then we check if the user with the given name exists.

Then we get the bcrypt.hash to hash the password with the secret.

And then we save the user with the name and then hashedPassword.

The POST /login route gets the name and password from the JSON request body.

We use the bcrypt.compare method to compare the password.

Then we create the token and return it in the response if validation is successful.

Otherwise, we send a 401 response.

Finally, we bring everything together in app.js in the backend folder.

We replace what’s there with:

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var cors = require('cors')

var todorouter = require('./routes/todo');
var usersRouter = require('./routes/users');

var app = express();
app.use(cors())
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/todos', todorouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

We add the cors middleware so that we can communicate with the front end.

We have the todorouter and usersRouter to add the routes to our app.

Vue 3 Front End

Now that we finished the back end, we work on the front end.

First, in the components folder, we create TodoForm.vue and write:

<template>
  <div>
    <h1>{{ edit ? "Edit" : "Add" }} Todo</h1>
    <form @submit.prevent="submit">
      <div class="form-field">
        <label>Name</label>
        <br />
        <input v-model="form.name" />
      </div>
      <div>
        <label>Done</label>
        <input type="checkbox" v-model="form.done" />
      </div>
      <div>
        <input type="submit" value="Submit" />
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  name: "TodoForm",
  data() {
    return {
      form: { name: "", done: false },
    };
  },
  props: {
    edit: Boolean,
    id: String,
  },
  methods: {
    async submit() {
      const { name, done } = this.form;
      if (!name) {
        return alert("Name is required");
      }
      if (this.edit) {
        await axios.put(`${APIURL}/todos/${this.id}`, { name, done });
      } else {
        await axios.post(`${APIURL}/todos`, { name, done });
      }
      this.$router.push("/todos");
    },
    async getTodo() {
      const { data } = await axios.get(`${APIURL}/todos/${this.id}`);
      this.form = data;
    },
  },
  beforeMount() {
    if (this.edit) {
      this.getTodo();
    }
  },
};
</script>

We have a form to let us add or edit to todo entries.

It takes the edit prop to indicate whether we’re editing a todo or not.

We use v-model to bind the inputs to the reactive properties.

Then we check for the this.edit value.

If it’s true , we make a PUT request with the id of the todo entry.

Otherwise, we make a POST request to create a new todo entry.

The getTodo method lets us get the todo by the ID when we’re editing.

Next, in the src folder, we create the views folder to add the route components.

We create the AddTodoForm.vue file and add:

<template>
  <div>
    <TodoForm></TodoForm>
  </div>
</template>

<script>
import TodoForm from "@/components/TodoForm";

export default {
  components: {
    TodoForm,
  },
};
</script>

We register the TodoForm component and render it in the template.

Next, we create the EditTodoForm.vue in the views folder and add:

<template>
  <div>
    <TodoForm edit :id='$route.params.id'></TodoForm>
  </div>
</template>

<script>
import TodoForm from "@/components/TodoForm";

export default {
  components: {
    TodoForm,
  },
};
</script>

We pass the edit and id props into the TodoForm so it can get todo entry by ID.

$route.params.id has the Object ID of the todo entry.

Next, we create the Login.vue file in the views folder to add a login form.

In this file, we add:

<template>
  <div>
    <h1>Login</h1>
    <form @submit.prevent="login">
      <div class="form-field">
        <label>Username</label>
        <br />
        <input v-model="form.name" type="text" />
      </div>
      <div class="form-field">
        <label>Password</label>
        <br />
        <input v-model="form.password" type="password" />
      </div>
      <div>
        <input type="submit" value="Log in" />
        <button type="button" @click="$router.push('/register')">
          Register
        </button>
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      form: { name: "", password: "" },
    };
  },
  methods: {
    async login() {
      const { name, password } = this.form;
      if (!name || !password) {
        alert("Username and password are required");
      }
      try {
        const {
          data: { token },
        } = await axios.post(`${APIURL}/users/login`, {
          name,
          password,
        });
        localStorage.setItem("token", token);
        this.$router.push("/todos");
      } catch (error) {
        alert("Invalid username or password.");
      }
    },
  },
};
</script>

We have a form that takes the username and password and let the user log in.

When we submit the form, the login method is called.

In the method, we check if the name and password have values.

If they both are nonempty, we proceed with making the login request.

If it succeeds, we go to the /todos route.

Otherwise, we should an error message.

Similarly, we create Register.vue component for the registration form.

Then we fill it with the following code:

<template>
  <div>
    <h1>Register</h1>
    <form @submit.prevent="register">
      <div class="form-field">
        <label>Username</label>
        <br />
        <input v-model="form.name" type="text" />
      </div>
      <div class="form-field">
        <label>Password</label>
        <br />
        <input v-model="form.password" type="password" />
      </div>
      <div>
        <input type="submit" value="Register" />
        <button type="button" @click="$router.push('/')">Login</button>
      </div>
    </form>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      form: { name: "", password: "" },
    };
  },
  methods: {
    async register() {
      const { name, password } = this.form;
      if (!name || !password) {
        alert("Username and password are required");
      }
      try {
        await axios.post(`${APIURL}/users/register`, {
          name,
          password,
        });
        alert("Registration successful");
      } catch (error) {
        alert("Registration failed.");
      }
    },
  },
};
</script>

We get the username and password the same way.

If they’re both non-empty, then we make a request to the register route.

Once that succeeds, we show a success message.

Otherwise, we show a failure message, which is in the catch block.

Next, we have a component to show the todo items.

We create Todo.vue in the views folder and write:

<template>
  <div>
    <h1>Todos</h1>
    <button @click="$router.push(`/add-todo`)">Add Todo</button>
    <button @click="logOut">Logout</button>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Done</th>
          <th>Edit</th>
          <th>Delete</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="t of todos" :key="t._id">
          <td>{{ t.name }}</td>
          <td>{{ t.done }}</td>
          <td>
            <button @click="$router.push(`/edit-todo/${t._id}`)">Edit</button>
          </td>
          <td>
            <button @click="deleteTodo(t._id)">Delete</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import axios from "axios";
import { APIURL } from "../constants";

export default {
  data() {
    return {
      todos: [],
    };
  },
  methods: {
    async getTodos() {
      const { data: todos } = await axios.get(`${APIURL}/todos`);
      this.todos = todos;
    },
    async deleteTodo(id) {
      await axios.delete(`${APIURL}/todos/${id}`);
      this.getTodos();
    },
    logOut() {
      localStorage.clear();
      this.$router.push("/");
    },
  },
  beforeMount() {
    this.getTodos();
  },
};
</script>

<style scoped>
th:first-child,
td:first-child {
  width: 60%;
}

th {
  text-align: left;
}
</style>

We have the Add Todo button that goes to the add-todo route when we click it.

This will be mapped to the AddTodoForm.vue component.

The Logout button calls the logOut method when it’s clicked.

It just clears local storage and redirect the user to the login page.

The getTodos method gets the todo entries for the user.

The user identity is determined from the decoded token.

The v-for directive renders the items and display them.

The Edit button goes to the edit form.

And the Delete button calls deleteTodo when we click it.

deleteTodo makes a DELETE request to the todos route.

We call getTodos after deleting a todo entry or when we load the page.

Next in src/App.vue , we add our router-view to show the route:

<template>
  <router-view></router-view>
</template>

<script>
export default {
  name: "App",
};
</script>

<style>
.form-field input {
  width: 100%;
}

#app {
  margin: 0 auto;
  width: 70vw;
}
</style>

Then we create constants.js in the src folder and add:

export const APIURL = 'http://localhost:3000'

to let us use the APIURL everywhere so that we don’t have to repeat the base URL for the back end API when we make requests.

Finally, in main.js , we write:

import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHashHistory } from 'vue-router';
import Login from '@/views/Login';
import Todo from '@/views/Todo';
import Register from '@/views/Register';
import AddTodoForm from '@/views/AddTodoForm';
import EditTodoForm from '@/views/EditTodoForm';
import axios from 'axios';

axios.interceptors.request.use((config) => {
  if (config.url.includes('login') || config.url.includes('register')) {
    return config;
  }
  return {
    ...config, headers: {
      Authorization: localStorage.getItem("token"),
    }
  }
}, (error) => {
  return Promise.reject(error);
});

const beforeEnter = (to, from, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    next()
    return true;
  }
  next({ path: '/' });
  return false
}

const routes = [
  { path: '/', component: Login },
  { path: '/register', component: Register },
  { path: '/todos', component: Todo, beforeEnter },
  { path: '/add-todo', component: AddTodoForm, beforeEnter },
  { path: '/edit-todo/:id', component: EditTodoForm, beforeEnter },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

const app = createApp(App);
app.use(router);
app.mount('#app')

to add the vue-router and the Axios request interceptor to add the auth token for authenticated routes.

beforeEnter is a route guard that lets us restrict access for authenticated routes.

We check if the token is present before we redirect the user to the page they’re going to.

Otherwise, we redirect to the login page.

routes has all the routes. path has the route URLs an component has the component we want to load.

beforeEnter is the route guard that loads before the route loads.

createRouter creates the router object.

createWebHashHistory lets us use hash mode for URLs.

So URLs will have the # sign between the base URL and the rest of the URL segments.

Then we call app.use(router) to add the router and make the this.$route and thuis.$router properties available in components.

Now we can run npm run serve from the frontend folder and node bin/www to start the back end.

Conclusion

We can create a simple MEVN stack app with the latest JavaScript frameworks and features without much trouble.

Categories
Vue Testing

Unit Test Vue Apps with Vue Test Utils — Vue Router and Vuex Mocks

With the Vue Test Utils library, we can write and run unit tests for Vue apps easily.

In this article, we’ll look at how to write unit tests with the Vue Test Utils library.

Mocking $route and $router

We can mock the $route and $router objects to inject the Vue Router’s reactive properties into our mounted component.

For instance, we can write:

import { shallowMount } from '@vue/test-utils'

const Component = {
  template: `
    <div>
      <p>{{$route.path}}</p>
    </div>
  `
}

describe('Component', () => {
  it('renders the Component component with the path', async () => {
    const $route = {
      path: '/some/path'
    }
    const wrapper = shallowMount(Component, {
      mocks: {
        $route
      }
    })
    expect(wrapper.text()).toContain('/some/path')
  })

})

We have the Component component that renders the $route.path value.

We just put that in the mocks property so that it’ll be set with the given value when we mount the component.

Testing Vuex in Components

We can test components that depends on Vuex.

For example, if we have a Vue app that has the following code:

main.js

import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <button @click="$store.dispatch('increment')">add</button>
    <p>{{count}}</p>
  </div>
</template>

<script>
export default {
  name: "App",
  computed: {
    count() {
      return this.$store.state.count;
    },
  },
};
</script>

Then we can test the code by writing:

example.spec.js

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import App from '@/App'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('App.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      increment: jest.fn(),
    }
    store = new Vuex.Store({
      actions
    })
  })

  it('dispatches "increment" action when button is clicked', async () => {
    const wrapper = shallowMount(App, { store, localVue })
    const button = wrapper.find('button')
    await button.trigger('click')
    expect(actions.increment).toHaveBeenCalled()
  })
})

Once again, we call createLocalVue to create a local Vue instance we use for testing.

Then we have a beforeEach callback to set up the store and inject it into our app.

We inject the mocked store and the localVue object.

Then we get the button and trigger the click event on it.

And finally, we check that the increment action has been triggered after clicking the button.

Mocking Getters

Similarly, we can mock getters in our tests.

Given that we have the following code:

main.js

import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    increment(context) {
      context.commit('increment')
    }
  },
  getters: {
    count(state) {
      return state.count;
    }
  }
})

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <button @click="$store.dispatch('increment')">add</button>
    <p>{{count}}</p>
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  name: "App",
  computed: {
    ...mapGetters(['count'])
  },
};
</script>

Then to test our code, we write:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import App from '@/App'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('App.vue', () => {
  let actions
  let store
  let getters

  beforeEach(() => {
    getters = {
      count: () => 2,
    }
    actions = {
      increment: jest.fn(),
    }
    store = new Vuex.Store({
      actions,
      getters
    })
  })

  it('dispatches "increment" action when button is clicked', () => {
    const wrapper = shallowMount(App, { store, localVue })
    expect(+wrapper.find('p').text()).toBe(getters.count())
  })
})

We add the mock getter into out mock store object.

Then we get the p element’s text and sees if it matches what’s returned in the mocked getter.

Conclusion

We can mock Vuex and Vue Router in our tests so that our components can be tested in isolation.