UP | HOME
Land of Lisp

Zhao Wei

How can man die better than facing fearful odds, for the ashes of his fathers and the temples of his Gods? -- By Horatius.

Tips-on-Vuejs

1. How to validate Vuetify v-text-field asynchronously?

I met a situation where when user register account, they need to input their email address. It is easy to validate some input as a valid email address. But, I also need to validate the input doesn’t duplicate with any other email addresses already stored in the db.

General solution:

For example: we want to validate the text-field for email address. We must ensure the input email address is not duplicated with the existing email addresses from DB.

<li>
  <v-text-field
    v-model="emailInput"
    :rules="[vEmail]"
    label="Email"
    :error-messages="errors"
  ></v-text-field>
</li>

Then, in the watch clause:

watch: {
  async emailInput(email) {
    if (tools.toBoolean(tools.vEmail(email))) {
      this.editedItem.email = email;

      let duplicated = await this.$store.dispatch(
        "checkEmailDuplication",
        email
      );
      if (duplicated) {
        this.errors = ["That email has been used."];
      } else {
        this.errors = [];
      }
    }
  },
  ...
},

2. How to create your custom template?

  1. Create your custom component .vue and export it with a specific name as child component. Then, import child component into the parent component.

    import EditClientUris from "@/components/EditClientUris";
    export default {
      ...
      components: {
        EditClientUris
      }
    }
    
  2. Use probs to pass data from parent to child
    • Define those probs in child component, in EditClientUris.vue:

      props: {
        title: String,
        headers: Array,
        items: Array,
        uriType: String,
        clientId: String
      }
      
    • Pass values from parent to child

      <EditClientUris
        title="Logout URIs"
        uriType="LOGOUT"
        v-bind:clientId="currentClientId"
        v-bind:headers="headersForLogoutUris"
        v-bind:items="itemsForLogoutUris"
        ...
      ></EditClientUris>
      

      Here, we pass title and uriType as string literals and pass clientId, headers and items from data binding.

  3. Use emit to pass event from child to parent
    More often, when some data model within child component has been changed, we need to reflect this change to parent. This is done by custom event. For example, when we saved a new item within child component, we could emit a event and let parent component to catch it.
    • In child component

      newItemSave() {
        this.createNewItemDialog = false;
        this.$emit("addNewItem", {
          uri_id: Date.now(),
          uri: this.newUri,
          uri_type: this.uriType
        });
      },
      

      Here, we are emitting custom event type addNewItem.

    • In parent component

      <EditClientUris
        v-on:addNewItem="addNewUri",
        ...
      ></EditClientUris>
      

      Here, we are catching the custom event from parent component using v-on:addNewItem and handle it using local method addNewUri.

      methods: {
        addNewUri(uriInfo) {
          this.$store.state.clients.clientDetail?.uris.push(uriInfo);
        },
        ...
      }
      


That is it, with carefully plan we could construct our app with highly customized component to achieve maximum code reuse and abstraction.

3. How to access different part from component or vice versa?

The core part of Vuejs application is the “store” which does the state management for Vuejs application. It is from using Vuex package. After install it, the standard structure of project will contain a “store” folder.
Inside, there is an index.js in which it export the store:

Vue.use(Vuex);

let store = new Vuex.Store({
  plugins: [
    createPersistedState({
      storage: window.sessionStorage
    })
  ],
  modules:{
    ...
    users: usersModule,
    roles: roleModule,
    permissions: permissionModule
  }
});

export default store;

For different modules, we create them the same store folder and export them as:

export default {
  state: {
    ...
  },
  actions: {
    ...
  },
  mutations: {
    ...
  },
  getters: {
    ...
  }
}

When we refer state, we need to add module-name as its namespace. Suppose we have a module registered as permissions.

this.state.permissions.permissionDetail.permission_id

Here, we are accessing state from an action in a state module.

3.1. How to access state from a plain js file

import store from './index.js';

Now, just use it as store.state.<module_name>.<state>. Notice: we have to refer the state with additional namespace, the module name registered in store/index.js.

3.2. How to access state from a Vuejs component

return this.$store.state.roles.roleDetail;

Need to access it with this.$store.

3.3. How to access store from Vuejs router

In router/index.js

import store from '../store/index.js';

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) {
    if (store.getters.isAuthenticated) {
      next();
    } else {      
      store.dispatch("login");
    }
  } else {
    next();
  }
});
  • Import store just like in plain js file.
  • When use its getters, since the store does not use namespace, we can just any action, mutation and getters without refering the namespace as we must do for state.

3.4. How to access router from Vuejs component

this.$router.push({ name: "Home" });

This direct user to router path “Home”.

4. How to create currying functions to help validation?

In Vuejs, we propertly need to create different kind of “rules” to validate forms, especially textfile.

For example, a rule function which takes input v and check if its length is over maximum range or an other rule which check if the input v contain some speciall characters, like space. Sometimes, we want to create rule which combine multiple simple rule: test if input oversize and contain space. This quickly become tedious. We shall use high order function to solve it.

function makeMaxChars(num) {
  return (v)=>{
    if (v == null || v == undefined){
      return true;
    };    
    return v.length <= num || `Maximum number of characters are limited to ${num}.`
  }
}

const max16Chars = makeMaxChars(16);
const max36Chars = makeMaxChars(36);
const max40Chars = makeMaxChars(40);
const max64Chars = makeMaxChars(64);
const max80Chars = makeMaxChars(80);

Here, we created an function which returns the true function we will use to validate if the characters exceed maximum length defined by num. This is called “clojure”.

Suppose we have a rule function noSpace which test if the input contains spaces.

const noSpace = (v) => {
  if (v == null || v == undefined){
    return true;
  };
  return !v.includes(" ") || "No spaces are allowed";
};

We want to create several different rule functions which validate no-space and not exceed maximum chars. For example, “no space and not exceed 64 chars; no space and not exceed 80 chars”. We need to combine different simple rule into a new rule:

import _ from 'lodash';

function makeValidator(...testFuncs) {
  let funcs = _.cloneDeep(testFuncs);
  let numFuncs = funcs.length;
  let noError = true;

  return (v) => {
    for (let i = 0; i < numFuncs; i++) {
      noError = funcs[i](v);
      if (typeof noError == "string" || !noError) {
        break;
      }
    }

    return noError;
  };
}

export const vId = makeValidator(noSpace, max64Chars);
export const vName = makeValidator(noSpace, max80Chars);
  • Here, we created a feature called “curry”.
  • Notice the deep-copy we used from lodash package to save functions from parameters into local variables.
  • This created rule will validate each rule with the order they passed from parameters. If any of them return not true, it will stop validating and return error message.

5. How to use RBA to control section visibility?

5.1. Authentication

This ensures the user visits certains pages must login. We control which pages need authentication from Vuejs router.

const enableAuth = true;

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { requireAuth: false }
  },
  {
    path: '/permissionManagement',
    name: 'PermissionManagement',
    component: PermissionManagement,
    meta: { requireAuth: enableAuth }
  },  
  {
    path: '/clientManagement',
    name: 'ClientManagement',
    component: ClientManagement,
    meta: {requireAuth: enableAuth}
  }  
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
});


router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requireAuth)) {
    if (store.getters.isAuthenticated) {
      next();
    } else {      
      store.dispatch("login");
    }
  } else {
    next();
  }
});

export default router;
  • Here, we invoke store.getters.isAuthenticated to test if user has login before routing.
  • The isAuthenticated is a getter from store/auth.js. Remember, we don’t need to use module name as namespace except state.

    isAuthenticated: state => {
      return new Date().getTime() < state.expire_at;
    }
    

5.2. Authorization

Authorization refers the process of check if the user has the permissions, roles or scopes to send API request to certain endpoint of backend API service.

Therefore, this part consist of two part: frontend and backend

  • For frontend, we need to ensure to pass the access_token or what ever JWT token which contains scopes you want to check in the request header:

    export function postHelper(uri, api, payload, setAuthInfo = true) {
      return new Promise((resolve, reject) => {
        let headersOption = {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        };
    
        if (setAuthInfo) {
          headersOption.Authorization = 'Bearer ' + store.state.auth.selectedPermissionToken;
        }
    
        fetch(`${uri}${api}`,{
          method: 'POST',
          headers: headersOption,            
          body: JSON.stringify(payload)
        })
          .then(handleResponse)
          .then(json => {
            resolve(json);
          })
          .catch(error => {
            console.log(error);
            reject(`${uri}${api} failed. HTTP Status:` + error.status + ' Error: ' + error.statusText);
          });
      });
    }
    
    export function getHelper(uri, api, payload) {
      return new Promise((resolve, reject) => {
        fetch(`${uri}${api}`,{
          method: 'GET',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + store.state.auth.selectedPermissionToken
          }
        })
          .then(handleResponse)
          .then(json => {
            resolve(json);
          })
          .catch(error => {
            console.log(error);
            reject(`${uri}${api} failed. HTTP Status:` + error.status + ' Error: ' + error.statusText);
          });
      });
    }
    
    • Since send request to backend and process it is so common, we create these two helper functions to help to send request and process the response. To use them:

      return await postHelper(`${getApiUrl()}`, `/api/auth/inspectMe`, {
        'access_token': this.state.auth.access_token
      }, false);
      
      let result =  await getHelper(`${getApiUrl()}`, '/api/roleManagement/listAllRoles');
      commit("setAllRoles", result);
      
    • Embed token into Authorization header: 'Authorization': 'Bearer ' + store.state.auth.selectedPermissionToken.
  • For backend, we use check the token from middleware: 1) verify the token 2) check its scopes against endpoint scope defined.

    const jwt = require('jsonwebtoken');
    const jwtSignKey = process.env.jwtSignKey;
    
    // Defines route path and its permissions/scopes required
    export const authPermissionsMap: any = {
      "listAllUsers": "UserView",
      "addRolesToUser": "UserModify RoleView",
    }
    
    export const checkAuth = (req: any, res:any, next:any) => {
        console.log(">> enAuth: ", enableAuth);
        if(!enableAuth) {
            next();
        } else {
            console.log(">> check middleware");
            let bearerToken = req.headers.authorization;
            console.log(">> bearerToken: ", bearerToken);
            let permissionsToken = bearerToken.slice(bearerToken.indexOf(' ') + 1);
            console.log(">> permissionsToken:", permissionsToken);
            // jwtSignKey is the key used to sign the token
            try {
                let result = jwt.verify(permissionsToken, Buffer.from(jwtSignKey, 'base64'));
                console.log(">> jwt.verify result:", result);
    
                console.log(">> userPermissions:", result.scope);
                let route = req.route.path;
                console.log(">> request.route:", route);
    
                let key = route.slice(route.lastIndexOf("/") + 1);
                let neededPermissions = authPermissionsMap[key];
    
    
                if (!neededPermissions) {
                    // If we didn't specify route in authPermissionsMap, then we let it pass.
                    console.log(`>> ${key} does not need to check permissions`);
                    next();
                } else if (checkPermissions(result.scope, neededPermissions)) {
                    console.log(">> permission granted.");
                    next();
                } else {
                    console.log(">> You do not have enough permissions.");
                    res.status(403).json({message: "You do not have enough permissions."});
                }
            } catch(err) {
                console.log(">> jwt.verify error:", err.message);
                res.status(403).json({tokenVerifyError: err.message});
            }
        }
    };
    
    const checkPermissions = (userPermissions: string, neededPermissions: string): boolean => {
        console.log(">> userPermissions: ", userPermissions);
        console.log(">> neededPermissions: ", neededPermissions);
    
        if (!userPermissions) {
            return false;
        }
    
        if (userPermissions.includes("AllAccess")) {
            return true;
        }
    
        let permissions = neededPermissions.split(" ");
        return permissions.every((each: string)=>{
            return userPermissions.includes(each);
        });
    }
    

    The code self-explaines the process. However, this depends on my understanding, espeically on the auth-provider we used.

    For different auth-provider and different access_token or id_token, you may need to use different function to do the token verification. Here is another example to verify token:

    /// Packages required 
    var jwt = require('jsonwebtoken');
    var jwksClient = require('jwks-rsa');
    
    // More about caching certificates: https://github.com/auth0/node-jwks-rsa as a way to fetch the keys.
    var client = jwksClient({
      cache: true,
      jwksUri: '<Usually_this_is_auth_provider/certs>'
    });
    
    var token = "ID_TOKEN / ACCESS_TOKEN";
    
    // Verify the token
    jwt.verify(token, getKey, {}, function (err, decoded) {
      if (decoded) {
        // Here you will find decoded token
        console.log(decoded);
      }
      if (err) {
        console.log('Unable to Verify with error ', err);
      }
    });
    
    function getKey(header, callback) {
      client.getSigningKey(header.kid, function (err, key) {
        var signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
      });
    }
    

6. How to format Vuejs code?

Vuejs code contains js and html tags, so it is very helpful to format code with a common configuration. So, multiple developer could work on the project with same style. Here, we choose prettier + eslint.

  • What is prettier ?

    Prettier is used to format code.

  • What is eslint?

    Linter is used to validate code to check like non-used variables.

  • What is eslint-config-prettier ?

    Eslint-config-prettier turns off all rules that unnecessary or might conflict with “Prettier”.

  • What is eslint-plugin-prettier ?

    Runs Prettier as an ESLint rule and reports differences as individual ESLint issues.

  • How to use prettier and eslint to format Vuejs project
    1. Install dependencies for your npm project

      npm install --save-dev \
          prettier \
          eslint \
          eslint-plugin-vue \
          eslint-plugin-prettier \
          eslint-config-prettier
      
    2. Configure for npm project, edit/create .eslintrc.js in your project root.

      module.exports = {
          "root": true,
          "extends": [
              "eslint:recommended"
              "plugin:vue/essential",
              "plugin:prettier/recommended"
          ]
      }
      
    3. Format your JS and Vue file from terminal

      ./node_modules/eslint/bin/eslint.js --fix
      
  • Format vuejs code using in Emacs.

    ;; Searches the current files parent directories for the node_modules/.bin/ directory and adds it to the buffer local exec-path.
    ;; This allows Emacs to find project based installs of e.g. eslint.
    (when (maybe-require-package 'add-node-modules-path)
      (after-load 'vue-mode
        (add-hook 'vue-mode-hook 'add-node-modules-path)))
    
    (when (maybe-require-package 'prettier)
      ;; format code for vue-mode before save 
      (add-hook 'vue-mode-hook
                '(lambda ()
                   (add-hook 'before-save-hook
                             'prettier-prettify nil 'local))))
    

    This will format code whenever you save file.

  • References

7. Summary

This document record down my problems when working with Vuejs. I will keep updating this when new one comes.