How the v-permission Directive Works in vue-element-admin: Implementation and Extension Guide
The v-permission directive in vue-element-admin checks the current user's roles against a required array and physically removes the DOM element when access is denied.
The vue-element-admin repository provides a production-ready Vue.js admin dashboard that implements granular role-based access control through both routing guards and template directives. Understanding how the v-permission directive functions enables developers to secure UI components effectively and extend the system for complex authorization scenarios.
Core Implementation of the v-permission Directive
The directive is implemented in src/directive/permission/permission.js and operates entirely on the client side by interfacing with the Vuex store.
Reading Roles from Vuex Store
The directive imports the global store to access the authenticated user's role list. It retrieves the roles array from the user module's getter:
import store from '@/store'
// Inside checkPermission function:
const roles = store.getters && store.getters.roles
This roles array is populated after login through the user/getInfo action, typically fetching data from an authentication endpoint and committing it to the store.
The checkPermission Helper Function
The core logic resides in the checkPermission function, which receives the DOM element (el) and the directive binding value (value). The binding value must be an Array of role strings (e.g., ['admin', 'editor']):
function checkPermission(el, binding) {
const { value } = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`need roles! Like v-permission="['admin','editor']"`)
}
}
The permission check uses Array.prototype.some() to determine if at least one user role exists in the required roles array. This implements an OR logic—access is granted if any single role matches.
DOM Manipulation Strategy
When hasPermission evaluates to false, the directive performs aggressive DOM cleanup:
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
This physically removes the element from its parent node, ensuring the forbidden element never appears in the render tree or DOM inspector. This approach prevents users from inspecting hidden elements, offering stronger security than CSS-based hiding (e.g., display: none).
Lifecycle Hooks and Error Handling
The directive exports an object with two lifecycle hooks:
export default {
inserted(el, binding) {
checkPermission(el, binding)
},
update(el, binding) {
checkPermission(el, binding)
}
}
inserted: Runs when the bound element is inserted into the DOM, performing the initial permission check.update: Runs when the component updates and the binding value changes, re-evaluating permissions dynamically.
The directive includes strict type checking that throws a descriptive error if the binding value is not an array, guiding developers to use the correct syntax during development.
Integration with the Routing Permission System
While v-permission protects individual UI elements, vue-element-admin implements a complementary routing guard system in src/permission.js and src/store/modules/permission.js to protect entire navigation paths.
The router guard reads the same store.getters.roles array used by the directive, ensuring consistency between route-level and component-level access control. The hasPermission function in the permission store module checks route.meta.roles, and filterAsyncRoutes recursively filters the asyncRoutes array based on these permissions.
This architecture ensures that both the navigation menu (via filtered routes) and individual action buttons (via v-permission) derive authorization decisions from the single source of truth in the Vuex store.
Extending v-permission for Custom Role-Based Access Control
The built-in directive supports basic role arrays, but real-world applications often require complex logic such as hierarchical roles, resource-specific permissions, or time-based access. You can extend the system by creating custom directives that reuse the core logic while adding specialized checks.
Creating a Custom Directive with Additional Logic
To extend the directive while preserving the original behavior, create a new file (e.g., src/directive/permission/customPermission.js) that imports the store and implements enhanced checking logic:
// src/directive/permission/customPermission.js
import store from '@/store'
// Reuse the original role check logic
function coreCheck(requiredRoles) {
const userRoles = store.getters && store.getters.roles
return requiredRoles.some(r => userRoles.includes(r))
}
// Example: Additional check for a super-admin flag
function hasSuperAdmin() {
const user = store.getters && store.getters.user
return user && user.isSuperAdmin
}
function checkCustomPermission(el, binding) {
const { value } = binding
if (!Array.isArray(value)) {
throw new Error('v-custom-permission expects an array of roles')
}
const allowed = coreCheck(value) || hasSuperAdmin()
// Instead of removing the element, disable it visually
if (!allowed) {
el.setAttribute('disabled', 'true')
el.classList.add('no-permission')
el.title = 'You do not have permission to use this control'
}
}
export default {
inserted(el, binding) {
checkCustomPermission(el, binding)
},
update(el, binding) {
checkCustomPermission(el, binding)
}
}
This example demonstrates extending the directive to support a super-admin bypass and UI degradation (disabling rather than hiding elements).
Registering the Custom Directive
Register the new directive alongside the original in your directive index file:
// src/directive/index.js
import Vue from 'vue'
import permission from './permission/permission'
import customPermission from './permission/customPermission'
Vue.directive('permission', permission)
Vue.directive('custom-permission', customPermission)
Now you can use both directives in templates:
<!-- Original: removes element if not admin/editor -->
<button v-permission="['admin','editor']">Delete</button>
<!-- Extended: disables element for non-editors unless super-admin -->
<button v-custom-permission="['editor']">Advanced Settings</button>
Adapting Complex Role Data Models
If your application uses complex role objects rather than simple strings (e.g., {name: 'admin', scopes: ['read', 'write']}), adapt the Vuex getter to flatten the data for the directive:
// src/store/modules/user.js
const getters = {
// Return array of role names for directive compatibility
roleNames: state => state.roles.map(r => r.name),
// Keep full objects for complex permission checks elsewhere
roleObjects: state => state.roles
}
Then modify the directive to use store.getters.roleNames instead of store.getters.roles, ensuring compatibility with the existing array-based logic while preserving rich role data for other parts of the application.
Summary
- The
v-permissiondirective insrc/directive/permission/permission.jsremoves DOM elements when the user's Vuex roles array does not intersect with the required roles array. - It uses
Array.prototype.some()for OR-logic matching and physically removes nodes viael.parentNode.removeChild(el)rather than hiding them with CSS. - The directive runs on both
insertedandupdatelifecycle hooks to handle dynamic permission changes. - It shares the single source of truth (
store.getters.roles) with the router guard system insrc/permission.jsandsrc/store/modules/permission.js. - You can extend the directive by creating custom implementations that import the store, reuse the core
checkPermissionlogic, and add custom conditions like super-admin flags or resource-based checks. - Complex role objects can be supported by flattening them in Vuex getters while keeping the directive's array-based interface intact.
Frequently Asked Questions
How does v-permission differ from the router permission guard?
The v-permission directive controls UI element visibility by removing individual buttons or components from the DOM based on roles, while the router permission guard in src/permission.js controls navigation access by filtering route definitions before they render. The directive operates at the component level using store.getters.roles, whereas the router guard uses the same role list to generate accessible route maps via filterAsyncRoutes in src/store/modules/permission.js.
Can I use v-permission with a single role string instead of an array?
No, the directive strictly requires an array of role strings. If you pass a single string like v-permission="'admin'", the directive will throw the error: need roles! Like v-permission="['admin','editor']". Always wrap role requirements in array syntax, even for single roles: v-permission="['admin']".
What happens if the user's roles change after the component is mounted?
The directive responds to role changes dynamically because it implements the update lifecycle hook. When the Vuex store updates store.getters.roles (for example, after a role refresh or user switch), the directive re-evaluates the permission check. If the new role set satisfies the requirement, the element remains; if not, it is removed from the DOM during the update cycle.
Is it safe to rely solely on v-permission for security?
No, v-permission is a UI convenience, not a security mechanism. Since it runs entirely in the browser and can be bypassed by manipulating the DOM or JavaScript console, you must always enforce authorization on the server side for all sensitive operations. Use the directive to improve user experience by hiding irrelevant controls, but never depend on it to protect data or functionality from malicious users.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →