A directive to govern them all
epoundev 👨🏾💻
Posted on October 31, 2022
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>
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: {
/* ... */
}
}
}
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', {
/* ... */
})
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
}
}
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 */
})
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
}
}
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>
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>
At work
First we will create a new vuejs application. I use the vue-cli by typing
npm i vue-cli
vue create VueGate
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;
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();
}
}
},
});
},
};
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");
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");
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";
},
};
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>
The Vue’s logo will disappear
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>
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();
}
}
},
});
},
};
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
Posted on October 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.