Skip to content

Vue UI Guide

SA plugins use Vue 3 Single File Components (SFCs) with PrimeVue v3 for UI components and PrimeFlex for CSS utility classes. All PrimeVue components are registered globally — no imports needed in templates.

Vue Files in a Plugin

FilePurpose
widget.vueDashboard widget (registered via addWidget)
setting.vueSettings page (registered via addPage(..., "setting"))
Any other .vueAdditional pages registered via addPage()

Import other .vue files from your own plugin using the vue: prefix:

js
import MyDialog from "vue:my-plugin/dialogs/confirm.vue";

export default {
  components: { MyDialog }
}

Widget Layout

Widgets must wrap their root in the SA dashboard grid column — this is required for correct placement on the dashboard:

vue
<template>
    <div class="col-12 lg:col-6 xl:col-3 p-1">
        <div class="card h-full">
            <h5>{{ $t("my-plugin.name") }}</h5>
            <p>{{ value }}</p>
        </div>
    </div>
</template>

<script>
import api from 'api';

export default {
    data() {
        return { value: null }
    },
    created() {
        this.value = api.database("my-plugin").readJson(["data.json"]);
    }
}
</script>

Omitting the col-12 lg:col-6 xl:col-3 p-1 wrapper will break the dashboard grid layout.

Page Layout

Standard pages use the card class for panels:

vue
<template>
    <div>
        <!-- header card -->
        <div class="card flex align-items-center justify-content-between">
            <h5 class="m-0">{{ $t("my-plugin.name") }}</h5>
            <div>
                <Button icon="pi pi-save" class="p-button-text" @click="save()" />
                <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
            </div>
        </div>

        <!-- content card -->
        <div class="card">
            <!-- content here -->
        </div>
    </div>
</template>

Forms

Use the p-fluid formgrid grid pattern for responsive forms:

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</label>
                <InputText id="name" v-model="form.name" />
            </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>

Inline Editable Tables

SA plugins frequently use DataTable with editMode="row" for inline editing — ideal for settings lists:

vue
<template>
    <DataTable
        :value="items"
        v-model:editingRows="editingRows"
        editMode="row"
        @row-edit-save="onRowEditSave"
        responsiveLayout="scroll"
        class="card"
        :rows="20"
        :paginator="items.length > 20"
    >
        <template #header>
            <div class="flex align-items-center justify-content-between">
                {{ $t("my-plugin.name") }}
                <div>
                    <Button icon="pi pi-save" class="p-button-text" @click="save()" />
                    <Button icon="pi pi-plus" class="p-button-text" @click="items.push({})" />
                    <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
                </div>
            </div>
        </template>

        <Column field="name" header="Name" :sortable="true">
            <template #editor="{ data, field }">
                <InputText v-model="data[field]" class="w-full" />
            </template>
        </Column>

        <Column field="icon" header="Icon" :sortable="true">
            <template #editor="{ data, field }">
                <div class="p-inputgroup flex-1">
                    <InputText v-model="data[field]" readonly />
                    <Button icon="pi pi-search" @click="openIcons(data, field)" />
                </div>
            </template>
        </Column>

        <Column headerStyle="width:0px;" bodyStyle="text-align:end">
            <template #body="{ index }">
                <Button icon="pi pi-trash" class="p-button-text p-button-danger" @click="remove(index)" />
            </template>
        </Column>
        <Column :rowEditor="true" headerStyle="width: 110px;" bodyStyle="text-align:end" />
    </DataTable>
</template>

<script>
import api from 'api';
import { EventBus } from 'global';

export default {
    data() {
        return { items: [], editingRows: [] };
    },
    created() {
        this.init();
    },
    methods: {
        init() {
            this.items = api.database("my-plugin").readJson(["data.json"]);
        },
        onRowEditSave({ newData, index }) {
            this.items[index] = newData;
        },
        openIcons(data, field) {
            EventBus.emit("openIcons", icon => {
                data[field] = icon;
            });
        },
        remove(index) {
            this.$confirm.require({
                header: this.$t("text.confirmation"),
                message: this.$t("text.areSure"),
                acceptClass: "p-button-danger",
                accept: () => this.items.splice(index, 1)
            });
        },
        save() {
            api.database("my-plugin").writeJson(["data.json"], this.items);
            this.$toast.add({ severity: "success", summary: this.$t("my-plugin.name"), detail: this.$t("text.done") });
        }
    }
}
</script>

Standard DataTable (read-only list)

vue
<template>
    <DataTable
        :value="items"
        :rows="20"
        :paginator="items.length > 20"
        v-model:filters="filters"
        filterDisplay="row"
        dataKey="id"
        responsiveLayout="scroll"
        class="card mb-2"
    >
        <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 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>
        </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>

Dialogs

vue
<template>
    <Dialog
        header="Edit Item"
        v-model:visible="dialog"
        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>
        <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>

Notifications (Toast)

js
this.$toast.add({ severity: 'success', summary: 'Saved', detail: 'Data saved successfully' });
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'Something went wrong' });
this.$toast.add({ severity: 'warn', summary: 'Warning', detail: 'Check your input' });

Confirm Dialog

js
this.$confirm.require({
    header: this.$t("text.confirmation"),
    message: this.$t("text.areSure"),
    acceptLabel: this.$t("text.yes"),
    rejectLabel: this.$t("text.no"),
    acceptClass: "p-button-danger",
    accept: () => {
        // delete logic
    }
});

Routing

SA routes are registered via addPage() in script.js. Navigate with $router and read params with $route:

js
// Navigate to a page
this.$router.push(`/app/my-plugin/items`);
this.$router.push(`/app/my-plugin/items/${id}`);

// Navigate to a settings page
this.$router.push(`/app/setting/my-plugin`);

// Read dynamic param
const id = this.$route.params.id;

// React to param changes
watch: {
    '$route.params.id'(id, old) {
        if (id && id !== old) this.init();
    }
}

Database (api module)

SA uses the local api module instead of HTTP calls:

js
import api from 'api';

// Read JSON file
const data = api.database("my-plugin").readJson(["data.json"]);

// Write JSON file
api.database("my-plugin").writeJson(["data.json"], data);

// Nested path
const items = api.database("my-plugin").readJson(["subfolder", "items.json"]);

Icon Picker

js
import { EventBus } from 'global';

openIcons(data, field) {
    EventBus.emit("openIcons", icon => {
        data[field] = icon; // e.g. "mdi mdi-home" or "pi pi-star"
    });
}

Icons

PrimeIconspi prefix:

html
<i class="pi pi-check"></i>
<Button icon="pi pi-plus" />

Material Design Icons (MDI)mdi prefix:

html
<i class="mdi mdi-bell-outline"></i>
<Button icon="mdi mdi-link" />

i18n

Translations registered via addLanguage() in script.js:

vue
<template>
    <div>
        <h5>{{ $t("my-plugin.name") }}</h5>
        <Button :label="$t('button.save')" />
    </div>
</template>

PrimeFlex Reference

CategoryClasses
Flex layoutflex, align-items-center, justify-content-between, justify-content-end, gap-2
Gridgrid, col-12, col-6, md:col-6, lg:col-6, xl:col-3
Formp-fluid, formgrid, field
Spacingm-0, mb-2, mb-3, mb-4, mt-1, p-1, p-3
Widthw-full
Text colortext-blue-500, text-red-500, text-green-500

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

Common PrimeVue Components

ComponentUsage
ButtonActions, icon buttons
InputTextText input
InputNumberNumeric input
InputSwitchBoolean toggle
TextareaMulti-line text
DropdownSingle select
MultiSelectMulti select
DataTable + ColumnTabular data (standard or inline editable)
DialogModal overlay
ChipInline label
CardContent surface with title slot
ToastNotifications via $toast.add()
ConfirmDialogConfirmation via $confirm.require()