Vuestic UI & AG Grid Tutorial - Build Advanced Vue 3 Datatables with Epicmax
Learn how to integrate AG Grid with Vuestic UI for powerful Vue 3 datatables. Follow our step-by-step tutorial to build feature-rich CRUD tables, implement sorting, filtering, and customization. Explore the best Vue.js open-source tools with Epicmax!

Vuestic UI, the Vue 3 UI framework from Epicmax, have collaborated with AG Grid, the leading datagrid provider, to empower users with feature-rich table functionality.
AG Grid is available as an extension via Vuestic UI, meaning users can add AG Grid tables to their projects with no extra cost or difficulty. The company also provides a ready-made style theme for AG Grid’s library of tables, which integrates with Vuestic global config, keeping AG Grid in sync with other components on the page.
Vuestic selected AG Grid as an excellent solution for complex data handling needs. Features like fixed columns and grouping are too expensive to implement, so it’s more beneficial to use a trusted tool instead of developing something independently. The experience in non open source projects was positive as well.
AG Grid works with all JavaScript frameworks, including React, Angular, and Vue, including Vue 2 and Vue 3. AG Grid saves on development time, providing the perfect mix of out-of-the-box functionality and customization.
For teams upgrading legacy Vue 2 projects, combining AG Grid with Vuestic UI pairs well with professional Vue.js migration services, helping ensure a smooth transition to modern Vue 3 architecture.
You can find the repo here and the code sandbox here.
Requirements
The application we will build is a CRUD page of users - something an admin would use to manage users. You can find something similar in almost any admin dashboard.
Here’s a wireframe for the application:

Can’t work on an app without a plan, so here’s one:
- Scaffold new project
- Make a simple table with image, name, email
- Add active column
- Implement sorting for all columns
- Implement full-text filtering
- Add create, edit and delete functionality

Scaffold new project
npm create vite ag-grid-project -- --template vue-ts
cd ag-grid-project
npm install
npm run dev
If you open the page in browser - you should see default vite page:

npm install vuestic-ui @vuestic/ag-grid-theme sass
import { createApp } from "vue";
import App from "./App.vue";
import { createVuesticEssential } from "vuestic-ui"; // <--
import 'vuestic-ui/dist/vuestic-ui.css' // <--
const app = createApp(App);
app.use(createVuesticEssential()); // <--
app.mount("#app");
Make a simple table with "image", "name", "email"
<template>
<div style="height: 300px; width: 100%;">
<ag-grid-vue
class="ag-theme-vuestic"
style="width: 100%; height: 100%;"
:columnDefs="columnDefs"
:rowData="reactiveUsers"
:modules="modules"
/>
</div>
</template>
<script setup lang="ts">
import { AgGridVue } from '@ag-grid-community/vue3'
import {
ClientSideRowModelModule,
} from '@ag-grid-community/client-side-row-model'
import { users } from '../data/users'
import { reactive } from 'vue'
const modules = [ClientSideRowModelModule]
const reactiveUsers = reactive(users)
const columnDefs = [
{ field: 'name' },
{ field: 'email' },
]
</script>
<style lang="scss">
@import "@vuestic/ag-grid-theme/index.scss";
</style>
type User = {
id: number,
image: string,
name: string,
email: string,
active: boolean,
}
export const users: User[] = [
{
id: 1,
image: 'https://picsum.photos/id/1/50/50',
name: 'Ashley Mcdaniel',
email: 'ashleymcdaniel@nebulean.com',
active: true,
},
...

<template>
<va-avatar :src="url" :size="44" />
</template>
<script setup lang="ts">
import { VaAvatar } from 'vuestic-ui'
import { computed } from 'vue'
const props = defineProps<{params: {value: string}}>()
const url = computed(() => props.params.value)
</script>
const columnDefs = [
{ field: 'image', cellRenderer: UserImage, width: 65 },
...

Add 'active' column
<template>
<va-switch class="user-active" v-model="user.active" size="small" />
</template>
<script setup lang="ts">
import { VaSwitch } from 'vuestic-ui'
import { computed, reactive } from 'vue'
import { User } from '../data/users'
const props = defineProps<{params: {data: User}}>()
// AG Grid strips reactivity for some reason, so we have to force it back.
const user = computed(() => reactive(props.params.data))
</script>
const columnDefs = [
...
{ field: 'active', cellRenderer: UserActive, width: 75 },

const active = computed(() => props.params.data.active)
const setActive = (value) => api.setActive(props.params.data.id, value)
<va-switch :modelValue="active" @modelValue:update="setActive" />
Implement sorting for all columns
const columnDefs = [
{ field: 'image', headerName: '', cellRenderer: UserImage, width: 65 },
{ field: 'name', sortable: true },
{ field: 'email', sortable: true },
{ field: 'active', cellRenderer: UserActive, width: 75, sortable: true },
]

Implement full text filtering
<template>
<va-input
v-model="filter"
placeholder="Filter..."
/>
<div style="height: 300px; width: 100%;">
<ag-grid-vue
class="ag-theme-vuestic"
style="width: 100%; height: 100%;"
:columnDefs="columnDefs"
:rowData="reactiveUsers"
:isExternalFilterPresent="() => true"
:doesExternalFilterPass="doesExternalFilterPass"
@grid-ready="onGridReady"
:modules="modules"
/>
</div>
</template>
<script setup lang="ts">
...
const filter = ref('')
let gridApi: any = null
const onGridReady = (params: any) => {
gridApi = params.api
}
watch(() => filter.value, () => {
gridApi.onFilterChanged()
})
const doesExternalFilterPass = ({ data: user }: {user: User}) => {
return JSON.stringify(user).includes(filter.value)
}
...

Add create, edit and delete functionality
<template>
<div>
<va-button flat icon="edit" @click="editUser(user)" />
<va-button flat icon="close" @click="removeUser(user)" />
</div>
</template>
<script setup lang="ts">
import { VaButton } from 'vuestic-ui'
import { computed, reactive } from 'vue'
import { User } from '../data/users'
const props = defineProps<{params: {data: User, context: {
editUser: (user: User) => {},
removeUser: (user: User) => {},
}}}>()
const { editUser, removeUser } = props.params.context
const user = computed(() => reactive(props.params.data))
</script>
<va-button @click="createUser()" icon="add" text-color="white" color="success" />
<va-modal
:model-value="!!selectedUser"
ok-text="Save"
cancel-text="Cancel"
@cancel="selectedUser = null"
@ok="saveUser()"
>
<va-input class="mb-3 d-block" label="Image" v-model="selectedUser.image"/>
<va-input class="mb-3 d-block" label="Name" v-model="selectedUser.name"/>
<va-input class="mb-3 d-block" label="Email" v-model="selectedUser.email"/>
</va-modal>
const editUser = (user: User) => {
selectedUser.value = { ...user }
}
const removeUser = (userToRemove: User) => {
reactiveUsers.value = reactiveUsers.value.filter(user => user !== userToRemove)
}
const createUser = () => {
selectedUser.value = {
id: Math.ceil(Math.random()*10000),
image: 'https://picsum.photos/id/100/50/50',
name: '',
email: '',
active: true,
}
}
const saveUser = () => {
const currentUser = selectedUser.value
selectedUser.value = null
if (!currentUser) {
return
}
const users = reactiveUsers.value
const existingUser = users.find(user => user.id === currentUser.id)
if (existingUser) {
reactiveUsers.value = reactiveUsers.value.map(user => existingUser === user ? currentUser : user)
} else {
reactiveUsers.value.push(currentUser)
}
}
const agGridContext = {
editUser,
removeUser,
}
<ag-grid-vue
...
:context="agGridContext"
/>


Conclusion
Vue.js is our passion. Open Source is our culture 😍