Table of Contents


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

RuleDetail
No semicolonsDo not end statements with ;
Functions vs arrow functionsUse the function keyword for standalone (pure) functions. Use arrow functions only as callback parameters passed to other functions.
TypeScript types requiredSpecify parameter types and return types on all functions.
Avoid watchersWatchers are expensive. Use them only when reactivity through computed or event-based patterns cannot solve the problem.
Avoid onUpdatedDo not use the onUpdated lifecycle hook unless strictly required.
Meaningful namesUse 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:

  1. Props
  2. Emits
  3. Refs
  4. Vars
  5. Lifecycle Hooks
  6. Computed
  7. Watches
  8. Functions

Import Order

Group and order imports in the following sequence with no blank lines between groups:

  1. Vue framework imports
  2. Third-party library imports
  3. Local imports
  4. 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);
}
💡 Note: For the full list of available --mag-* CSS variables and portal CSS classes, see CSS Theming & Portal Variables.

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.

CategoryAvailable Classes
Buttonsbtn-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.
Formiris-label, iris-textbox
Alertsalert 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

If you want to…Go to…
Learn all SDK methods available in componentsMagentrix SDK Reference
Apply platform CSS variables and themingCSS Theming & Portal Variables
Review the full Vue coding standardsCoding Standards
Build and publish your componentCLI Reference
Last updated on 5/14/2026

Attachments