###################### Arches Vue Style Guide ###################### Table of Contents ================= - `Purpose`_ - `Basis for Style Guide`_ - `Contributions`_ - `Project Directory Structure & Naming`_ - `File and Folder Naming Conventions`_ - `Top-Level Structure`_ - `Component Folder Hierarchy`_ - `Component Structure`_ - `Single-File Components`_ - `Component Decomposition`_ - `Slots`_ - `Data Flow & State`_ - `Fetch Proximity`_ - `Primitives First`_ - `Derived State`_ - `Event Emission`_ - `The **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 .. 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 **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 **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