Skip to content

Component Structure

While standard slots allow parent components to inject HTML markup into a child component, Scoped Slots allow child components to pass dynamic data back to the parent’s slot template. This allows parent components to customize how child data is rendered while keeping data management inside the child component.

1. Child Component Syntax (<slot :prop="value">)

Section titled “1. Child Component Syntax (<slot :prop="value">)”

To expose data to the parent slot template, bind properties onto the <slot> tag using the : attribute prefix:

src/components/ListContainer.component.js
<state currentItem="{ id: 1, name: 'Avenx Framework', category: 'Web' }" isVisible="true" />
<div class="list-container">
<!-- Exposing child state onto the default slot -->
<slot :item="state.currentItem" :visible="state.isVisible"></slot>
</div>

Child components can also expose data on named scoped slots:

<header class="card-header">
<slot name="header" :title="state.title" :badgeCount="state.badges.length"></slot>
</header>

2. Parent Component Syntax (data-slot-props)

Section titled “2. Parent Component Syntax (data-slot-props)”

Parent components receive the child’s exposed data by wrapping the transcluded slot markup in a <template> tag with the data-slot-props attribute. The attribute value defines the local variable name used to access child data:

src/pages/HomePage.page.js
<state selectedItem="null" />
<action name="handleItemSelect">
const [item] = args;
this.state.selectedItem = item;
</action>
<div class="home-page">
<ListContainer>
<template data-slot-props="slotProps">
<div class="custom-item" data-ax-show="slotProps.visible">
<h3>{{ slotProps.item.name }}</h3>
<span class="badge">{{ slotProps.item.category }}</span>
<button @click="handleItemSelect(slotProps.item)">Select</button>
</div>
</template>
</ListContainer>
</div>

When using named slots together with scoped slot props, specify both the name attribute and data-slot-props on the <template> tag:

<CardWidget>
<!-- Consuming a named scoped slot -->
<template name="header" data-slot-props="headerProps">
<h2>{{ headerProps.title }}</h2>
<span class="count-pill">{{ headerProps.badgeCount }} new</span>
</template>
<!-- Default slot content -->
<template data-slot-props="bodyProps">
<p>{{ bodyProps.content }}</p>
</template>
</CardWidget>

Inside a scoped slot template:

  • Interpolations ({{ slotProps.item.name }}), directives (data-ax-show="slotProps.visible"), and action handlers (@click="handleItemSelect(slotProps.item)") automatically evaluate against slotProps.
  • Event action handlers inside scoped slots can pass slotProps properties directly to component methods.
  • Updates to child state automatically re-evaluate expressions inside the parent’s scoped slot template without unmounting the slot DOM nodes.

Avenx components support lifecycle hooks that allow you to run logic at different stages of a component’s lifecycle.

Lifecycle hooks can be defined in two ways:

  1. Through the methods object.
  2. By defining lifecycle methods directly in a class that extends AvenxComponent.

Defining Lifecycle Hooks with Methods Object

Section titled “Defining Lifecycle Hooks with Methods Object”

Lifecycle hooks can be provided through the component’s methods configuration:

methods: {
onMount() {
console.log('Component mounted');
},
onUpdate() {
console.log('Component updated');
},
onUnmount() {
console.log('Component unmounted');
}
}

When using class-based components, lifecycle hooks can be defined directly as methods on a subclass of AvenxComponent.

class MyComponent extends AvenxComponent {
onMount() {
console.log('Component mounted');
}
onUpdate() {
console.log('Component updated');
}
onUnmount() {
console.log('Component unmounted');
}
}

If a lifecycle hook is defined in both the methods object and the subclass, the methods object takes priority.

Available lifecycle hooks:

  • onBeforeMount() — Runs right before initial component template rendering.
  • onMount() — Runs when the component is mounted to the DOM.
  • onBeforeUpdate() — Runs before DOM patching upon state changes.
  • onUpdate() — Runs when the component updates and finishes DOM patching.
  • onUnmount() — Runs when the component is removed from the DOM.
  • onActivate(params) / onDeactivate() — Runs for cached keepAlive: true pages.
  • onErrorCaptured(error, instance, info) — Captures unhandled errors from descendant components.

Centralized Error Boundaries (onErrorCaptured)

Section titled “Centralized Error Boundaries (onErrorCaptured)”

Avenx components can act as Error Boundaries by implementing the onErrorCaptured lifecycle hook. When an unhandled error occurs in a descendant component’s lifecycle hook (such as onMount) or event action, the error bubbles up the parent component hierarchy until an onErrorCaptured handler intercepts it.

onErrorCaptured(error, instance, info)
  • error (Error): The error instance caught from the descendant component.
  • instance (AvenxComponent): The component instance where the error occurred.
  • info (string): Description string of where the error originated (e.g., 'onMount', 'action click', 'template render').

By default, an error captured by onErrorCaptured continues bubbling up to ancestor components and eventually triggers the global application error handler (app.config.errorHandler).

To stop error propagation and prevent the application from crashing, return false explicitly from onErrorCaptured:

onErrorCaptured(error, instance, info) {
console.error(`Caught error from ${instance.constructor.name} during ${info}:`, error);
this.state.hasError = true;
this.state.errorMessage = error.message;
return false; // Prevents error from bubbling further
}

Error Boundaries are reusable container components that wrap dynamic UI sections (such as charts, external feeds, or user-generated widgets) to display fallback UI when something breaks inside them.

Single-File Component Example (ErrorBoundary.component.js)

Section titled “Single-File Component Example (ErrorBoundary.component.js)”
<state hasError="false" errorMessage="''" />
<action name="onErrorCaptured">
const [error, instance, info] = args;
console.error('ErrorBoundary captured exception:', error, info);
// Set fallback UI state
this.state.hasError = true;
this.state.errorMessage = error.message || 'An unexpected error occurred';
// Stop propagation to prevent full application crash
return false;
</action>
<action name="resetError">
this.state.hasError = false;
this.state.errorMessage = '';
</action>
<div class="error-boundary-wrapper">
<div data-ax-show="hasError" class="error-fallback-card">
<h3>Something went wrong</h3>
<p>{{ errorMessage }}</p>
<button @click="resetError()">Try Again</button>
</div>
<div data-ax-show="!hasError">
<slot></slot>
</div>
</div>

Wrap unstable or third-party components inside the <ErrorBoundary> component:

src/pages/dashboard.page.js
<div class="dashboard-page">
<h2>Analytics Dashboard</h2>
<ErrorBoundary>
<UnstableChartWidget />
</ErrorBoundary>
</div>

If <UnstableChartWidget> throws an exception during data fetching in onMount(), <ErrorBoundary> catches the exception, returns false to stop propagation, and renders the fallback card with a “Try Again” button without affecting the rest of the dashboard page.

The Avenx compiler processes .component.js files by scanning for supported configuration tags and template content. During compilation, only <state>, <computed>, <action>, and template content are preserved and transformed into the generated component output.

Standard JavaScript declarations written outside these supported tags, such as ES module imports, local variables, constants, and helper functions, are not preserved by the compiler. Code that depends on these declarations may therefore cause runtime ReferenceError exceptions after compilation.

For example, avoid declaring imports or helper functions directly in a component file:

import { formatName } from './utils.js';
const defaultName = 'Guest';
function getDisplayName(name) {
return formatName(name);
}
<state username="Guest" />
<action name="updateName">
state.username = getDisplayName(defaultName);
</action>

In this example, the import, defaultName, and getDisplayName declarations are outside the supported component tags and may be removed during compilation.

Instead, keep component logic inside supported tags or move reusable utilities into external files that can be accessed through supported application patterns.

For utilities that are intentionally exposed globally, reference them through the window object:

<action name="updateName"> state.username = window.AppUtils.formatName(state.username); </action>

When writing .component.js files:

  • Keep reactive state declarations inside <state> tags.
  • Keep computed values inside <computed> tags.
  • Keep component methods and state mutations inside <action> tags.
  • Move reusable helper logic into external utility files.
  • Reference intentionally global utilities through properties on window.

Understanding these compilation limits helps prevent missing imports, undefined helpers, and runtime ReferenceError exceptions caused by code being removed from the compiled output.

Slots can define fallback content that is rendered when a parent component does not provide any content for that slot.

This allows components to display sensible default content while still supporting customization through composition.

Define fallback content by placing it between the opening and closing <slot> tags.

<div class="card">
<slot>
No content provided.
</slot>
</div>

If a parent component does not supply any child content, the text inside the <slot> element is rendered automatically.

Child component:

<div class="card">
<slot>
No content provided.
</slot>
</div>

Parent component:

<Card>
<p>Welcome to Avenx-JS!</p>
</Card>

Rendered output:

<div class="card">
<p>Welcome to Avenx-JS!</p>
</div>

The fallback content is ignored because the parent supplied slot content.

Child component:

<div class="card">
<slot>
No content provided.
</slot>
</div>

Parent component:

<Card />

Rendered output:

<div class="card">
No content provided.
</div>

Since no slot content was passed from the parent, the fallback content is rendered instead.


Each <slot> element manages its own fallback content independently.

<header>
<slot>
Default Header
</slot>
</header>
<main>
<slot>
Default Body
</slot>
</main>
<footer>
<slot>
Default Footer
</slot>
</footer>

If content is not supplied for a particular slot, that slot renders its own fallback while other slots continue to render the content they receive.


  • Provide meaningful fallback content whenever a slot is optional.
  • Use concise placeholders such as loading states, default messages, or instructional text.
  • Keep fallback content relevant to the component’s purpose.
  • Avoid relying on fallback content for required information that should always be supplied by the parent component.

Actions can be invoked with arguments directly from template event bindings.

<button @click="greet(state.username, 'morning')">
Greet
</button>

Arguments passed to an action are automatically exposed inside the action block through an implicit args array.

The values are available in the same order they are passed.

<action name="greet">
const username = args[0];
const timeOfDay = args[1];
console.log(`Good ${timeOfDay}, ${username}!`);
</action>

In this example:

  • args[0] contains state.username
  • args[1] contains "morning"

This makes it easy to pass dynamic values from the template without defining additional component state.

Any number of arguments can be supplied.

<button
@click="updateUser(state.id, state.name, state.role)">
Update User
</button>
<action name="updateUser">
const id = args[0];
const name = args[1];
const role = args[2];
console.log(id, name, role);
</action>

Arguments are always available through the args array in the order they were passed.

  • args is automatically available inside <action> blocks.
  • Arguments are zero-indexed (args[0], args[1], args[2], …).
  • If an argument is omitted when the action is called, its corresponding entry in args will be undefined.