Skip to content

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:

FilePurpose
widget.vueDashboard widget (added via addWidget)
setting.vueSettings 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:

CategoryClasses
Flex layoutflex, align-items-center, justify-content-between, justify-content-end, gap-2
Gridgrid, col-12, col-6, md:col-6, md:col-4
Formp-fluid, formgrid, field
Spacingm-0, mb-2, mb-3, mb-4, mt-1, p-3, px-5
Widthw-full
Text colortext-red-500, text-green-500, text-gray-600, text-primary
Borderborder-1, border-round

Full docs: v3.primevue.org · PrimeFlex: primeflex.org

Common PrimeVue Components

ComponentUsage
ButtonActions, form submit, icon buttons
InputTextText input
InputNumberNumeric input with min/max
InputSwitchBoolean toggle
TextareaMulti-line text
DropdownSingle select
MultiSelectMulti select with chip display
DataTable + ColumnTabular data with sort/filter/pagination
DialogModal (always use appendTo="#containerElement")
ChipInline label badge
TagStatus badge
ToastNotifications (via this.$toast.add())
ConfirmDialogConfirmation prompt (via this.$confirm.require())

Full component docs: v3.primevue.org