IRIS App Vue.js Component Development
Overview
IRIS Apps are built with Vue 3, TypeScript, and the Composition API using <script setup>. This section covers the component structure rules, coding conventions, data fetching patterns, and common pitfalls specific to the Magentrix platform.
These rules are derived from the official Magentrix Vue Coding Guidelines. Following them consistently ensures components are readable, maintainable, and compatible with the platform's build and deployment pipeline.
Component Structure
Every IRIS App component has three sections — <script setup>, <template>, and <style scoped> — each with specific ordering and syntax rules.
Script Section Rules
| Rule | Detail |
|---|
| No semicolons | Do not end statements with ; |
| Functions vs arrow functions | Use the function keyword for standalone (pure) functions. Use arrow functions only as callback parameters passed to other functions. |
| TypeScript types required | Specify parameter types and return types on all functions. |
| Avoid watchers | Watchers are expensive. Use them only when reactivity through computed or event-based patterns cannot solve the problem. |
Avoid onUpdated | Do not use the onUpdated lifecycle hook unless strictly required. |
| Meaningful names | Use descriptive variable and function names. Avoid abbreviations and single-character names outside of short-lived loop variables. |
Logic Order Inside <script setup>
Organise all declarations and logic in the following order:
- Props
- Emits
- Refs
- Vars
- Lifecycle Hooks
- Computed
- Watches
- Functions
Import Order
Group and order imports in the following sequence with no blank lines between groups:
- Vue framework imports
- Third-party library imports
- Local imports
- CSS imports
import { onMounted, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { MagentrixError, type MagentrixConfig } from '@magentrix-corp/magentrix-sdk'
import { useMagentrixSdk } from '@magentrix-corp/magentrix-sdk/vue'
import AccountCard from '@/components/AccountCard.vue'
import '@/assets/styles/accounts.css'
Template Section Rules
- Use kebab-case for multi-word component names and attribute names.
<!-- ✅ Correct -->
<account-list-item :max-items="20" />
<!-- ❌ Avoid -->
<AccountListItem :maxItems="20" />
Style Section Rules
- Always use
<style scoped> for component-specific styles. - Place
@use imports at the very top of the style block. - Never apply styles to
html, head, or body.
<style scoped>
@use '@/assets/styles/mixins.scss';
.account-card {
border: 1px solid var(--mag-element-border-color);
border-radius: var(--mag-border-radius);
}
</style>
Complete Component Example
The following example demonstrates a well-structured component following all conventions — correct import order, logic order, function vs arrow function usage, and TypeScript types throughout.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { MagentrixError, RequestMethod, type MagentrixConfig } from '@magentrix-corp/magentrix-sdk'
import { useMagentrixSdk } from '@magentrix-corp/magentrix-sdk/vue'
// 1. Props
const props = defineProps<{
title: string
pageSize?: number
}>()
// 2. Emits
const emit = defineEmits<{
(e: 'select', accountId: string): void
}>()
// 3. Refs
const accounts = ref<any[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const currentPage = ref(1)
// 4. Vars
const config: MagentrixConfig = {
baseUrl: import.meta.env.VITE_SITE_URL,
refreshToken: import.meta.env.VITE_REFRESH_TOKEN,
isDevMode: import.meta.env.DEV
}
const dataService = useMagentrixSdk().getInstance(config)
// 5. Lifecycle Hooks
onMounted(() => {
loadAccounts()
})
// 6. Computed
const totalPages = computed((): number => {
return Math.ceil(accounts.value.length / (props.pageSize ?? 25))
})
// 7. Watches — omitted (not needed here)
// 8. Functions
async function loadAccounts(): Promise<void> {
loading.value = true
error.value = null
try {
const size = props.pageSize ?? 25
const offset = (currentPage.value - 1) * size
const result = await dataService.query(
`SELECT Id, Name, Status FROM Account WHERE IsActive = true ORDER BY Name ASC LIMIT ${size}, ${offset}`
)
accounts.value = result.data
} catch (err) {
if (err instanceof MagentrixError) {
error.value = err.message
}
} finally {
loading.value = false
}
}
function selectAccount(accountId: string): void {
emit('select', accountId)
}
function goToPage(page: number): void {
currentPage.value = page
loadAccounts()
}
</script>
<template>
<div class="account-list">
<h2>{{ title }}</h2>
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="alert alert-danger">{{ error }}</div>
<ul v-else>
<li
v-for="account in accounts"
:key="account.Id"
@click="selectAccount(account.Id)"
>
{{ account.Name }}
</li>
</ul>
<div class="pagination">
<button
v-for="page in totalPages"
:key="page"
class="btn-primary"
@click="goToPage(page)"
>
{{ page }}
</button>
</div>
</div>
</template>
<style scoped>
.account-list {
padding: 1rem;
background: var(--mag-page-bg-color);
}
.pagination {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
</style>
Data Fetching Patterns
Standard Query on Mount
The most common pattern — fetch a list of records when the component mounts.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { MagentrixError, type MagentrixConfig } from '@magentrix-corp/magentrix-sdk'
import { useMagentrixSdk } from '@magentrix-corp/magentrix-sdk/vue'
const contacts = ref<any[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const config: MagentrixConfig = {
baseUrl: import.meta.env.VITE_SITE_URL,
refreshToken: import.meta.env.VITE_REFRESH_TOKEN,
isDevMode: import.meta.env.DEV
}
const dataService = useMagentrixSdk().getInstance(config)
onMounted(() => {
loadContacts()
})
async function loadContacts(): Promise<void> {
loading.value = true
error.value = null
try {
const result = await dataService.query(
'SELECT Id, Name, Email FROM Contact WHERE IsActive = true ORDER BY Name ASC'
)
contacts.value = result.data
} catch (err) {
if (err instanceof MagentrixError) {
error.value = err.message
}
} finally {
loading.value = false
}
}
</script>
Create, Edit, and Delete
import { MagentrixError, DatabaseError } from '@magentrix-corp/magentrix-sdk'
// Create
async function createAccount(name: string, email: string): Promise<void> {
try {
await dataService.create('Account', { Name: name, Email: email, IsActive: true })
await loadAccounts()
} catch (err) {
if (err instanceof DatabaseError) {
err.getErrors().forEach(e => console.error(`${e.fieldName}: ${e.message}`))
} else if (err instanceof MagentrixError) {
error.value = err.message
}
}
}
// Edit
async function updateAccountStatus(id: string, status: string): Promise<void> {
try {
await dataService.edit('Account', { Id: id, Status: status })
} catch (err) {
if (err instanceof MagentrixError) {
error.value = err.message
}
}
}
// Delete
async function deleteAccount(id: string): Promise<void> {
try {
await dataService.delete('Account', id)
accounts.value = accounts.value.filter(a => a.Id !== id)
} catch (err) {
if (err instanceof MagentrixError) {
error.value = err.message
}
}
}
Calling a Custom Controller
import { MagentrixError, RequestMethod } from '@magentrix-corp/magentrix-sdk'
async function processOrder(accountId: string, amount: number): Promise<void> {
try {
const result = await dataService.execute(
'/acls/order/processorder',
{ accountId, amount }
)
if (result.success) {
await loadAccounts()
}
} catch (err) {
if (err instanceof MagentrixError) {
error.value = err.message
}
}
}
⚠️ Warning: Import RequestMethod from @magentrix-corp/magentrix-sdk, not from @magentrix-corp/magentrix-sdk/vue. Importing it from the Vue subpath will cause a runtime error.
Error Handling Pattern
Always use a loading, error, and data ref pattern so the template can render appropriate states. Reset error at the start of each operation.
const loading = ref(false)
const error = ref<string | null>(null)
const records = ref<any[]>([])
async function loadData(): Promise<void> {
loading.value = true
error.value = null // clear previous error
try {
const result = await dataService.query('SELECT Id, Name FROM Account')
records.value = result.data
} catch (err) {
if (error instanceof DatabaseError && error.errors.length > 0) {
errorMessage.value = error.errors[0].message
return
}
errorMessage.value = "Unexpected error."
} finally {
loading.value = false // always runs
}
}
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="alert alert-danger">{{ error }}</div>
<ul v-else>
<li v-for="record in records" :key="record.Id">{{ record.Name }}</li>
</ul>
</template>
Common Mistakes
Using Arrow Functions for Standalone Functions
// ❌ Avoid — arrow function for a standalone function
const loadAccounts = async (): Promise<void> => {
...
}
// ✅ Correct — function keyword for standalone functions
async function loadAccounts(): Promise<void> {
...
}
Using the function Keyword for Callbacks
// ❌ Avoid — function keyword as a callback parameter
accounts.value.forEach(function(account) {
account.processed = true
})
// ✅ Correct — arrow function for callbacks
accounts.value.forEach((account) => {
account.processed = true
})
Missing TypeScript Types
// ❌ Avoid — no types specified
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// ✅ Correct — parameter and return types specified
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
Calling useMagentrixSdk() Inside an Async Function
// ❌ Fails — composable called inside async context
async function loadData(): Promise<void> {
const dataService = useMagentrixSdk().getInstance(config)
...
}
// ✅ Correct — composable called synchronously during setup
const dataService = useMagentrixSdk().getInstance(config)
async function loadData(): Promise<void> {
...
}
Using Semicolons
// ❌ Avoid
const accounts = ref<any[]>([]);
// ✅ Correct
const accounts = ref<any[]>([])
Hardcoding Colors Instead of Using CSS Variables
/* ❌ Avoid — hardcoded color */
.card {
background: #ffffff;
border: 1px solid #e5e7eb;
}
/* ✅ Correct — platform CSS variables */
.card {
background: var(--mag-page-bg-color);
border: 1px solid var(--mag-element-border-color);
}
Not Scoping Styles
<!-- ❌ Avoid — unscoped styles leak into the host portal -->
<style>
.card { ... }
</style>
<!-- ✅ Correct -->
<style scoped>
.card { ... }
</style>
Platform CSS Classes
The Magentrix portal provides ready-to-use CSS classes. Use these instead of defining your own equivalents — they are already themed to the portal's visual language.
| Category | Available Classes |
|---|
| Buttons | btn-primary, btn-danger, btn-success, btn-light, btn-info, btn-warning |
| Buttons (app-level) | btn-secondary — not provided by the portal. Defined in the shared App.vue stylesheet using --mag-* variables. Use it directly in any component — do not redefine it locally. |
| Form | iris-label, iris-textbox |
| Alerts | alert alert-success, alert alert-danger, alert alert-warning, alert alert-info |
⚠️ Warning: Do not redefine or override the platform-provided classes. Overriding them breaks theme consistency and may cause visual regressions when the portal theme changes.
What to Read Next