**Why?**
- **Encapsulation**: All component-related code is in one place, making it easier to understand and maintain.
- **Separation of concerns**: Each section (template, script, style) has its own purpose, improving readability.
Component Decomposition
-----------------------
Components should be decomposed into smaller, reusable components whenever possible. Aim for a single responsibility per component.
.. code-block:: vue
{{ widgetValue }}
.. code-block:: vue
**Why?**
- **Reusability**: Smaller components can be reused in different contexts, reducing code duplication.
- **Maintainability**: Easier to understand and modify smaller components than large monolithic ones.
- **Testing**: Smaller components are easier to test in isolation.
Slots
-----
Use scoped slots when the consumer needs access to slot data; use regular named slots for simple content projection. Name slots clearly to indicate their purpose.
.. code-block:: vue
{{ $gettext('Table Header') }}
**Why?**
- **Flexibility**: Consumers can customize the rendering of specific parts of the component.
- **Separation of concerns**: Slots allow for a clear distinction between the component's structure and its content.
Data Flow & State
=================
Use the simplest mechanism that meets your needs. In order of increasing complexity:
1. **Props & emits** — The default. For parent-child data flow when the component tree is shallow and the data is local to a subtree.
2. **provide/inject** — For sharing state across a deep component tree without prop drilling. The providing component owns and controls the state; injecting components consume it read-only.
3. **Composables** — For shared data-fetching logic or reactive utilities needed by multiple unrelated components. Each call site gets its own reactive instance unless the composable explicitly exports a shared singleton.
4. **Pinia** — For state that must be shared globally across unrelated component trees, persist across navigation, or be accessed outside of a Vue component context. Use sparingly — most Arches application state does not require a global store.
.. code-block:: vue
.. code-block:: vue
.. code-block:: typescript
// 3. Composable: shared fetch logic — each component that calls useNodegroupData()
// gets its own reactive instance independently tracking its own nodegroup
// use-nodegroup-data.ts
import { ref, watchEffect, type Ref } from 'vue';
import { generateArchesURL } from '@/arches/utils/generate-arches-url.ts';
import type { Nodegroup } from '@/project_name/types.ts';
export function useNodegroupData(nodegroupId: Ref) {
const nodegroup = ref(null);
watchEffect(async () => {
const response = await fetch(
generateArchesURL('arches:nodegroup', { id: nodegroupId.value })
);
nodegroup.value = await response.json();
});
return { nodegroup };
}
.. code-block:: typescript
// 4. Pinia: the authenticated user is global state — it must be accessible from
// unrelated trees (nav bar, permission checks, audit logging) and persist
// across navigation, making it the one case that genuinely warrants a store
// stores/auth-store.ts
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { generateArchesURL } from '@/arches/utils/generate-arches-url.ts';
import type { AuthUser } from '@/arches/types.ts';
export const useAuthStore = defineStore('auth', () => {
const currentUser = ref(null);
async function fetchCurrentUser() {
const response = await fetch(generateArchesURL('arches:me'));
currentUser.value = await response.json();
}
return { currentUser, fetchCurrentUser };
});
**Why?**
- **Simplicity**: Props are explicit and traceable; escalate only when the complexity genuinely requires it.
- **Encapsulation**: Each mechanism has a defined ownership model — props flow down, events flow up, provide/inject scopes to a tree, Pinia is global.
- **Testability**: Simpler mechanisms are easier to test; Pinia stores and composables can both be tested independently of components.
Fetch Proximity
---------------
Fetch data as close to the consumer as possible. Don't lift network calls higher than needed. When multiple components need the same data, extract the fetch logic into a shared composable. When state must be shared globally across unrelated component trees, use Pinia. See `Data Flow & State`_ for guidance on choosing the right mechanism.
.. code-block:: vue
.. code-block:: vue
{{ resource.displayName }}
**Why?**
- **Encapsulation**: Data-fetch logic lives alongside the view that consumes it.
- **Limited prop drilling**: Minimizes passing data through unrelated parents.
- **Error isolation**: Failures are handled locally, without cascading side effects.
Primitives First
----------------
When a component only needs part of a model, pass only what it needs — not the whole object.
.. code-block:: vue
**Why?**
- **Explicit API**: Readers, tools, and developers see exactly which fields the component needs.
- **Immutable flow**: Primitives can't be mutated in place, preserving one-way data flow.
- **Efficient updates**: Changes to unused object properties won't force re-renders.
Derived State
-------------
If a component's sole responsibility is to derive or summarize data, pass the raw data and let it compute internally.
.. code-block:: vue
When multiple children need the same computed value, derive once in the parent and pass primitives to avoid duplication and ensure consistency.
.. code-block:: vue
**Why?**
- **Performance**: Avoids recomputing derived values in multiple components.
- **Predictable props**: Child components receive only the exact values they need.
- **Consistency**: Ensures every consumer uses the same computed values, preventing drift.
Event Emission
--------------
Emit semantic events (kebab-case) with typed payloads:
.. code-block:: vue
**Why?**
- **Explicit contracts**: Consumers know exactly what events to expect and how to handle them.
- **Type safety**: TypeScript ensures the payload matches the expected structure.
The
**Why?**
- **TypeScript support**: Enables full TypeScript support directly within each component.
- **Scope safety**: All variables and functions are scoped to the component, preventing accidental global pollution.
**Function Declarations**
Use named ``function`` declarations for component methods; **do not** use anonymous/arrow functions or function expressions. Anonymous/arrow functions are allowed for inline callbacks (e.g., ``setTimeout``, ``Promise.then``, ``filter``, ``onMounted``, ``computed``, etc.).
.. code-block:: js
// Bad: arrow function for component method
const incrementCount = () => { count.value++; };
// Bad: function expression for component method
const incrementCount = function() { count.value++; };
// Good: named function declaration for component method
function incrementCount() { count.value++; }
// Good: arrow function used for inline callback
setTimeout(() => { count.value++; }, 1000);
**Why?**
- **Hoisting**: Named functions are hoisted, allowing them to be called before their declaration in the code. This can help avoid issues with function order and improve readability.
- **Debugging**: Named functions provide better stack traces and error messages, making issues easier to diagnose.
**Constants & Literals**
Declare fixed values in ``SCREAMING_SNAKE_CASE``. Extract string literals and magic numbers as named constants when their meaning isn't self-evident from context, or when they appear in more than one place.
.. code-block:: js
// Bad: magic number and string literal
function calculateTotal(price) {
return price * 0.0825;
}
function isOrderComplete(order) {
return order.status === 'PENDING';
}
// Good: named constants
const TAX_RATE = 0.0825;
const ORDER_STATUS_PENDING = 'PENDING';
function calculateTotal(price) {
return price * TAX_RATE;
}
function isOrderComplete(order) {
return order.status === ORDER_STATUS_PENDING;
}
**Why?**
- **Readability**: Named constants make the code more self-explanatory and easier to understand and debug.
- **Maintainability**: Changing a single constant is easier than searching for all occurrences of a magic number or string literal.
**Naming Conventions**
Use descriptive identifiers; avoid single-letter names.
.. code-block:: js
// Bad: single-letter naming
function doubleValue(x) { return x * 2; }
// Good: descriptive naming
function doubleValue(value) { return value * 2; }
**Why?**
- **Clarity**: Descriptive names provide context and meaning, making the code easier to read and understand.
- **Maintainability**: Clear names help future developers (or yourself) quickly grasp the purpose of variables and functions.
**Modularity & Reuse**
Extract non-UI logic (data transformations, business rules) into composables or utility modules.
.. code-block:: js
// Bad: non-UI logic in component
function calculateDiscount(price, discount) {
return price - (price * discount);
}
// Good: non-UI logic in utility module
import { calculateDiscount } from '@/my_project/utils/discounts.ts';
**Why?**
- **Separation of concerns**: Keeps UI logic separate from business logic, making components easier to read and maintain.
- **Reusability**: Composables and utility modules can be reused across multiple components, reducing code duplication.
**Side-Effects & Async Handling**
Avoid performing side-effects (API calls, timers, storage access, data formatting, etc.) at module scope in ``
**Why?**
- **Predictability**: Side-effects should only occur in controlled environments to avoid unexpected behavior.
- **Reactivity**: ``watchEffect`` automatically tracks reactive dependencies and re-fetches when they change, without needing ``immediate: true``.
- **Error handling**: Wrapping async operations in ``try/catch`` allows for graceful error handling and user feedback.
**Type Safety**
Import and use explicit types; avoid use of the ``any`` type. Annotate all function return types.
.. code-block:: typescript
// Bad: using any type
function fetchData(): any {
return fetch(generateArchesURL('my_app:data')).then(response => response.json());
}
// Good: explicit type annotation
interface User {
id: number;
name: string;
}
function fetchData(): Promise {
return fetch(generateArchesURL('my_app:data')).then(response => response.json());
}
**Why?**
- **Type safety**: Using explicit types helps catch errors at compile time, reducing runtime issues.
- **Documentation**: Type annotations serve as documentation for function behavior and expected input/output.
Import Pathing
--------------
**Use project alias**
Use ``@/…`` for all local imports; avoid raw relative paths.
.. code-block:: js
// Bad: raw relative path
import { fetchData } from '../../utils/fetch-data.ts';
// Good: project alias
import { fetchData } from '@/project_name/utils/fetch-data.ts';
**Why?**
- **Readability**: Project aliases make it clear where the module is located without needing to trace relative paths.
- **Maintainability**: Avoids issues with deep nesting and makes it easier to refactor or reorganize the project structure.
Import Order
------------
Import lines must be grouped and ordered as follows:
1. **Vue core**
2. **Third-party modules**
3. **Vue components** (third-party → arches core → arches applications → local)
4. **Utilities/composables** (third-party → arches core → arches applications → local)
5. **Types** (third-party → arches core → arches applications → local)
.. code-block:: vue
Declaration Order
-----------------
Within your ``
The Tag
====================
Defines the component's UI. Keep templates clear, consistent, and easy to scan.
Attribute Ordering & Formatting
-------------------------------
When declaring attributes in your ````, group and order them as follows:
1. **Directives** (e.g. ``v-for``, ``v-if``)
2. **Slots** (e.g. ``v-slot:header="…"``)
3. **Static attributes** (e.g. ``id``, ``class``)
4. **Dynamic props** (e.g. ``:prop="…"``)
5. **Event listeners** (e.g. ``@click="…"``, ``@click.prevent="…"``)
**Inline vs. multiline** — One attribute: keep on the same line as the tag. Two or more: one per line, indented under the tag.
**Explicit assignment** — Always write ``prop="value"`` or ``:prop="value"``. Do not use shorthand (``:prop`` without a value).
**Kebab-case** — All attribute names, including custom props and events, must use kebab-case.
.. code-block:: vue
**Why?**
- **Readability**: Consistent ordering and formatting make it easier to scan and understand the template.
- **Maintainability**: Clear structure helps future developers (or yourself) quickly grasp the component's purpose and behavior.
Self-Closing Tags
-----------------
Use self-closing syntax for elements or components without children:
.. code-block:: vue
**Why?**
- **Clarity**: Self-closing tags clearly indicate that the element has no children, improving readability.
- **Consistency**: Using self-closing syntax for void elements (e.g., ``, ``) maintains a consistent style throughout the codebase.
Logic in Templates
------------------
Simple ternaries are acceptable. Compound or nested ternaries, chained method calls, and heavy expressions must be moved into ``computed`` properties or methods.
.. code-block:: vue
**Why?**
- **Readability**: Templates should be easy to read and understand at a glance.
- **Performance**: Heavy computations in templates can lead to unnecessary re-renders and performance issues.
Text in Templates
-----------------
**Internationalization** — Wrap all user-facing strings with ``$gettext()``. For runtime values, use ``%{placeholder}`` syntax and pass a values object as the second argument. Never concatenate translated strings.
**When to use interpolate()** — ``$gettext(msg, values)`` handles interpolation for most cases. Use ``interpolate()`` explicitly in two situations:
- The translation contains **HTML markup** that will be rendered via ``v-html``. Omit the third argument so substituted values are HTML-escaped, preventing markup injection.
- A substituted value may contain **literal angle brackets** (e.g. ``Aircraft ``). Pass ``true`` as the third argument to prevent those characters from being HTML-escaped.
**No loose text nodes** — Surround plain text with an inline element (e.g. ````) or semantic tag.
.. code-block:: vue
**Why?**
- **Translator context**: Placeholders keep the full sentence intact in ``.po`` files, giving translators the context they need to produce accurate translations.
- **HTML safety**: ``interpolate()`` HTML-escapes substituted values by default, preventing markup injection via ``v-html``. Pass ``true`` only when substituted values are plain text that may legitimately contain angle brackets.
- **Semantic HTML**: Using inline elements or semantic tags improves accessibility and SEO by providing context to screen readers and search engines.
The
**Why?**
- **Isolation**: Scoped styles prevent unintended side effects on other components, ensuring consistent styling.
- **Maintainability**: Changes to a component's styles won't affect other components, reducing the risk of introducing undesired behavior.
Layout Patterns
---------------
**Flexbox & Grid only** — Use ``display: flex`` for one-dimensional layouts and ``display: grid`` for two-dimensional arrangements.
**Use gap** — Space items with ``gap``; do not rely on margins for core layout.
**No legacy hacks** — Never use ``float`` or other outdated layout techniques.
**Single-line vs. multi-line selectors** — Use single-line selectors for a single atomic override; use multi-line selectors when grouping multiple rules.
.. code-block:: css
/* Bad: single-line selector for multiple rules */
.item { display: flex; gap: 1rem; }
/* Good: single-line selector for one rule */
.item { display: flex; }
/* Good: multi-line selector for multiple rules */
.item {
display: flex;
gap: 1rem;
}
**Why?**
- **Flexibility**: Flexbox and Grid provide powerful layout capabilities for modern web applications.
- **Maintainability**: Using `gap` simplifies spacing management and reduces the need for complex margin calculations.
- **Scanability**: Single-line selectors signal a single atomic override and are easy to scan past. Multi-line formatting means each property appears on its own line in diffs, making code review clearer.
Units & Sizing
--------------
**rem for nearly everything** — Use ``rem`` units for spacing, typography, gaps, borders, and other dimensional values.
**Viewport units sparingly** — Reserve ``vh``/``vw`` for elements that must span the viewport (e.g. full-screen sections or modals).
**Percentages for fluid layouts** — Apply ``%`` when you need relative sizing (e.g. fluid widths in responsive grids).
**No px** — Avoid ``px`` units entirely to ensure scalability, accessibility, and consistent theming.
.. code-block:: css
/* Bad: using px units */
.container { width: 800px; padding: 20px; }
/* Good: using rem units */
.container { width: 50rem; padding: 1.25rem; }
/* Good: using percentage for fluid layout */
.container { width: 100%; }
**Why?**
- **Scalability**: Using `rem` and `%` units allows for better scaling across different screen sizes and resolutions.
- **Accessibility**: Relative units ensure that text and elements can be resized according to user preferences, improving accessibility.
Offsets & Positioning
---------------------
**No single-side offsets** — Use logical properties (``margin-inline-start``, ``margin-block-start``) instead of physical directional properties like ``margin-left`` or ``margin-top``.
**No negative margins** — Negative ``margin-*`` values are forbidden.
.. code-block:: css
/* Bad: negative margin, not using logical properties */
.container .item { margin-left: -1rem; }
/* Good: no negative margin, using logical properties */
.container { padding-inline-start: 1rem; }
.item { margin-inline-start: 0; }
**Why?**
- **Logical properties**: Using logical properties ensures consistent behavior across different language displays (e.g. left-to-right vs. right-to-left).
- **Avoiding layout shifts**: Negative margins can lead to unexpected layout shifts and make it harder to maintain a consistent design.
No `calc()`
-----------
Avoid ``calc()`` for layout problems that flexbox or grid can solve directly.
.. code-block:: css
/* Bad: using calc() to fake a two-column layout */
.sidebar { width: calc(100% - 800px); }
.content { width: 800px; }
/* Good: use grid instead */
.container {
display: grid;
grid-template-columns: 1fr auto;
}
**Why?**
- Layout math in `calc()` is often a sign that flexbox or grid would be a cleaner solution.
- Mixing `calc()` with hardcoded pixel values creates fragile layouts that break when surrounding elements change.
Theming & Colors
----------------
**Design tokens only** — Always reference design tokens instead of raw values.
**Centralize & document** — Keep all tokens (colors, typography scales, breakpoints) in a single theme file.
**Semantic layers** — Build on top of raw palette entries with semantic tokens (e.g. ``--color-success``) so UI intent drives your choices.
**Light/dark support** — Define variants for both modes in your theme preset.
.. code-block:: js
import { definePreset } from '@primeuix/themes';
import { ArchesPreset } from "@/arches/themes/default.ts";
export const MyTheme = definePreset(ArchesPreset, {
semantic: {
colorScheme: {
light: {
primary: { color: '{primary.500}', contrast: '{primary.50}' },
success: { color: 'green', contrast: '{surface.900}' }
},
dark: {
primary: { color: '{primary.300}', contrast: '{surface.900}' }
}
}
}
});
**Consuming tokens in CSS** — PrimeVue tokens are available as CSS custom properties using the ``--p-`` prefix (set by Arches's ``DEFAULT_THEME``). Reference them directly in ``
.. code-block:: js
// src/components/CounterButton.spec.ts
import { describe, it, expect } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import CounterButton from '@/my_project/components/CounterButton.vue';
describe('CounterButton.vue', () => {
it('mounts and displays initial count', () => {
const wrapper = mount(CounterButton);
expect(wrapper.text()).toContain('0');
});
it('increments count on click', async () => {
const wrapper = mount(CounterButton);
const button = wrapper.find('button');
await button.trigger('click');
await flushPromises();
expect(wrapper.text()).toContain('1');
});
});
**Why?**
- **Isolation**: Testing components in isolation helps identify issues more easily and ensures that tests are not affected by other components.
- **Readability**: Clear and descriptive test names make it easier for developers to understand the purpose of each test.
- **Maintainability**: Well-structured tests are easier to maintain and update as the codebase evolves.
Running Frontend Tests
----------------------
Use the following npm scripts in your terminal:
.. code-block:: shell
# Run all tests once
npm run vitest
# Run a specific test file
npm run vitest -- src/components/CounterButton.spec.ts
# Watch mode — re-runs on file changes
npm run vitest -- --watch
# Run with coverage report (output appears under coverage/)
npm run vitest -- --coverage