Here we see how to build a real-world client application for your FeathersJS backend. We will use Vue + Feathers-Vuex and quickly create the frontend app.
We can use FeathersJS and Feathers-Vuex (our choice of client storage) to conjure up a client application that totally blows away our users.
Our choice of technology for building client -
Vue + Feathers-Vuex
Vuetify
Client: Get Started
This is what we will build for the client..
Let’s get started.
All great things in life starts with Vue CLI.
But, we don’t have the time and this is not a Vue tutorial. So, it’s time to work the magic and improve that statement.
So, we change the above quote..
A few great things start with boilerplates.
Unfortunately, again, we don’t quite have a starter-friendly boilerplate. We can either start from our vue-vuetify-booster boilerplate, or start afresh.
Either ways your Vue application should be setup in the client folder that is adjacent to the server folder where FeathersJS server resides. We will get our design inspiration from the above boilerplate.
Panel / Subpanel etc. are layouts defined in that boilerplate - we will reuse them
There are a few useful ways shortcuts to show snackbar, alerts etc, which we carry forward to this project
The home page, toolbar and navigation drawers will remain largely unchanged except for the links
importfeathersfrom"@feathersjs/feathers";importauthfrom"@feathersjs/authentication-client";importfeathersVuexfrom"feathers-vuex";importsocketiofrom"@feathersjs/socketio-client";importiofrom"socket.io-client";import{iff,discard}from"feathers-hooks-common";constsocket=io("http://localhost:3030",{transports:["websocket"]});constfeathersClient=feathers().configure(socketio(socket)).configure(auth({storage:window.localStorage})).hooks({before:{all:[iff((context)=>["create","update","patch"].includes(context.method),discard("__id","__isTemp")),],},});exportdefaultfeathersClient;// Setting up feathers-vuex
const{makeServicePlugin,makeAuthPlugin,BaseModel,models,FeathersVuex,}=feathersVuex(feathersClient,{serverAlias:"api",// optional for working with multiple APIs (this is the default value)
idField:"id",// Must match the id field in your database table/collection
whitelist:["$regex","$options"],});export{makeAuthPlugin,makeServicePlugin,BaseModel,models,FeathersVuex};
You should now be able to run the application without issues..
npm run serve
Router
Not a lot of interesting things here.. We just define a dynamic home to redirect users to dashboard or to the generic home page. Everything else is pretty standard.
Note that we have not implemented any navigation guards - but that is easy enough to do by uncommenting the requireAuth function and using that for the routes that need prior authentication.
importVuefrom"vue";importstorefrom"../store/index";importVueRouterfrom"vue-router";importHomefrom"../views/Home.vue";Vue.use(VueRouter);// function requireAuth(to, from, next) {
// if (store.state.auth.accessToken) next();
// else next("/login");
// }
functiondynamicHome(to,from,next){// this should be ideally replaced by store.getters["auth/isAuthenticated"]
// .. in newer versions
if(store.state.auth.accessToken)next("/dashboard");elsenext();}functionlogout(to,from,next){store.dispatch("auth/logout");next("/login");}constroutes=[{path:"/",name:"home",component:Home,beforeEnter:dynamicHome,},{path:"/signup",name:"signup",component:()=>import(/* webpackChunkName: "register" */"../views/Register.vue"),},{path:"/login",name:"login",component:()=>import(/* webpackChunkName: "login" */"../views/Login.vue"),},{path:"/logout",name:"logout",beforeEnter:logout,},{path:"/terms",name:"Terms",component:()=>import(/* webpackChunkName: "terms" */"../views/Terms.vue"),},{path:"/dashboard",name:"Dashboard",component:()=>import(/* webpackChunkName: "dashboard" */"../views/Dashboard.vue"),// beforeEnter: requireAuth
},{path:"/contacts",name:"Contact",component:()=>import("../views/Contact.vue"),},{path:"/activities",name:"Activity",component:()=>import("../views/Activity.vue"),},{path:"/contact-us",name:"Contact Us",component:()=>import(/* webpackChunkName: "contact-us" */"../views/ContactUs.vue"),},{path:"/about",name:"About",// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component:()=>import(/* webpackChunkName: "about" */"../views/About.vue"),},{// catch all 404
path:"*",component:()=>import("../views/NotFound.vue"),},];constrouter=newVueRouter({mode:"history",base:process.env.BASE_URL,routes,});exportdefaultrouter;
Vuex
Add a new folder called store/services and create users.js.
importfeathersClient,{makeServicePlugin,BaseModel,}from"../../feathers-client";classUserextendsBaseModel{constructor(data,options){super(data,options);}// Required for $FeathersVuex plugin to work after production transpile.
staticmodelName="User";// Define default properties here
staticinstanceDefaults(){return{email:"",password:"",name:"",};}}constservicePath="users";constservicePlugin=makeServicePlugin({Model:User,service:feathersClient.service(servicePath),servicePath,});// Setup the client-side Feathers hooks.
feathersClient.service(servicePath).hooks({before:{all:[],find:[],get:[],create:[],update:[],patch:[],remove:[],},after:{all:[],find:[],get:[],create:[],update:[],patch:[],remove:[],},error:{all:[],find:[],get:[],create:[],update:[],patch:[],remove:[],},});exportdefaultservicePlugin;
As you see - this is has the familiar structure of FeathersJS. We can fine tune this and change app behaviour, but will leave it that for now.
Create a similar file for contacts - we will use the name contactsPlugin instead of servicePlugin. Rest remains the same.
The above code imported and used a feathers provided mixin that makes our coding easier and more generic. We have indicated our intent to use the contacts service - this refers to the service file that we created earlier, which also automatically “maps” (not exactly, but bear with me) to the contacts service in the backend Feathers.
We create a computed variable which can be used to pass params to the mixin -
1
2
3
contactsParams(){return{query:this.search};},
By doing just this, we have access to the contacts list. This is the list of contacts fetched from the backend and rendered as a table.
We can change search to pass search criteria. User enters search, and we just pass it as a param in a function.
All we did was to add a service, introduced boilerplate code for feathers-client.js and pulled in the mixin. This resulted in the contacts being pulled from the server / from our database.
In addition to what has been outlined so far -
We can add functionality with simple param changes on client side and by adding hooks/custom services on server - again a lot of functionality with less code
The app has real-time functions - any changes (on server through API calls / by other users) are published to clients (all clients with default configuration) and our browser starts showing those changes
What we did not do -
use REST calls with something like fetch/ Axios
manage variables in components / Vuex
create logic to search - on client and server
That’s a lot of work saved.
Contact Edit
Let’s keep going.
We will add a reference to a new component in ContactList to enable us to edit contacts.
<template><v-dialogv-model="showDialog"persistentmax-width="1100px"><SubPanel><templateslot="toolbar-items"><spanclass="subtitle-2">New/Edit Contact</span></template><templateslot="content"v-if="item"><FeathersVuexFormWrapper:item="item"watch><templatev-slot="{ clone, save, reset, remove }"><v-formref="form"v-model="validInput"><v-cardflat><v-card-text><v-row><v-colcols="12"md="3"><v-text-fieldv-model="item['first_name']"></v-text-field></v-col><v-colcols="12"md="3"><v-text-fieldv-model="item['last_name']"required></v-text-field></v-col><v-colcols="12"md="3"><v-select:items="['active', 'inactive']"label="Status"v-model="item['status_cd']"required></v-select></v-col></v-row></v-card-text><v-card-actions><v-spacer></v-spacer><v-btnoutlined@click="closeDialog">Cancel</v-btn><v-btncolor="primary"@click="saveRecord">
Save
</v-btn></v-card-actions></v-card></v-form></template></FeathersVuexFormWrapper></template></SubPanel></v-dialog></template><script>import{mapMutations}from"vuex";importSubPanelfrom"./layouts/SubPanel";import{FeathersVuexFormWrapper}from"feathers-vuex";exportdefault{data(){return{validInput:true,item:null,};},props:{value:Boolean,currentItem:{type:Object,required:true,},},mounted(){const{Contact}=this.$FeathersVuex.api;constcontactModel=newContact();this.item=!this.currentItem||!this.currentItem["id"]?contactModel:Contact.getFromStore(this.currentItem["id"]);},computed:{showDialog:{get(){returnthis.value;},set(value){this.$emit("input",value);},},},components:{SubPanel,},methods:{...mapMutations("snackbar",["setSnack"]),saveRecord(){if(this.$refs.form.validate()){if(!this.item["id"]){this.item.save().catch((e)=>{this.showError(e);});}elsethis.item.update();this.closeDialog();}},closeDialog(){this.showDialog=false;},showError(e){// you can do other housekeeping here.
this.setSnack({message:"Error saving contact.",color:"error"});console.error("Error saving record",e);},},};</script>
When the component loads, we specify whether we are creating a new record or editing an existing record.
Automatically manage vuex best practices to clone our storage variable (item in our case) when necessary. We also indicated support saving, resetting form and removing records
Feathers client automatically maps the record received from the list to the context of the Contact service (either a new record or find existing record from store)
This code block validates form, and invokes save or update methods supported by our feathers-client to send data to our server. Server Feathers knows the operations from the invoked services and carries out those operations on server.
You may also observe that we did not quite use auth here, but it is implied in the background. Feathers figures out that for us. However, you will observe that we have created the auth module (all of two lines), included it as a plugin in the store, and created components for user registration and login.
That is it - you now have a full fledged.. oh wait.
Remember our previous statement? CRM application is not contacts alone - all of us and our neighbour’s dog know that activities make CRM application “whole”.
That said - I don’t want to take away that opportunity of creating a brand new service and Vuex plugin from you. Like the seers of yore, I just point to the direction and you execute all magic, eh?
Finis!
So here we are then.. A complete.. , er.., 50% complete “totally real-world application” built using FeathersJS on server and client
It is time for you to spread your wings and fly to newer heights and into the next telephone pole with this knowledge.
I mentioned earlier that the code used here is available on Github - please don’t forget to star the repository. Those stars are what I count sometimes to go to sleep - more the merrier.