Appearance
Vue UI Guide
EA plugins use Vue 3 Single File Components (SFCs) with PrimeVue v3 for UI components and PrimeFlex for CSS utility classes. All components are registered globally — no imports needed for PrimeVue components in templates.
Vue Files in a Plugin
A plugin can have multiple Vue files. The two special ones registered in script.js are:
| File | Purpose |
|---|---|
widget.vue | Dashboard widget (added via addWidget) |
setting.vue | Settings page (added via addToSetting) |
You can also create any number of additional .vue files as sub-pages or dialogs, and import them like this:
js
// Import a Vue file from your own plugin
import MyDialog from "vue:my-plugin/my-dialog.vue";
export default {
components: { MyDialog }
}The vue:plugin-name/path.vue syntax resolves files relative to your plugin's version folder.
Page Layout
Wrap every page in a <div> root. Use the card class for white surface panels.
vue
<template>
<div>
<!-- page header -->
<div class="card flex align-items-center justify-content-between">
<h5 class="m-0">Page Title</h5>
<div>
<Button label="Save" icon="pi pi-save" class="p-button-text" @click="save()" />
</div>
</div>
<!-- content card -->
<div class="card">
<!-- content here -->
</div>
</div>
</template>Forms
Use the p-fluid formgrid grid pattern for responsive forms. Each field gets field col-12 md:col-6.
vue
<template>
<div class="card">
<div class="p-fluid formgrid grid">
<div class="field col-12 md:col-6">
<label for="name" class="block mb-2">Name <span class="text-red-500">*</span></label>
<InputText id="name" v-model="form.name" :class="{ 'p-invalid': errors.name }" />
<small v-if="errors.name" class="p-error block mt-1">{{ errors.name }}</small>
</div>
<div class="field col-12 md:col-6">
<label for="type" class="block mb-2">Type</label>
<Dropdown
id="type"
v-model="form.type"
:options="[{ label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }]"
optionLabel="label"
optionValue="value"
class="w-full"
/>
</div>
<div class="field col-12 md:col-6">
<label for="count" class="block mb-2">Count</label>
<InputNumber id="count" v-model="form.count" :min="0" class="w-full" />
</div>
<div class="field col-12">
<label for="notes" class="block mb-2">Notes</label>
<Textarea id="notes" v-model="form.notes" rows="4" />
</div>
<div class="field col-12 md:col-4">
<label for="active" class="block mb-2">Active</label>
<InputSwitch id="active" v-model="form.active" />
</div>
</div>
<div class="flex justify-content-end gap-2">
<Button label="Cancel" icon="pi pi-times" class="p-button-text" @click="cancel" />
<Button label="Save" icon="pi pi-save" :loading="saving" @click="save" />
</div>
</div>
</template>Data Tables
Use PrimeVue DataTable with Column for listing records.
vue
<template>
<DataTable
:value="items"
:rows="20"
:paginator="items.length > 20"
v-model:filters="filters"
v-model:selection="selected"
:globalFilterFields="['name', 'type']"
filterDisplay="row"
dataKey="id"
responsiveLayout="scroll"
class="card mb-2"
@row-click="open($event.data.id)"
>
<template #header>
<div class="flex align-items-center justify-content-between">
Items
<div>
<Button icon="pi pi-plus" class="p-button-text" @click="open('add')" />
<Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
</div>
</div>
</template>
<Column selectionMode="multiple" headerStyle="width: 3rem" />
<Column field="id" header="ID" :sortable="true" />
<Column field="name" header="Name" :sortable="true">
<template #filter="{ filterModel, filterCallback }">
<InputText v-model="filterModel.value" @input="filterCallback()" placeholder="Search" />
</template>
</Column>
<Column field="type" header="Type" :sortable="true" :showFilterMenu="false">
<template #body="{ data }">
<Chip :label="data.type" />
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown
v-model="filterModel.value"
@change="filterCallback()"
:options="['admin', 'user']"
:showClear="true"
style="min-width: 10rem"
/>
</template>
</Column>
<Column header="Actions">
<template #body="{ data }">
<div class="flex gap-2">
<Button icon="pi pi-pencil" v-tooltip.top="'Edit'" class="p-button-text" @click="open(data.id)" />
<Button icon="pi pi-trash" v-tooltip.top="'Delete'" class="p-button-text p-button-danger" @click="remove(data)" />
</div>
</template>
</Column>
</DataTable>
</template>
<script>
export default {
data() {
return {
items: [],
selected: [],
filters: {
name: { value: null },
type: { value: null, matchMode: 'equals' }
}
};
}
}
</script>Dialogs
Always set appendTo="#containerElement" on dialogs — this mounts them inside the EA frame instead of the document body.
vue
<template>
<Dialog
header="Edit Item"
v-model:visible="dialog"
appendTo="#containerElement"
modal
:draggable="false"
:style="{ width: '600px' }"
>
<div class="p-fluid formgrid grid">
<div class="field col-12">
<label>Title</label>
<InputText v-model="form.title" />
</div>
<div class="field col-12">
<label>Message</label>
<Textarea :rows="5" v-model="form.message" style="resize: vertical;" />
</div>
</div>
<template #footer>
<Button label="Cancel" icon="pi pi-times" class="p-button-text" severity="danger" @click="dialog = false" />
<Button label="Save" icon="pi pi-check" @click="save" autofocus />
</template>
</Dialog>
</template>
<script>
export default {
data: () => ({ dialog: false, form: {} }),
methods: {
open(item) {
this.form = { ...item };
this.dialog = true;
}
}
}
</script>Notifications (Toast)
js
// Success
this.$toast.add({ severity: 'success', summary: 'Saved', detail: 'Data saved successfully' });
// Error
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'Something went wrong' });
// Warning
this.$toast.add({ severity: 'warn', summary: 'Warning', detail: 'Check your input' });Confirm Dialog
js
this.$confirm.require({
message: 'Are you sure you want to delete this item?',
header: 'Confirm',
icon: 'pi pi-exclamation-triangle',
accept: () => {
// delete logic
}
});Routing
EA plugins run inside a router context. Use this.$router and this.$route to navigate and read params.
js
// Navigate to a sub-page
this.$router.push(`/app/my-plugin/users/add`);
this.$router.push(`/app/my-plugin/users/${id}`);
// Read route param
const id = this.$route.params.id;
// React to param changes
watch: {
'$route.params.id'(id, old) {
if (id && id !== old) this.init();
}
}Icons
Two icon sets are available:
PrimeIcons — use the pi prefix:
html
<i class="pi pi-check"></i>
<Button icon="pi pi-plus" />Material Design Icons (MDI) — use the mdi prefix:
html
<i class="mdi mdi-bell-outline"></i>
<Button icon="mdi mdi-music-note" />Browse MDI icons at pictogrammers.com/library/mdi.
File Explorer Integration
To let users pick a file path using the EA file explorer, use EventBus and app:
js
import { EventBus } from "global";
import app from "app";
openFilePicker() {
EventBus.emit("openFileExplorer", {
type: "file",
exe: [], // allowed extensions, empty = all
callback: (ele) => {
// ele.path is a relative path array
this.form.image = [...app.role.fileExplorer, ...ele.path].join(",");
}
});
}The comma-joined path is the format EA uses for file references in the database.
HTTP Requests
Use axios for all API calls. Plugin routes mounted by main.js are available at plugins/plugin-name/.... EA platform routes are available directly by their path.
js
import axios from "axios";
// Call your own plugin's backend (mounted by main.js)
const res = await axios.get(`my-plugin/items`);
// Read from the JSON database
const data = (
await axios.get(`plugins/database?path=${["my-plugin", "data.json"].join(",")}`)
).data;
// Write to the JSON database
await axios.post(
`plugins/database?path=${["my-plugin", "data.json"].join(",")}`,
{ data: this.items }
);i18n
Translations registered in script.js via this.addLanguage() are available in templates:
vue
<template>
<div>
<h5>{{ $t('myPlugin.title') }}</h5>
<Button :label="$t('button.save')" />
</div>
</template>See the script.js API Reference for how to register translations.
Role-Based Visibility
Permissions registered via this.addRole() are checked in templates with the global role() helper:
vue
<template>
<div>
<Button
v-if="role('my-plugin', 'canDelete')"
icon="pi pi-trash"
class="p-button-danger"
@click="remove"
/>
</div>
</template>See the script.js API Reference for how to define roles.
PrimeFlex Reference
Common utility classes used in EA plugins:
| Category | Classes |
|---|---|
| Flex layout | flex, align-items-center, justify-content-between, justify-content-end, gap-2 |
| Grid | grid, col-12, col-6, md:col-6, md:col-4 |
| Form | p-fluid, formgrid, field |
| Spacing | m-0, mb-2, mb-3, mb-4, mt-1, p-3, px-5 |
| Width | w-full |
| Text color | text-red-500, text-green-500, text-gray-600, text-primary |
| Border | border-1, border-round |
Full docs: v3.primevue.org · PrimeFlex: primeflex.org
Common PrimeVue Components
| Component | Usage |
|---|---|
Button | Actions, form submit, icon buttons |
InputText | Text input |
InputNumber | Numeric input with min/max |
InputSwitch | Boolean toggle |
Textarea | Multi-line text |
Dropdown | Single select |
MultiSelect | Multi select with chip display |
DataTable + Column | Tabular data with sort/filter/pagination |
Dialog | Modal (always use appendTo="#containerElement") |
Chip | Inline label badge |
Tag | Status badge |
Toast | Notifications (via this.$toast.add()) |
ConfirmDialog | Confirmation prompt (via this.$confirm.require()) |
Full component docs: v3.primevue.org