Events Architecture

This document describes the event-driven architecture used throughout the Alviere UI component library.

Overview

The Alviere UI components use a standardized event system to communicate state changes, user actions, and data flow between components. This event-driven architecture enables:

  • Loose Coupling: Components communicate through events rather than direct dependencies
  • Composability: Components can be easily combined in flows and workflows
  • Extensibility: New event listeners can be added without modifying component internals
  • Type Safety: All events have typed interfaces for their detail payloads

Event Types

Core Event Categories

Events are organized into the following categories:

  1. Input Component Events - User input and validation state changes
  2. Form Component Events - Form submission, success, and error states
  3. Flow Component Events - Multi-step flow progress and completion
  4. Domain-Specific Events - Business logic events (account creation, payment methods, etc.)
Event Names

All event names are defined in ComponentEvents constant (src/types/events.ts):

export const ComponentEvents = {
  // Form component events
  FORM_SUCCESS: 'form-success',
  FORM_ERROR: 'form-error',
  FORM_SUBMIT: 'form-submit',
  FORM_RESET: 'form-reset',

  // Flow component events
  FLOW_STEP_COMPLETE: 'flow-step-complete',
  FLOW_STEP_ERROR: 'flow-step-error',
  FLOW_COMPLETE: 'flow-complete',
  FLOW_RESET: 'flow-reset',

  // Domain-specific events
  ACCOUNT: 'account',
  PAYMENT_METHOD: 'payment-method',
} as const;

Event Flow Architecture

Single Component Usage

When a form component is used standalone:

User Action → Component Logic → Backend API Call → Event Dispatch

Example with CreateConsumerAccount:

// 1. User submits form
// 2. Form validates and calls backend
// 3. Backend returns successful response
// 4. Component dispatches TWO events:

// Event A: Domain-specific 'account' event
{
  type: 'account',
  detail: {
    account_uuid: '...',
    status: 'ACTIVE'
  }
}

// Event B: Generic 'form-success' event
{
  type: 'form-success',
  detail: {
    stepType: 'CREATE_CONSUMER_ACCOUNT',
    account_uuid: '...',
    success: true
  }
}
Multi-Step Flow Usage

When components are orchestrated in a MultiStepFlow:

Step Component → form-success → Flow Controller → FLOW_STEP_COMPLETE → Next Step

The flow automatically:

  1. Listens for form-success events from step components
  2. Accumulates results from completed steps
  3. Passes accumulated data to subsequent steps
  4. Dispatches flow-level events for overall progress

Event Details Structure

Form Success Event

Dispatched when a form component successfully completes its operation:

interface FormEventDetail {
  stepType: string;        // Component identifier (e.g., 'CREATE_CONSUMER_ACCOUNT')
  stepIndex?: number;      // Step index if in a flow
  success: boolean;        // Always true for success events
  [key: string]: any;      // Component-specific data (account_uuid, payment_method_uuid, etc.)
}

Example:

{
  stepType: 'CREATE_CONSUMER_ACCOUNT',
  stepIndex: 0,
  success: true,
  account_uuid: '862a2524-ce32-4fa7-b138-e7ac7e93c022'
}
Account Event

Dispatched when an account is created or activated:

interface AccountEventDetail {
  account_uuid: string;
  status: 'CREATED' | 'ACTIVE';
}

Example:

{
  account_uuid: '862a2524-ce32-4fa7-b138-e7ac7e93c022',
  status: 'ACTIVE'
}
Payment Method Event

Dispatched when a payment method is created:

interface PaymentMethodEventDetail {
  payment_method_uuid: string;
  status: string;
}

Example:

{
  payment_method_uuid: '12345678-1234-1234-1234-123456789012',
  status: 'ACTIVE'
}
Flow Step Complete Event

Dispatched when a flow step completes successfully:

interface StepEventDetail {
  stepIndex: number;
  stepType: string;
  result: any;              // Accumulated results from all completed steps
  error?: Error;
}
Flow Complete Event

Dispatched when all steps in a flow are completed:

interface FlowEventDetail {
  currentStep: number;
  totalSteps: number;
  stepResults: Record<number, any>;
  isComplete: boolean;
  result?: any;             // Merged results from all steps
}

Usage Examples

Listening to Events in HTML/JavaScript
<alviere-create-consumer-account
  jwt="your-jwt-token"
  account-uuid="your-account-uuid">
</alviere-create-consumer-account>

<script>
  const component = document.querySelector('alviere-create-consumer-account');
  
  // Listen for domain-specific account event
  component.addEventListener('account', (event) => {
    console.log('Account created:', event.detail.account_uuid);
    console.log('Status:', event.detail.status);
  });
  
  // Listen for generic form success event
  component.addEventListener('form-success', (event) => {
    console.log('Form completed:', event.detail);
    // Navigate to next step, update UI, etc.
  });
  
  // Listen for errors
  component.addEventListener('form-error', (event) => {
    console.error('Form error:', event.detail.error);
  });
</script>
Multi-Step Flow Events
<alviere-multi-step-flow
  jwt="your-jwt-token"
  config='{"steps":[...]}'>
</alviere-multi-step-flow>

<script>
  const flow = document.querySelector('alviere-multi-step-flow');
  
  // Listen for step completion
  flow.addEventListener('flow-step-complete', (event) => {
    console.log(`Step ${event.detail.stepIndex} completed`);
    console.log('Accumulated results:', event.detail.result);
  });
  
  // Listen for flow completion
  flow.addEventListener('flow-complete', (event) => {
    console.log('Flow completed!');
    console.log('All results:', event.detail.result);
    // Navigate to success page, etc.
  });
  
  // Listen for domain events from within the flow
  flow.addEventListener('account', (event) => {
    console.log('Account created in flow:', event.detail.account_uuid);
    // Update external state, analytics, etc.
  });
  
  flow.addEventListener('payment-method', (event) => {
    console.log('Payment method created:', event.detail.payment_method_uuid);
    // Update external state, analytics, etc.
  });
</script>
React/Next.js Integration
import { useRef, useEffect, useState } from 'react';

function PaymentFlow() {
  const flowRef = useRef<HTMLElement>(null);
  const [accountUuid, setAccountUuid] = useState<string | null>(null);
  const [paymentMethodUuid, setPaymentMethodUuid] = useState<string | null>(null);
  const [isComplete, setIsComplete] = useState(false);

  useEffect(() => {
    const flow = flowRef.current;
    if (!flow) return;

    // Listen for account creation
    const handleAccount = (event: CustomEvent) => {
      setAccountUuid(event.detail.account_uuid);
      console.log('Account created:', event.detail.account_uuid);
    };

    // Listen for payment method creation
    const handlePaymentMethod = (event: CustomEvent) => {
      setPaymentMethodUuid(event.detail.payment_method_uuid);
      console.log('Payment method created:', event.detail.payment_method_uuid);
    };

    // Listen for flow completion
    const handleFlowComplete = (event: CustomEvent) => {
      setIsComplete(true);
      console.log('Flow completed with results:', event.detail.result);
      // Navigate to success page or update app state
    };

    flow.addEventListener('account', handleAccount as EventListener);
    flow.addEventListener('payment-method', handlePaymentMethod as EventListener);
    flow.addEventListener('flow-complete', handleFlowComplete as EventListener);

    return () => {
      flow.removeEventListener('account', handleAccount as EventListener);
      flow.removeEventListener('payment-method', handlePaymentMethod as EventListener);
      flow.removeEventListener('flow-complete', handleFlowComplete as EventListener);
    };
  }, []);

  return (
    <alviere-multi-step-flow
      ref={flowRef}
      jwt={yourJwt}
      config={JSON.stringify(flowConfig)}
    />
  );
}
Vue.js Integration
<template>
  <alviere-multi-step-flow
    ref="flowRef"
    :jwt="jwt"
    :config="JSON.stringify(flowConfig)"
    @account="handleAccount"
    @payment-method="handlePaymentMethod"
    @flow-complete="handleFlowComplete"
  />
</template>

<script setup>
import { ref } from 'vue';

const flowRef = ref(null);
const accountUuid = ref(null);
const paymentMethodUuid = ref(null);
const isComplete = ref(false);

const handleAccount = (event) => {
  accountUuid.value = event.detail.account_uuid;
  console.log('Account created:', event.detail);
};

const handlePaymentMethod = (event) => {
  paymentMethodUuid.value = event.detail.payment_method_uuid;
  console.log('Payment method created:', event.detail);
};

const handleFlowComplete = (event) => {
  isComplete.value = true;
  console.log('Flow completed:', event.detail);
  // Navigate or update state
};
</script>

Event Flow Examples

Example 1: Create Account Flow

This example shows the sequence of events when creating a consumer account:

1. User fills form and submits

2. CreateConsumerAccount component calls backend API

3. Backend returns account with status 'CREATED'

4. Component dispatches 'account' event with status 'CREATED'

5. Component polls backend for activation

6. Backend returns account with status 'ACTIVE'

7. Component dispatches 'account' event with status 'ACTIVE'

8. Component dispatches 'form-success' event

9. If in flow: MultiStepFlow receives 'form-success' and moves to next step
Example 2: Add Bank Account Flow
1. User enters bank details and submits

2. AddBankAccount component calls backend API

3. Backend creates payment method and returns response

4. Component immediately dispatches 'payment-method' event

5. Component refreshes bank account list

6. Component dispatches 'form-success' event

7. If in flow: MultiStepFlow receives 'form-success' and moves to next step
Example 3: Complete Multi-Step Payment Flow
Step 1: CREATE_CONSUMER_ACCOUNT
  → 'account' event (status: CREATED)
  → 'account' event (status: ACTIVE)
  → 'form-success' event { account_uuid: '...' }
  → MultiStepFlow accumulates: { account_uuid: '...' }
  → MultiStepFlow dispatches 'flow-step-complete'
  → Move to Step 2

Step 2: ADD_BANK_ACCOUNT
  → Receives account_uuid from Step 1
  → 'payment-method' event { payment_method_uuid: '...', status: 'ACTIVE' }
  → 'form-success' event { payment_method_uuid: '...' }
  → MultiStepFlow accumulates: { account_uuid: '...', payment_method_uuid: '...' }
  → MultiStepFlow dispatches 'flow-step-complete'
  → Move to Step 3

Step 3: CHECKOUT_CONFIRM
  → Receives account_uuid and payment_method_uuid from previous steps
  → User confirms payment
  → 'form-success' event { amount: 60000 }
  → MultiStepFlow accumulates: { account_uuid: '...', payment_method_uuid: '...', amount: 60000 }
  → MultiStepFlow dispatches 'flow-step-complete'
  → MultiStepFlow dispatches 'flow-complete' with all accumulated results

Data Flow and Accumulation

How Data Flows Between Steps

The MultiStepFlow component automatically accumulates data from completed steps and passes it to subsequent steps:

  1. Step Completion: When a step completes, it dispatches a form-success event with its result data
  2. Accumulation: The flow stores this result in stepResults[stepIndex]
  3. Propagation: When the next step starts, it receives accumulated data from all previous steps
  4. Merging: At flow completion, all step results are merged into a single result object

Example accumulation:

// After Step 1 (CREATE_CONSUMER_ACCOUNT)
stepResults = {
  0: { stepType: 'CREATE_CONSUMER_ACCOUNT', account_uuid: 'abc-123', success: true }
}

// After Step 2 (ADD_BANK_ACCOUNT)
stepResults = {
  0: { stepType: 'CREATE_CONSUMER_ACCOUNT', account_uuid: 'abc-123', success: true },
  1: { stepType: 'ADD_BANK_ACCOUNT', payment_method_uuid: 'xyz-789', success: true }
}

// Step 3 receives: { account_uuid: 'abc-123', payment_method_uuid: 'xyz-789' }
Accessing Previous Step Data

Step components can access data from previous steps through their props:

// In getStepProps() function (MultiStepFlow.svelte)
case 'CHECKOUT_CONFIRM':
  const previousStepResult = flowState.stepResults[flowState.currentStepIndex - 1];
  const accountUuid = previousStepResult?.account_uuid || accountUuid;
  const paymentMethodUuid = previousStepResult?.payment_method_uuid || null;
  
  return {
    accountUuid,
    paymentMethodUuid,
    amount: config.amount  // From flow-level config
  };

Best Practices

1. Always Listen for Domain-Specific Events

For key business operations, listen to domain-specific events (account, payment-method) in addition to generic form events:

// ✅ Good - Listen to specific domain event
element.addEventListener('account', (event) => {
  updateUserState(event.detail.account_uuid);
  trackAnalytics('account_created', event.detail);
});

// ❌ Less ideal - Only listen to generic event
element.addEventListener('form-success', (event) => {
  // Have to check stepType to know what happened
  if (event.detail.stepType === 'CREATE_CONSUMER_ACCOUNT') {
    // ...
  }
});
2. Handle Errors Gracefully

Always provide error handlers for critical operations:

element.addEventListener('form-error', (event) => {
  const error = event.detail.error;
  console.error('Operation failed:', error);
  
  // Show user-friendly error message
  showErrorNotification(error.message);
  
  // Report to error tracking service
  reportError(error);
});
3. Use Typed Event Listeners

For TypeScript projects, use typed event listeners:

import type { AccountEventDetail, PaymentMethodEventDetail } from '@alviere/ui-svelte/types';

const element = document.querySelector('alviere-create-consumer-account');

element?.addEventListener('account', ((event: CustomEvent<AccountEventDetail>) => {
  // event.detail is now typed
  const { account_uuid, status } = event.detail;
}) as EventListener);
4. Clean Up Event Listeners

Always remove event listeners when components unmount:

// React example
useEffect(() => {
  const flow = flowRef.current;
  
  const handleAccount = (event) => { /* ... */ };
  const handleComplete = (event) => { /* ... */ };
  
  flow.addEventListener('account', handleAccount);
  flow.addEventListener('flow-complete', handleComplete);
  
  // Cleanup
  return () => {
    flow.removeEventListener('account', handleAccount);
    flow.removeEventListener('flow-complete', handleComplete);
  };
}, []);

Component Event Reference

CreateConsumerAccount

Dispatches:

  • account (when account is created with status 'CREATED')
  • account (when account becomes 'ACTIVE')
  • form-success (when operation completes)
  • form-error (on failure)

Event Details:

// account event
{ account_uuid: string, status: 'CREATED' | 'ACTIVE' }

// form-success event
{ stepType: 'CREATE_CONSUMER_ACCOUNT', account_uuid: string, success: true, stepIndex?: number }
AddBankAccount

Dispatches:

  • payment-method (immediately after backend creates payment method)
  • form-success (when operation completes)
  • form-error (on failure)

Event Details:

// payment-method event
{ payment_method_uuid: string, status: 'ACTIVE' }

// form-success event
{ stepType: 'ADD_BANK_ACCOUNT', payment_method_uuid: string, success: true, stepIndex?: number }
CheckoutConfirm

Dispatches:

  • form-success (when user confirms payment)

Event Details:

// form-success event
{ stepType: 'CHECKOUT_CONFIRM', success: true, amount: number }
MultiStepFlow

Dispatches:

  • flow-step-complete (after each step completes)
  • flow-step-error (if a step fails)
  • flow-complete (when all steps are done)
  • flow-reset (when flow is reset)

Bubbles Up: All events from child components bubble up through the flow, so you can listen for account, payment-method, etc. on the flow element itself.

Flows Components Guide

Link to a page in the guide