Today tutorial we will demonstrate how to build a web application using Vuejs and Firebase. we will be using VueJS the frontend UI framework and The firebase as our backend in building a simple forum app.
Series Topics:
- Building a Simple Forum With Vue-js, Vuex and Firebase Part1: Authentication
- Building a Simple Forum With Vue-js, Vuex and Firebase Part2: Forum and Topic CRUD
- Building a Simple Forum With Vue-js, Vuex and Firebase Part3: Home Forums and Replies
Â
Requirements
- npm
- Vuejs 2 & Vuex
- Firebase
- Boostrap
- Http Server: Apache for example
As you may already know Vuejs is a javascript framework for building single page applications (SPA). Why Vuejs? Because Vuejs is a lightweight framework that have a powerful speed in building SPA apps. Also Easy to learn and understand quickly than other frameworks like Angular 4 or 5.
If you didn’t have an idea of what Vuejs and how it’s working take a look at VueJs Docs first at the Vuejs official website here Because we will concentrate on this tutorial on building the Forum.
Application
Our application will have the following pages:
- Home
- Forum display
- Topic display
- Login
- Register
- User Forums
- Add Forum
- Add Topic
- Edit Forum
- Edit Topic
- Topics in Specific User Forum
So let’s see in the next section how to utilize Vuejs to split our app into separate components and use the routing to go to any page.
Database Structure
Our database will contain four documents also called tables in other databases which are:
- Forums
- Users
- Topics
- Replies
We will using firebase as our database management system so go to https://console.firebase.google.com and create an empty project call it forum then go to Database >> Realtime Database. Then complete any required info. Also go Authentication >> Sign-in method then enable email/password authentication, Take a look at the preview video below for more details:
Â
Project Structure
Let’s create a new folder with the following structure:
Open up package.json file and insert those contents:
{ "name": "forum", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch", "build": "WEBPACK_ENV=production webpack" }, "author": "", "license": "ISC", "dependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.5", "babel-polyfill": "^6.26.0", "babel-preset-es2015": "^6.24.1", "css-loader": "^1.0.1", "firebase": "^5.5.9", "path": "^0.12.7", "querybase": "^0.6.0", "style-loader": "^0.23.1", "vue": "^2.5.17", "vue-loader": "^15.4.2", "vue-router": "^3.0.2", "vue-template-compiler": "^2.5.17", "vuex": "^3.0.1", "webpack": "^4.26.0" }, "devDependencies": { "webpack-cli": "^3.1.2" } }
Then run
npm install
This command will install all the dependencies listed in the package.json above.
Â
Webpack Configuration
We will be using The Single File Components in this tutorials so we we need support for ecmascript 2015 syntax so we will incorporate using webpack to transform and compile the es2015 using babel. Webpack is one of the most powerful javascript bundlers and it used a lot with Vuejs and other Frameworks, read more about webpack here.
Open webpack.config.js and insert the following:
var webpack = require('webpack'); var path = require('path'); const { VueLoaderPlugin } = require('vue-loader') // Naming and path settings var appName = 'app'; var entryPoint = './app.js'; var exportPath = path.resolve(__dirname, './build'); // Enviroment flag var plugins = []; var env = process.env.WEBPACK_ENV; // Differ settings based on production flag if (env === 'production') { var mode = 'production'; // var UglifyJsPlugin = webpack.optimization; // plugins.push(new UglifyJsPlugin({ minimize: true })); plugins.push(new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } } )); appName = appName + '.min.js'; } else { appName = appName + '.js'; var mode = 'development'; } // Main Settings config module.exports = { mode: mode, entry: entryPoint, output: { path: exportPath, filename: appName }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, loader: 'babel-loader', query: { presets: ['es2015'] } }, { test: /\.vue$/, loader: 'vue-loader' } ] }, resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' } }, plugins: [ new VueLoaderPlugin() ] };
The above code tells webpack that to compile all the vue code into js code that are recognizable by web browsers because web browsers at this moment can’t recognize vue code. The entry point will be app.js , this is the file that will be contain the output build of all the Vue components and the build will be in directory named build.
Preparing Vue Components
We split our app into smaller separate components, The main component which hold all the other child components. In the next section we will create the blueprint of each component and we will modify them later as we progress on our tutorial.
Â
The Main Component
Open the file located in components/App.vue this is our main component that will contain the other child components, then insert this code:
<template> <div> </div> </template> <script> export default { } </script>
Create the following files in components
- components/pages/Home.vue
- components/pages/Login.vue
- components/pages/Register.vue
- components/pages/Logout.vue
- components/pages/MyForums.vue
- components/pages/AddForum.vue
- components/pages/AddTopic.vue
- components/pages/EditTopic.vue
- components/pages/EditForum.vue
- components/pages/ForumDisplay.vue
- components/pages/ForumTopics.vue
- components/pages/TopicDisplay.vue
- components/partials/AddReply.vue
- components/partials/ForumTopics.vue
- components/partials/TopicReplies.vue
- components/Header.vue
and insert the below code
<template> <div> </div> </template> <script> export default { } </script>
Preparing routes:
Open routes.js and insert the below code:
import Home from './components/pages/Home.vue' import ForumDisplay from './components/pages/ForumDisplay.vue' import TopicDisplay from './components/pages/TopicDisplay.vue' import Login from './components/pages/Login.vue' import Logout from './components/pages/Logout.vue' import Register from './components/pages/Register.vue' import AddForum from './components/pages/AddForum.vue' import MyForums from './components/pages/MyForums.vue' import EditForum from './components/pages/EditForum.vue' import AddTopic from './components/pages/AddTopic.vue' import EditTopic from './components/pages/EditTopic.vue' import ForumTopics from './components/pages/ForumTopics.vue' export const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/home', redirect: '/' }, { path: '/forum-display/:id', name: 'ForumDisplay', component: ForumDisplay }, { path: '/topic-display/:id', name: 'TopicDisplay', component: TopicDisplay }, { path: '/login', name: 'Login', component: Login }, { path: '/logout', name: 'Logout', component: Logout }, { path: '/register', name: 'Register', component: Register }, { path: '/add-forum', name: 'AddForum', component: AddForum }, { path: '/my-forums', name: 'MyForums', component: MyForums }, { path: '/edit-forum/:id', name: 'EditForum', component: EditForum }, { path: '/add-topic/:id', name: 'AddTopic', component: AddTopic }, { path: '/edit-topic/:forum_id/:topic_id', name: 'EditTopic', component: EditTopic }, { path: '/forum-topics/:id', name: 'ForumTopics', component: ForumTopics } ];
There is nothing complex here we just imported the components and exported the router object, later we will add this router object to Vue Router.
Preparing Vuex Store and Api:
Open store.js and add the below code, we will update this file later:
import Vue from 'vue' import Vuex from 'vuex' import api from './helper/api' Vue.use(Vuex); export default new Vuex.Store( { state: { // add state here }, mutations: { // add mutuations here }, actions: { // add actions here } } )
Open helper/api.js and add the below code, we will also update this file later:
import firebase from "firebase"; // add your own config params, get them from // https://console.firebase.google.com/project/<project-name>/settings/general/ var config = { apiKey: "<your api key>", authDomain: "<your auth domain>", databaseURL: "<your database url>", storageBucket: "<your storage bucket>", }; firebase.initializeApp(config); var db = firebase.database(); export default { // api functions will be added here }
The above will contain our api so here we will connect to firebase, create and add documents, delete them, and also add and authenticate users.
Finally open the entry point app.js and add the below code:
import 'babel-polyfill'; import Vue from 'vue'; import VueRouter from 'vue-router'; import { routes } from './routes'; import store from './store'; import App from './components/App.vue'; Vue.use(VueRouter); let router = new VueRouter({ mode: 'hash', routes, store }); const app = new Vue({ el: "#app", components: { App }, router, store, created: function() { document.getElementById("loading").style.display = "block"; }, mounted: function() { // we will use this lifecyle hook later }, updated: function() { } });
In the above code we load the imported dependencies like the Vue and Vue-router then initialized the routes and finally created a Vue instance and booted our app in #app
Open index.html and add the following code:
<!DOCTYPE html> <html> <head> <title>Simple Forum</title> <meta http-equiv="content-type" content="text/html;charset=utf-8" /> <link type="text/css" rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <link type="text/css" rel="stylesheet" href="css/style.css" /> </head> <body> <!-- Modify this according to your working path --> <base href="http://localhost/js/forum/" /> <!-- App will render here --> <div id="app"> <app></app> </div> <div id="loading-wrapper"> <i class="fa fa-spinner fa-spin" style="font-size:100px; color: #428bca" id="loading" style="display: none;"></i> </div> <script src="./build/app.js"></script> </body> </html>
As you see the #app will contain our rendered app we load the bundled javascript file output from webpack. Note the <base> tag above, this is very important in order for the routing to function properly adjust it’s value according to your working directory.
<base href="http://localhost/js/forum/" />
Now run this command to build and watch for your javascript
npm run dev
You see a loading spinner but don’t worry we will populate our pages with the required in the next sections.
Preparing Login and Register
The first thing to consider when building a forum is to build the login and register functionality that’s because each forum, topic and reply connected to specific user.
Let’s open helper/api.js and modify it as follows:
import firebase from "firebase"; // add your own config params, get them from // https://console.firebase.google.com/project/<project-name>/settings/general/ var config = { apiKey: "<your api key>", authDomain: "<your auth domain>", databaseURL: "<your database url>", storageBucket: "<your storage bucket>", }; firebase.initializeApp(config); var db = firebase.database(); export default { authenticate(email, password, successcallback, errorcallback) { firebase.auth().signInWithEmailAndPassword(email, password).then(successcallback).catch(errorcallback); }, logout(successcallback, errorcallback) { firebase.auth().signOut().then(successcallback).catch(errorcallback); }, getCurrentUser(callback) { firebase.auth().onAuthStateChanged(callback); }, register(email, password, successcallback, errorcallback) { firebase.auth().createUserWithEmailAndPassword(email, password).then(successcallback).catch(errorcallback); }, updateUserDisplayname(name) { var user = firebase.auth().currentUser; user.updateProfile({ displayName: name, }).then(function() { // Update successful. }).catch(function(error) { // An error happened. }); }, addUser(name, email, uid) { var usersRef = db.ref('users'); var usersPush = usersRef.push(); const key = usersPush.getKey(); usersPush.set({ name: name, email: email, uid: uid, created_at: (new Date()).toLocaleString() }); return key; }, getUserByUID(UID, callback) { var userRef = db.ref('users').orderByChild("uid").equalTo(UID); userRef.on('value', function(snapshot) { if(snapshot.val() != null) { callback(Object.keys(snapshot.val())[0], snapshot.val()); } else { callback(null, null); } }); } }
In the above code we setup functions that register and authenticate users using the firebase api, also we add a function that tracks if the user already login or not, this function will be called on every request to ensure that user is already login.
getCurrentUser(callback) { firebase.auth().onAuthStateChanged(callback); }
Next open store.js and modify it as follows:
import Vue from 'vue' import Vuex from 'vuex' import api from './helper/api' Vue.use(Vuex); export default new Vuex.Store( { state: { auth: { email: "", password: "", name: "" }, currentUser: { id: "", // this id is the datebase key for this record name: "", email: "", uid: "", // this is is the user authenticated object status: 0 // 0=logout 1=login } }, mutations: { setAuthEmail(state, data) { state.auth.email = data }, setAuthPassword(state, data) { state.auth.password = data }, setAuthName(state, data) { state.auth.name = data }, setCurrUserId(state, data) { state.currentUser.id = data }, setCurrUserName(state, data) { state.currentUser.name = data }, setCurrUserEmail(state, data) { state.currentUser.email = data }, setCurrUserUid(state, data) { state.currentUser.uid = data }, setCurrUserStatus(state, data) { state.currentUser.status = data } }, actions: { getCurrentUser({commit}) { api.getCurrentUser(function(user) { if(user) { api.getUserByUID(user.uid, function(key, val) { if(key != null && val != null) { commit('setCurrUserId', key); commit('setCurrUserName', user.displayName); commit('setCurrUserEmail', user.email); commit('setCurrUserUid', user.uid); commit('setCurrUserStatus', 1); } }) } }); }, clearUserData({commit}) { commit('setCurrUserId', ''); commit('setCurrUserName', ''); commit('setCurrUserEmail', ''); commit('setCurrUserUid', ''); commit('setCurrUserStatus', 0); commit('setAuthEmail', ''); commit('setAuthPassword', ''); commit('setAuthName', ''); } } } )
Here we add some state variables and some mutations and some actions that we will need to call in our login and register pages.
open components/pages/Register.vue and modify it like this:
<template> <div> <section class="row panel-body"> <div class="col-md-12"> <form method="post" class="form-inline" v-on:submit.prevent="register"> <div class="alert alert-danger" v-if="this.errors.length > 0"> <ul> <li v-for="(error, index) in this.errors" :key="index">{{ error }}</li> </ul> </div> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" id="email" placeholder="Email" v-model="email"> </div> <div class="form-group"> <label for="displayName">Display name</label> <input type="text" class="form-control" id="displayName" placeholder="displayName" v-model="name"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" name="password" class="form-control" placeholder="Password" v-model="password"> </div> <button type="submit" class="btn btn-success"><i v-show="isLoading" class="fa fa-refresh fa-lg fa-spin btn-load-indicator"></i> Register</button> </form> </div> </section> </div> </template> <script> import api from '../../helper/api' export default { data() { return { isLoading: false, errors: [] } }, computed: { email: { get() { return this.$store.state.auth.email }, set(value) { this.$store.commit('setAuthEmail', value) } }, password: { get() { return this.$store.state.auth.password }, set(value) { this.$store.commit('setAuthPassword', value) } }, name: { get() { return this.$store.state.auth.name }, set(value) { this.$store.commit('setAuthName', value) } } }, methods: { register() { var self = this; this.errors = []; if(this.$store.state.auth.email == "") { this.errors.push('Email address required'); } if(this.$store.state.auth.password == "") { this.errors.push('Password required'); } if(this.$store.state.auth.name == "") { this.errors.push('Display name required'); } if(this.$store.state.auth.email !="" && !this.validateEmail(this.$store.state.auth.email)) { this.errors.push('Invalid email address'); } if(this.$store.state.auth.password !="" && this.$store.state.auth.password.length < 6) { this.errors.push('Password must be at least 6 characters long'); } if(this.errors.length > 0) { return false; } this.isLoading = true; // if everthing ok authenticate api.register(self.$store.state.auth.email, self.$store.state.auth.password, function(user) { // update name api.updateUserDisplayname(self.$store.state.auth.name); setTimeout( () => { self.isLoading = false; // insert into firebase db const key = api.addUser(self.$store.state.auth.name, user.user.email, user.user.uid); // save these info into store self.$store.commit('setCurrUserId', key); self.$store.commit('setCurrUserName', self.$store.state.auth.name); self.$store.commit('setCurrUserEmail', user.user.email); self.$store.commit('setCurrUserUid', user.user.uid); self.$store.commit('setCurrUserStatus', 1); self.$router.push('/'); }, 1500 ) }, function(error) { alert(error.message) }) }, validateEmail(email) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(String(email).toLowerCase()); } } } </script>
In the above code we created a registration form and then we bind it to computed properties that read and write from the store like that:
email: { get() { return this.$store.state.auth.email }, set(value) { this.$store.commit('setAuthEmail', value) } }
Then we create a method that triggers when submitting the form, then prepares some validation like missing field then we call our api that registers the user, after that on success the user is logged in automatically and we store the user data into our store again so to be used later when adding forums and topics.
open components/pages/Login.vue and modify it like this:
<template> <div> <section class="row panel-body"> <div class="col-md-12"> <form method="post" class="form-inline" v-on:submit.prevent="register"> <div class="alert alert-danger" v-if="this.errors.length > 0"> <ul> <li v-for="(error, index) in this.errors" :key="index">{{ error }}</li> </ul> </div> <div class="form-group"> <label for="email">Email address</label> <input type="email" class="form-control" id="email" placeholder="Email" v-model="email"> </div> <div class="form-group"> <label for="displayName">Display name</label> <input type="text" class="form-control" id="displayName" placeholder="displayName" v-model="name"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" name="password" class="form-control" placeholder="Password" v-model="password"> </div> <button type="submit" class="btn btn-success"><i v-show="isLoading" class="fa fa-refresh fa-lg fa-spin btn-load-indicator"></i> Register</button> </form> </div> </section> </div> </template> <script> import api from '../../helper/api' export default { data() { return { isLoading: false, errors: [] } }, computed: { email: { get() { return this.$store.state.auth.email }, set(value) { this.$store.commit('setAuthEmail', value) } }, password: { get() { return this.$store.state.auth.password }, set(value) { this.$store.commit('setAuthPassword', value) } }, name: { get() { return this.$store.state.auth.name }, set(value) { this.$store.commit('setAuthName', value) } } }, methods: { register() { var self = this; this.errors = []; if(this.$store.state.auth.email == "") { this.errors.push('Email address required'); } if(this.$store.state.auth.password == "") { this.errors.push('Password required'); } if(this.$store.state.auth.name == "") { this.errors.push('Display name required'); } if(this.$store.state.auth.email !="" && !this.validateEmail(this.$store.state.auth.email)) { this.errors.push('Invalid email address'); } if(this.$store.state.auth.password !="" && this.$store.state.auth.password.length < 6) { this.errors.push('Password must be at least 6 characters long'); } if(this.errors.length > 0) { return false; } this.isLoading = true; // if everthing ok authenticate api.register(self.$store.state.auth.email, self.$store.state.auth.password, function(user) { // update name api.updateUserDisplayname(self.$store.state.auth.name); setTimeout( () => { self.isLoading = false; // insert into firebase db const key = api.addUser(self.$store.state.auth.name, user.user.email, user.user.uid); // save these info into store self.$store.commit('setCurrUserId', key); self.$store.commit('setCurrUserName', self.$store.state.auth.name); self.$store.commit('setCurrUserEmail', user.user.email); self.$store.commit('setCurrUserUid', user.user.uid); self.$store.commit('setCurrUserStatus', 1); self.$router.push('/'); }, 1500 ) }, function(error) { alert(error.message) }) }, validateEmail(email) { var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return re.test(String(email).toLowerCase()); } }, created() { document.getElementById("loading").style.display = "none" } } </script>
open components/pages/Logout.vue and modify it like this:
<template> <div> <section class="row panel-body"> <div class="col-md-12"> <h6>Waiting to logout...</h6> <div class="alert alert-danger" v-if="this.error != '' ">{{ this.error }}</div> </div> </section> </div> </template> <script> import api from '../../helper/api' export default { data() { return { error: "" } }, created() { document.getElementById("loading").style.display = "none" }, mounted() { var self = this; api.logout(function() { setTimeout( () => { self.$store.dispatch('clearUserData'); self.$router.push('/'); }, 2000 ) }, function(error) { self.error = error }) } } </script>
Here of successful login we clear the user data and redirect to the homepage.
Modifying Header and The Main Component
Open components/Header.vue and modify it as shown:
<template> <header class="panel-heading"> <strong class="heading"> <router-link to="/">Simple Forum</router-link> </strong> <div class="pull-right"> <router-link v-if="this.$store.state.currentUser.status==0" to="/register" class="btn btn-primary">Register</router-link> <router-link v-if="this.$store.state.currentUser.status==0" to="/login" class="btn btn-primary">Sign in</router-link> <router-link v-if="this.$store.state.currentUser.status==1" to="/logout" class="btn btn-primary">Logout</router-link> <router-link v-if="this.$store.state.currentUser.status==1" to="/my-forums" class="btn btn-warning">My Forums</router-link> <router-link v-if="this.$store.state.currentUser.status==1" to="/add-forum" class="btn btn-warning">Add Forum</router-link> </div> </header> </template> <script> export default { } </script>
Open components/App.vue and modify it as shown:
<template> <div class="container"> <div class="row"> <section class="panel panel-success"> <app-header></app-header> <router-view></router-view> </section> </div> </div> </template> <script> import Header from './Header.vue'; export default { components: { 'app-header': Header } } </script>
Open app.js in the mounted function add this code:
mounted: function() { // check for current user if he is still login store.dispatch('getCurrentUser') }
Now in terminal run npm run dev then navigate to http://localhost/project_path/ and try to create a new account and login.
Is there a demo for this? Im looking to add to my eddcaller.com website but waned to test demo content first before building everything.
Sorry there is no demo for this, just follow the parts and see if it works for you