Skip to content

Architecture Overview

Christopher Schwarz edited this page Aug 26, 2025 · 7 revisions

Architecture Overview

The Expandable Blocks extension is a Vue 3 interface extension for Directus that enables inline editing of M2A (Many-to-Any) relationships while integrating seamlessly with Directus' native save system.

Note: As of v1.0.0, the API functionality has been separated into an optional API extension for better modularity and optional usage tracking features.

🎯 Core Concept

Unlike traditional block editors that use custom API calls, this extension works entirely within Directus' native form system:

graph LR
    subgraph "Traditional Block Editors"
        A1[User Edits] --> A2[Custom Save Logic]
        A2 --> A3[Custom API Calls]
        A3 --> A4[Manual State Sync]
    end
    
    subgraph "Expandable Blocks"
        B1[User Edits] --> B2[Emit to Directus]
        B2 --> B3[Native Form State]
        B3 --> B4[Standard Save Flow]
    end
Loading

Key Benefits:

  • ✅ No custom API calls needed
  • ✅ Native save/discard functionality
  • ✅ Proper dirty state detection
  • ✅ Works with all Directus save options

🏗️ Composables Architecture

The extension uses a modular composables pattern for better maintainability:

graph TB
    subgraph "Components"
        IV[interface.vue<br/>Orchestrator]
        BL[BlockList.vue]
        BI[BlockItem.vue]
        BH[BlockHeader.vue]
        ISD[ItemSelectorDrawer.vue]
        IST[ItemSelectorTable.vue]
        STI[SearchTagInput.vue]
    end
    
    subgraph "Composables"
        UEB[useExpandableBlocks<br/>Main Logic]
        BS[useBlockState<br/>State]
        BA[useBlockActions<br/>CRUD]
        M2A[useM2AData<br/>Transform]
        BW[useBlockWatchers<br/>Reactivity]
        UI[useUIHelpers<br/>UI Logic]
        IS[useItemSelector<br/>Item Selection]
    end
    
    subgraph "Directus Integration"
        FS[Field Store]
        RS[Relations Store]
        PS[Permissions Store]
    end
    
    IV --> UEB
    UEB --> BS & BA & M2A & BW & UI & IS
    BS & BA & M2A --> FS & RS & PS
    BL & BI & BH --> UEB
    ISD & IST & STI --> IS
Loading

Key Composables

  1. useExpandableBlocks - Main orchestrator that combines all functionality
  2. useBlockState - Manages reactive state and dirty tracking
  3. useBlockActions - Handles CRUD operations (add, update, delete, reorder)
  4. useM2AData - Transforms data between storage and display formats
  5. useBlockWatchers - Manages reactive side effects and save detection
  6. useUIHelpers - Provides computed properties for UI rendering
  7. useItemSelector - Handles selection of existing items from collections

💡 The M2A Challenge

Directus stores M2A relationships differently than it displays them:

Storage: [57, 58, 59] (junction IDs)
Display: [{ id: 57, collection: "content_text", item: {...} }] (full objects)

Our solution: Selective Emitting

  • Clean blocks → emit ID only
  • Dirty blocks → emit full object
  • Result: Minimal payload size & accurate dirty detection

🔄 Data Flow

sequenceDiagram
    participant User
    participant Component
    participant Composables
    participant Directus
    
    User->>Component: Edit Block
    Component->>Composables: Update State
    Composables->>Composables: Track Dirty State
    Composables->>Component: Emit Changes
    Component->>Directus: Native Form Update
    
    Note over Directus: User clicks Save
    Directus->>Directus: Process M2A Data
    Directus->>Component: Return Saved Data
    Component->>Composables: Detect Save
    Composables->>Composables: Reset Dirty States
Loading

📁 Project Structure

src/
├── interface.vue          # Main orchestrator (~124 lines)
├── components/           # Modular UI components (14+ files)
│   ├── BlockList.vue
│   ├── BlockItem.vue
│   ├── ItemSelectorDrawer.vue
│   ├── ItemSelectorTable.vue
│   └── ...
├── composables/          # Business logic (7 composables)
├── services/             # Service layer (API client, relation checker)
├── utils/                # Helper functions
└── types/                # TypeScript definitions

The refactored architecture reduced complexity from ~1400 lines in a single file to a modular structure with clear separation of concerns. Recent additions include:

  • Item Selector System: Add existing items from other collections
  • Table View: Alternative view with sortable columns and field selection
  • Advanced Search: Full-text search with field-specific filtering (e.g., title:"search term")
  • Persistent Settings: User preferences for drawer width, view mode, and selected fields
  • API Client Service: Optional integration with external API for usage tracking

🌊 Data Flow & State Management

The M2A Challenge

Directus stores M2A relationships differently than it displays them:

Storage Format: [57, 58, 59] (junction IDs)
Display Format: [{ id: 57, collection: "content_text", item: {...} }] (full objects)

Solution: Selective Emitting

graph TD
    A[useM2AData.prepareValueForEmit] --> B{Is Block Dirty?}
    B -->|Yes| C[Emit Full Object]
    B -->|No| D{Position Changed?}
    D -->|Yes| C
    D -->|No| E{Is Temp ID?}
    E -->|Yes| C
    E -->|No| F[Emit ID Only]
Loading

The useM2AData composable implements intelligent selective emitting:

  • Clean blocks → emit ID only
  • Dirty blocks → emit full object
  • Result: Minimal payload size & accurate dirty detection

Component Lifecycle

sequenceDiagram
    participant User
    participant Component
    participant Watchers
    participant Directus
    
    Note over Component: Mount Phase
    Component->>Watchers: Setup watchers
    
    Note over Directus: Data Arrival
    Directus->>Component: props.value
    Component->>Watchers: Process data
    Watchers->>Component: Store original state
    
    Note over User: Interaction
    User->>Component: Edit block
    Component->>Component: Update state
    Component->>Component: Mark dirty
    Component->>Directus: Emit changes
    
    Note over User: Save
    User->>Directus: Click save
    Directus->>Directus: Process M2A
    Directus->>Component: Return saved data
    Component->>Watchers: Detect save
    Watchers->>Component: Reset dirty state
Loading

State Management

The extension tracks several critical states through composables:

useBlockState:

  • items: Current blocks data
  • blockOriginalStates: Map for dirty detection
  • originalItemOrder: Track position changes
  • expandedBlocks: UI expansion state

useBlockWatchers:

  • Monitors props changes
  • Detects save completion
  • Handles discard operations
  • Manages timing and synchronization

useM2AData:

  • Transforms between storage and display formats
  • Prepares data for emission
  • Handles selective emitting based on dirty state

Critical Timing

Data must be processed in watchers, not in mount:

// ❌ Wrong - Manual timing management
onMounted(() => {
  processData(props.value); // Often null!
});

// ✅ Correct - Handled by useBlockWatchers composable
watch(() => props.value, (newValue) => {
  if (newValue) {
    actions.processInitialData(newValue);
  }
}, { immediate: true });

🔌 Native Directus Integration

Native Integration Principle

Instead of custom save logic, the extension works within Directus' native form system:

// ❌ Traditional approach - bypass Directus
await api.post('/items/blocks', blockData)

// ✅ Our approach - emit to Directus
emit('input', preparedData)

Optional API Extension

For advanced usage tracking features, you can install the separate API extension:

  • Without API: All core features work using native Directus API
  • With API: Adds usage tracking, reference detection, and safe deletion warnings

Store Integration

graph TB
    subgraph "Composables"
        BA[useBlockActions]
        M2A[useM2AData]
        UI[useUIHelpers]
    end
    
    subgraph "Directus Stores"
        FS[Fields Store<br/>Field configs]
        RS[Relations Store<br/>M2A structure]
        PS[Permissions Store<br/>Access control]
        CS[Collections Store<br/>Metadata]
        NS[Notifications Store<br/>User feedback]
    end
    
    BA --> PS & NS
    M2A --> RS & FS
    UI --> CS
Loading

Key Store Interactions:

  1. Relations Store - M2A structure and sort fields
  2. Fields Store - Field configurations
  3. Permissions Store - CRUD permissions
  4. Collections Store - Icons and display templates
  5. Notifications Store - Success/error messages

External API Extension (Optional)

The optional API extension provides usage tracking and advanced features:

/expandable-blocks-api/health           # API availability check
/expandable-blocks-api/:collection/detail    # Item details with usage info
/expandable-blocks-api/:collection/search    # Advanced search (optional)
/expandable-blocks-api/:collection/metadata  # Collection metadata (optional)

Primary Purpose:

  • Track where items are used across collections
  • Provide safe deletion warnings
  • Handle junction table relationships

Example:

POST /expandable-blocks-api/content_text/detail
{
  ids: ["item-123"],
  fields: ["*.*"]
}

// Response with usage tracking
{
  data: [{
    id: "item-123",
    usage_locations: [...],
    usage_summary: { total_count: 3, can_delete: false }
  filteredCount: 20
}

📚 Related Documentation

  • Configuration - Learn how to configure the interface options
  • Development - Set up development environment and contribute
  • Security - Understand security measures and best practices
  • Installation - Installation and setup instructions

Back to: Home

Directus Expandable Blocks

🏠 Getting Started

📖 Technical Documentation

🛠️ Development

📋 Project Info

🔗 Quick Links


GitHub release

Clone this wiki locally