This package allows to integrate @casl/ability
with Vue application. So, you can show or hide UI elements based on user ability to see them. This package provides a Vue plugin which defines $ability
object and $can
method for all components. Also package provides functional Can
component (not included in the plugin), both allow to hide or show UI elements based on the user ability to see them.
npm install @casl/vue @casl/ability
# or
yarn add @casl/vue @casl/ability
# or
pnpm add @casl/vue @casl/ability
If you don't plan to use multiple Ability
instances across your application (99.9% likelihood that you don't), you can pass Ability
instance as a 2nd argument to Vue.use
:
import Vue from 'vue';
import { abilitiesPlugin } from '@casl/vue';
import ability from './services/ability';
Vue.use(abilitiesPlugin, ability);
but if you one from that 0.1%, you need to pass it in Vue
constructor:
import Vue from 'vue';
import { abilitiesPlugin } from '@casl/vue';
import ability from './services/ability';
Vue.use(abilitiesPlugin);
new Vue({
el: '#app',
ability
})
The difference is that the 1st approach defines Ability
instance on Vue.prototype
and 2nd one passes ability from parent to child in component tree.
The 2nd approach potentially may slowdown components creation but you will not notice this ;)
The plugin doesn't register Can
component, so you can decide whether to use it or not. In most cases, $can
function is enough and it's more lightweight than Can
component.
To use Can
functional component, you need to import it in a particular component or register it globally:
import Vue from 'vue';
import { Can } from '@casl/vue';
Vue.component('Can', Can);
See CASL guide to learn how to define
Ability
instance.
To check permissions, you can use $can
method in any component, it accepts the same arguments as Ability
's can
:
<template>
<div v-if="$can('create', 'Post')">
<a @click="createPost">Add Post</a>
</div>
</template>
There is an alternative way you can check your permissions in the app by using the Can
component. Instead of using v-if="$can(...)"
, we can do this:
<template>
<Can I="create" a="Post">
<a @click="createPost">Add Post</a>
</Can>
</template>
It accepts default slot and 5 properties:
-
do
- name of the action (e.g.,read
,update
). Has an aliasI
-
on
- checked subject. Hasa
,an
,this
aliases -
field
- checked field<Can I="read" :this="post" field="title"> Yes, you can do this! ;) </Can>
-
not
- inverts ability check and show UI if user cannot do some action:<Can not I="create" a="Post"> You are not allowed to create a post </Can>
-
passThrough
- renders children in spite of whatability.can
returns. This is useful for creating custom components based onCan
. For example, if you need to disable button based on user permissions:<template> <div> <Can I="delete" a="Post" passThrough v-slot="{ allowed }"> <button :disabled="!allowed">Delete post</button> </Can> </div> </template>
Can
component has several downsides in comparison to $can
function.
- It's more expensive to use because Vue needs to spend some time creating it
- It adds additional nesting, that makes code harder to read
As you can see from the code above, the component name and its property names and values create an English sentence, actually a question. The example above reads as "Can I create a Post?".
There are several other property aliases which allow constructing a readable question:
-
use the
a
(oran
) alias when you check by Type<Can I="read" a="Post">...</Can>
-
use
this
alias instead ofa
when you check action on a particular instance. So, the question can be read as "Can I read this particular post?"<Can I="read" :this="post">...</Can>
-
use
do
andon
if you are bored and don't want to make your code more readable :)<Can do="read" :on="post">...</Can> <Can do="read" :on="post" field="title">...</Can>
The package is written in TypeScript, so don't worry that you need to keep all the properties and aliases in mind. If you use TypeScript, your IDE will suggest you the correct usage and TypeScript will warn you if you make a mistake.
To define application specific Ability
type, create a separate file, for example:
import { Ability, AbilityClass } from '@casl/ability';
type Actions = 'create' | 'read' | 'update' | 'delete';
type Subjects = 'Article' | 'User'
export type AppAbility = Ability<[Actions, Subjects]>;
export const AppAbility = Ability as AbilityClass<AppAbility>;
By default, Vue['$ability']
is declared as AnyAbility
type. So, to make it more useful for our app, we need to redeclare Vue['$ability']
type. To do so, create src/shims-ability.d.ts
file:
import { AppAbility } from './AppAbility'
declare module 'vue/types/vue' {
interface Vue {
$ability: AppAbility;
$can(this: this, ...args: Parameters<this['$ability']['can']>): boolean;
}
}
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
ability?: AppAbility;
}
}
And update tsconfig.json
to replace default vue modules augmentation (i.e., @casl/vue/patch
) with application specific:
{
"compilerOptions": {
// other options
"baseUrl": ".",
"paths": {
// other mappings
"@casl/vue/patch": [
"src/shims-ability.d.ts"
]
}
},
// other options
}
Read Vue TypeScript to understand why it's so hard to properly type Vue plugins.
Majority of applications that need permission checking support have something like AuthService
or LoginService
or Session
service (name it as you wish) which is responsible for user login/logout functionality. Whenever user login (and logout), we need to update Ability
instance with new rules. Usually you will do this in your LoginComponent
.
Let's imagine that server returns user with a role on login:
<template>
<form @submit.prevent="login">
<input type="email" v-model="email" />
<input type="password" v-model="password" />
<button type="submit">Login</button>
</form>
</template>
<script>
import { AbilityBuilder } from '@casl/ability';
export default {
name: 'LoginForm',
data: () => ({
email: '',
password: ''
}),
methods: {
login() {
const { email, password } = this;
const params = { method: 'POST', body: JSON.stringify({ email, password }) };
return fetch('path/to/api/login', params)
.then(response => response.json())
.then(({ user }) => this.updateAbility(user));
},
updateAbility(user) {
const { can, rules } = new AbilityBuilder();
if (user.role === 'admin') {
can('manage', 'all');
} else {
can('read', 'all');
}
this.$ability.update(rules);
}
}
};
</script>
See Define rules to get more information of how to define
Ability
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for contributing.
If you'd like to help us sustain our community and project, consider to become a financial contributor on Open Collective
See Support CASL for details