-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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
Key Benefits:
- ✅ No custom API calls needed
- ✅ Native save/discard functionality
- ✅ Proper dirty state detection
- ✅ Works with all Directus save options
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
- useExpandableBlocks - Main orchestrator that combines all functionality
- useBlockState - Manages reactive state and dirty tracking
- useBlockActions - Handles CRUD operations (add, update, delete, reorder)
- useM2AData - Transforms data between storage and display formats
- useBlockWatchers - Manages reactive side effects and save detection
- useUIHelpers - Provides computed properties for UI rendering
- useItemSelector - Handles selection of existing items from collections
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
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
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
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)
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]
The useM2AData
composable implements intelligent selective emitting:
- Clean blocks → emit ID only
- Dirty blocks → emit full object
- Result: Minimal payload size & accurate dirty detection
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
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
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 });
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)
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
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
Key Store Interactions:
- Relations Store - M2A structure and sort fields
- Fields Store - Field configurations
- Permissions Store - CRUD permissions
- Collections Store - Icons and display templates
- Notifications Store - Success/error messages
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
}
- 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