A directive to govern them all

epoundev

epoundev 👨🏾‍💻

Posted on October 31, 2022

A directive to govern them all

What’s a directive? 🧏🏾‍♂️

A custom directive is defined as an object containing lifecycle hooks similar to those of a component. The hooks receive the element the directive is bound to. Here is an example of a directive that focuses an input when the element is inserted into the DOM by Vue:

<script setup>
// enables v-focus in templates
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>
Enter fullscreen mode Exit fullscreen mode

In <script setup>, any camelCase variable that starts with the v prefix can be used as a custom directive. In the example above, vFocus can be used in the template as v-focus.

If not using <script setup>, custom directives can be registered using the directives option:

export default {
  setup() {
    /*...*/
  },
  directives: {
    // enables v-focus in template
    focus: {
      /* ... */
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It is also common to globally register custom directives at the app level:

const app = createApp({})

// make v-focus usable in all components
app.directive('focus', {
  /* ... */
})
Enter fullscreen mode Exit fullscreen mode

Next, we will do this by defining a plugin

How can we implement a plugin ? 💁🏾‍♂️

A plugin is defined as either an object that exposes an install()
 method, or simply a function that acts as the install function itself. The install function receives the app instance along with additional options passed to app.use(), if any:

const vueGates = {
  install(app, options) {
    // configure the app
  }
}
Enter fullscreen mode Exit fullscreen mode

Plugins are self-contained code that usually add app-level functionality to Vue. This is how we install a plugin:

import { createApp } from 'vue'

const app = createApp({})

app.use(vueGates, {
  /* optional options */
})
Enter fullscreen mode Exit fullscreen mode

Writing my Plugin 🔌

Let's begin by setting up the plugin object. It is recommended to create it in a separate file and export it, as shown below to keep the logic contained and separate.

// plugins/vueGates.js
export default {
  install: (app, options) => {
    // Plugin code goes here
  }
}
Enter fullscreen mode Exit fullscreen mode

We want to create a Gate class. This Gate can allows a user in relation to a certain door (it’s like a function). It’s will work like that.

<template v-gate:can="addAUser">
    <h1> You are able to create a new user </h1>
</template>

<template v-gate:allows="clickButton">
    <button> You can click it cause you're V.I.P </button>
</template>

<template v-gate:authorize="toSeeTheDoc">
    <doc
        :name="doc.name"
        :author="doc.author"
        :src="doc.src" 
    /> 
</template>
Enter fullscreen mode Exit fullscreen mode

Or like that

<template v-if="$gate.can('addAUser')">
    <h1> You are able to create a new user </h1>
</template>

<template v-if="$gate.allows('clickButton')">
    <button> You can click it cause you're V.I.P </button>
</template>

<template v-if="$gate.authorize('toSeeTheDoc')">
    <doc
        :name="doc.name"
        :author="doc.author"
        :src="doc.src" 
    /> 
</template>
Enter fullscreen mode Exit fullscreen mode

At work

First we will create a new vuejs application. I use the vue-cli by typing

npm i vue-cli
vue create VueGate
Enter fullscreen mode Exit fullscreen mode

Create a folder called plugins in the src folder. In this folder, we will create a gate.js file that will contain the Gate class17 octobre 2022 01:00

//src/plugins/VueGate/gate.js
class Gate {
  constructor(user) {
    this.user = user;
    this.policies = {};
    this.abilities = {};
    this.resolveUserFn = () => null;
    this.resolveUserPermissionFn = () => [];
  }

  resolveUser(fn) {
    this.resolveUserFn = fn;
  }

  resolveUserPermission(fn) {
    this.resolveUserFn = fn;
  }

  registerPolicies(policies = {}) {
    Object.keys(policies).forEach((modelName) => {
      this.policies[modelName] = new policies[modelName]();
    });
  }

    registerAbilities(abilities) {
    Object.keys(abilities).forEach((name) => {
      this.define(name,abilities[name]);
    });
  }

  define(name, fn) {
    this.abilities[name] = fn;
  }

  allows(abilities = [], ...args) {
    if (typeof abilities === "string") {
      abilities = [abilities];
    }

    const user = this.resolveUserFn();

    if (!user) {
      return false;
    }

    return abilities.every((ability) => {
      if (this.abilities[ability]) {
        if (typeof this.abilities[ability] === "function") {
          return this.abilities[ability].call(null, user, ...args);
        }

        return false;
      }

      const model = args[0];

      if (!model) {
        return user.allPermissions.includes(ability);
      }

      const policy = this.policies[model.constructor.name];

      if (!policy) {
        return false;
      }

      if (typeof policy[ability] === "function") {
        return policy[ability].call(policy, user, ...args);
      }

      return false;
    });
  }

  denies(abilities = [], ...args) {
    return !this.allows(abilities, args);
  }

  can(permissions) {
    const allPermissions = this.resolveUserPermissionFn();
    if (allPermissions.length === 0) {
      console.warn(
        "You have to define the ways to get all permission of a user throught use resolveUserPermission method"
      );
    }
    return allPermissions.includes(permissions);
  }

  cannot(permissions) {
    return !this.can(permissions);
  }
}
const gate = new Gate();

export default gate;
Enter fullscreen mode Exit fullscreen mode

Then we will create the plugin

//src/plugins/VueGate/index.js
import gate from "./gate";

export default {
  install: (app, options) => {
        //options for gate's class
    gate.resolveUser(() => options.user || {});
    gate.registerPolicies(options.policies || {});
    gate.registerAbilities(options.abilities || {});

    app.directive("gate", {
      beforeMount: (el, binding,vnode) => {
        if (["can","cannot", "allows","denies","authorized", "unauthorized"].includes(binding.arg)) {
          if (!gate[binding.arg](binding.value)) {
            el.style.display = "none";
            vnode.el.remove();
          }
        }
      },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Our plugin thus created, we will use it

//src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import VueGates from "./plugins/vueGates";

createApp(App)
  .use(VueGates)
  .mount("#app");
Enter fullscreen mode Exit fullscreen mode

As you saw in the src/plugins/VueGate/index.js file, the Gate class needs some options. Let’s create it

//src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import VueGates from "./plugins/vueGates";

const user = {
  id: 12062001,
  name: "Epoundor",
};
const policies= {}
const abilities = {};
createApp(App)
  .use(VueGates, {
      user,
      policies,
      abilities
  })
  .mount("#app");
Enter fullscreen mode Exit fullscreen mode

Now you can use the v-gate directive everywhere in our app

17 octobre 2022 We gonna try it out . First of all define a new abilities

//src/main.js

const abilities = {
  seeImage: (user) => {
    return typeof user.id === "string";
  },
};
Enter fullscreen mode Exit fullscreen mode

Then in your component, use the v-gate:allows=”’seeImage’” directive

<template>
  <img alt="Vue logo" src="./assets/logo.png" v-gate:allows="'seeImage'">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
Enter fullscreen mode Exit fullscreen mode

The Vue’s logo will disappear

Vue app without logo

Vue app without logo

We want pass it to a v-if or a v-show directive like

<template>
  <img alt="Vue logo" src="./assets/logo.png" v-if="$gate.allows('seeImage')">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
Enter fullscreen mode Exit fullscreen mode

Let's modify our plugin a little by adding a line

//src/plugins/VueGate/index.js

import gate from "./gate";

export default {
  install: (app, options) => {
    // // Plugin code goes here
    gate.resolveUser(() => options.user || {});
    gate.registerPolicies(options.policies || {});
    gate.registerAbilities(options.abilities || {});

        //We define $gate as global property
    app.config.globalProperties.$gate = gate;

    app.directive("gate", {
      beforeMount: (el, binding,vnode) => {
        if (["can","cannot", "allows","denies","authorized", "unauthorized"].includes(binding.arg)) {
          if (!gate[binding.arg](binding.value)) {
            el.style.display = "none";
            vnode.el.remove();
          }
        }
      },
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Conclusion 🫡

Now you’re able to create a directive and use it on a application. You will have noticed that in the Gate class I did not define some functions as authorized. I'll let you take care of that

💖 💪 🙅 🚩
epoundev
epoundev 👨🏾‍💻

Posted on October 31, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related