Build Vue.js UI with Element UI

John Au-Yeung
Jan 5 · 8 min read

There are many UI frameworks for Vue.js. One popular one is Element UI. It provides the components that other frameworks provide. Also, it’s easy to add to any Vue.js app. Form validation is also built it, which is very useful since Vue.js doesn’t come with any form validation functionality built-in. Its form validation functionality checks the model, so it doesn’t matter what the structure of your form is.

In this article, we will build a movie database app which lets users enter the title, release date, summary and add a photo for each movie entry. To start the project, we run the Vue CLI by running npx @vue/cli create movie-app . In the wizard, we choose to include Vuex, Vue Router, Babel, and CSS preprocessor. Then we add Element UI by running vue add element .

Once that’s done, we need to add Axios to make HTTP requests and Vue-Filter-Date-Format for displaying dates. We install them by running npm i axios vue-filter-date-format .

Next, we work on the code. In the components folder, create a file called MovieForm.vue and add:

<template>
<el-form :model="form" :rules="rules" ref="ruleForm">
<el-form-item label="Title" prop="title">
<el-input v-model="form.title"></el-input>
</el-form-item>
<el-form-item label="Release Date" prop="date">
<el-date-picker v-model="form.date" type="date" placeholder="Release Date"></el-date-picker>
</el-form-item>
<el-form-item label="Summary" prop="summary">
<el-input v-model="form.summary" type="textarea"></el-input>
</el-form-item>
<el-form-item>
<input type="file" style="display: none" ref="file" @change="onChangeFileUpload($event)" />
<el-button type="primary" @click="$refs.file.click()">Upload Photo</el-button>
</el-form-item>
<el-form-item prop="photo">
<img ref="photo" :src="form.photo" class="photo" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">Save</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "MovieForm",
mixins: [requestsMixin],
props: {
movie: Object,
edit: Boolean
},
data() {
return {
form: {},
rules: {
title: [
{
required: true,
message: "Please input title",
trigger: "blur"
}
],
date: [
{
required: true,
message: "Please input date",
trigger: "blur"
}
],
summary: [
{
required: true,
message: "Please input summary",
trigger: "blur"
}
],
photo: [
{
required: true,
message: "Please upload photo",
trigger: "blur"
}
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate(async valid => {
if (valid) {
if (this.edit) {
await this.editMovie(this.form);
} else {
await this.addMovie(this.form);
}
const { data } = await this.getMovies();
this.$store.commit("setMovies", data);
this.$emit("saved");
}
return false;
});
},
cancel() {
this.$emit("cancelled");
},
onChangeFileUpload($event) {
const file = $event.target.files[0];
const reader = new FileReader();
reader.onload = () => {
this.$refs.photo.src = reader.result;
this.form.photo = reader.result;
};
reader.readAsDataURL(file);
}
},
watch: {
movie: {
handler(val) {
this.form = JSON.parse(JSON.stringify(val || {}));
},
deep: true,
immediate: true
}
}
};
</script>

This is the form for adding and edit movie entries. The template has the Element UI form which has the text inputs for the name, release date, and summary; and there’s a photo file input. The el-form-item and el-input are styled so that we don’t have to add any styles ourselves. The label for each field in the label prop of the el-input-form components. The data binding is done by passing the form object into the model prop of the el-form component.

In the script section, we have the rules object that’s passed into the el-form component. This object does the form validation for the form object. Since we read the photo into a Base64 string, it also does validation for that. The onChangeFileUpload function gets a reference to the file input and read the content into a Base64 string.

When the Save button is clicked, the submitForm button is called since we passed it in the click handler of the button. The el-form has a ref called ruleForm , so we pass that in as the argument, so that we can do validation on the form. Then in the function, it calls the validate function provided by Element UI to do the form validation, and then edit or add an entry depending on the value of the edit prop. Then the new values are obtained from getMovies and put in the Vuex store. Then the saved event is emitted to close the modal.

To make the edit functionality work, we add a watch block to watch for the movie prop, which we will pass into this component when there is something to be edited.

Next, we create a mixins folder and add requestsMixin.js into the mixins folder. In the file, we add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
methods: {
getMovies() {
return axios.get(`${APIURL}/movies`);
},
addMovie(data) {
return axios.post(`${APIURL}/movies`, data);
},
editMovie(data) {
return axios.put(`${APIURL}/movies/${data.id}`, data);
},
deleteMovie(id) {
return axios.delete(`${APIURL}/movies/${id}`);
}
}
};

These are the functions we use in our components to make HTTP requests to get and save our data.

Next in Home.vue , replace the existing code with:

<template>
<div class="page" id='top'>
<h1 class="text-center">Recipes</h1>
<b-button-toolbar class="button-toolbar">
<b-button @click="openAddModal()" variant="primary">Add Recipe</b-button>
</b-button-toolbar>
<b-card
v-for="r in recipes"
:key="r.id"
:title="r.name"
:img-src="r.photo"
img-alt="Image"
img-top
tag="article"
class="recipe-card"
img-bottom
>
<b-card-text>
<h1>Ingredients</h1>
<div class="wrap">{{r.ingredients}}</div>
</b-card-text>
<b-card-text>
<h1>Recipe</h1>
<div class="wrap">{{r.recipe}}</div>
</b-card-text>
<b-button
href="#"
v-scroll-to="{
el: '#top',
container: 'body',
duration: 500,
easing: 'linear',
offset: -200,
force: true,
cancelable: true,
x: false,
y: true
}"
variant="primary"
>Scroll to Top</b-button>
<b-button @click="openEditModal(r)" variant="primary">Edit</b-button> <b-button @click="deleteOneRecipe(r.id)" variant="danger">Delete</b-button>
</b-card>
<b-modal id="add-modal" title="Add Recipe" hide-footer>
<RecipeForm @saved="closeModal()" @cancelled="closeModal()" :edit="false" />
</b-modal>
<b-modal id="edit-modal" title="Edit Recipe" hide-footer>
<RecipeForm
@saved="closeModal()"
@cancelled="closeModal()"
:edit="true"
:recipe="selectedRecipe"
/>
</b-modal>
</div>
</template>
<script>
// @ is an alias to /src
import RecipeForm from "@/components/RecipeForm.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
name: "home",
components: {
RecipeForm
},
mixins: [requestsMixin],
computed: {
recipes() {
return this.$store.state.recipes;
}
},
beforeMount() {
this.getAllRecipes();
},
data() {
return {
selectedRecipe: {}
};
},
methods: {
openAddModal() {
this.$bvModal.show("add-modal");
},
openEditModal(recipe) {
this.$bvModal.show("edit-modal");
this.selectedRecipe = recipe;
},
closeModal() {
this.$bvModal.hide("add-modal");
this.$bvModal.hide("edit-modal");
this.selectedRecipe = {};
},
async deleteOneRecipe(id) {
await this.deleteRecipe(id);
this.getAllRecipes();
},
async getAllRecipes() {
const { data } = await this.getRecipes();
this.$store.commit("setRecipes", data);
}
}
};
</script>

In this file, we have a list of Element UI cards to display a list of recipe entries and let users open and close the add and edit modals. We have buttons on each card to let users edit or delete each entry. Each card has an image of the movie at the bottom which was uploaded when the recipe is entered.

In the scripts section, we have the beforeMount hook to get all the password entries during page load with the getMovies function we wrote in our mixin. When the Edit button is clicked, the selectedMovie variable is set, and we pass it to the MovieFormfor editing.

To delete a recipe, we call deleteOneMovie in our mixin to make the request to the back end.

Next in App.vue , we replace the existing code with:

<template>
<div id="app">
<el-menu mode="horizontal">
<el-menu-item index="1">Movie App</el-menu-item>
<el-menu-item index="2">Home</el-menu-item>
</el-menu>
<router-view />
</div>
</template>
<script>
export default {
name: "app"
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.page {
padding: 20px;
margin: 0 auto;
max-width: 700px;
}
body {
margin: 0px;
}
</style>

to add an Element UI navigation bar to the top of our pages, and a router-view to display the routes we define. We have to set the font ourselves since Element UI doesn’t set it to any particular font. We also have to set the body’s margin to 0 so that we remove it. We make a page class and set the maximum width to 700px so that images won’t be too big.

Then in element-variable.scss which is generated when we run vue add element . We replace the existing code with:

/*
Write your variables here. All available variables can be
found in element-ui/packages/theme-chalk/src/common/var.scss.
For example, to overwrite the theme color:
*/
$--color-primary: teal;
/* icon font path, required */
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";
button,
input,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
}
.el-picker-panel__body-wrapper {
font-family: "Avenir", Helvetica, Arial, sans-serif;
}

to make the font in our form consistent.

Next in main.js , we replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import './plugins/element.js'
import VueFilterDateFormat from 'vue-filter-date-format';
Vue.use(VueFilterDateFormat);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");

We add the Vue-Filter-Date-Format library here, along with the Element UI library.

In router.js we replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
Vue.use(Router);export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
}
]
});

to include the home page in our routes so users can see the page.

And in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);export default new Vuex.Store({
state: {
movies: []
},
mutations: {
setMovies(state, payload) {
state.movies = payload;
}
},
actions: {}
});

to add our movies state to the store so we can observe it in the computed block of MovieFormand HomePage components. We have the setMovies function to update the passwords state and we use it in the components by call this.$store.commit(“setMovies”, response.data); like we did in MovieForm .

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>Movie App</title>
<link
rel="stylesheet"
href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"
/>
</head>
<body>
<noscript>
<strong
>We're sorry but vue-element-ui-tutorial-app doesn't work properly
without JavaScript enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

to change the title.

After all the hard work, we can start our app by running npm run serve.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
"movies": [
]
}

So we have the movies endpoints defined in the requests.js available.

After all the hard work, we get:

JavaScript in Plain English

Learn the web's most important programming language.

John Au-Yeung

Written by

Web developer. Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade