Skip to main content
The Kodexa Data Form system is built on a card-based architecture. Cards are reusable, configurable UI components that can be composed together to create complex data-driven interfaces.

Card Architecture

Component Structure

Each card consists of three parts:
  1. Vue Component (*.vue) - The actual UI implementation
  2. Metadata File (*.ts) - Configuration and options definition
  3. Documentation (*.md) - Usage guide and examples

Registration System

Cards are automatically discovered and registered through:
// src/components/dataForm/cards/cards.ts
const cardModules = import.meta.glob("./*.vue", { eager: true });
const metadataModules = import.meta.glob("./*.ts", { eager: true });
This means:
  • New cards are automatically available once created
  • No manual registration required
  • Metadata is optional but recommended

Card Lifecycle

1. Definition

Cards are defined in YAML configuration:
- type: dataStoreGrid
  id: main-grid
  properties:
    dataStoreRef: "customers"
  layout:
    x: 0
    y: 0
    w: 12
    h: 10

2. Rendering

The data form renderer:
  1. Reads the YAML configuration
  2. Looks up the card type in the registry
  3. Instantiates the Vue component
  4. Passes properties as props
  5. Positions using grid layout

3. Interaction

Cards communicate through:
  • Props - Configuration from YAML
  • Events - User interactions (clicks, edits, etc.)
  • Store - Shared state (workspace, project, data stores)

Card Types

Container Cards

Purpose: Group and organize other cards Characteristics:
  • supportsChildren: true
  • Render child cards inside
  • Provide layout and visual structure
Examples:
  • tabs - Tabbed interface
  • cardGroup - Visual grouping
  • cardPanel - Titled panel with collapse
Usage:
- type: tabs
  id: main-tabs
  children:
    - type: dataStoreGrid
      id: tab1
      properties:
        title: "Data"

Data Display Cards

Purpose: Show read-only data Characteristics:
  • supportsChildren: false
  • Display data from stores/APIs
  • Often read-only
  • Support filtering/sorting
Examples:
  • dataStoreGrid - Tabular data
  • dataObjectsTree - Hierarchical data
  • label - Static text

Data Editor Cards

Purpose: Allow data modification Characteristics:
  • Interactive inputs
  • Validation
  • Save/update functionality
  • Change tracking
Examples:
  • dataAttributeEditor - Single field editor
  • taxonGrid - Grid with inline editing

Specialized Cards

Purpose: Domain-specific functionality Characteristics:
  • Advanced features
  • Complex configuration
  • Specific use cases
Examples:
  • transposedGridRollup - Financial rollups
  • exceptions - Exception tracking
  • workspaceDataGrid - Cross-workspace views

Card Metadata

Component Metadata Structure

export const componentMetadata = {
  label: "Display Name",
  description: "What this card does",
  supportsChildren: false,
  defaultWidth: 12,
  defaultHeight: 10,
  options: [
    {
      name: "propertyName",
      label: "Display Label",
      description: "What this property controls",
      type: "string" | "boolean" | "number" | "object",
      required: true | false,
      default: "defaultValue"
    }
  ]
};

Metadata Properties

label

  • Display name shown in card selector/documentation
  • Should be human-readable and concise

description

  • Explains the card’s purpose
  • Shown in tooltips and documentation

supportsChildren

  • true if card can contain child cards
  • false for leaf cards

defaultWidth / defaultHeight

  • Suggested dimensions in grid units
  • Used when adding new cards in editor

options

  • Array of configurable properties
  • Each option defines a property that can be set in YAML

Card Properties

Common Properties

All cards have access to:
interface CommonCardProps {
  // From YAML configuration
  id: string;              // Unique card identifier
  type: string;            // Card type
  properties: object;      // Card-specific properties
  layout: {
    x: number;             // Column position
    y: number;             // Row position
    w: number;             // Width in columns
    h: number;             // Height in rows
  };
  children?: Card[];       // Child cards (if container)

  // Runtime context
  readonly?: boolean;      // Disable editing
  visible?: boolean;       // Show/hide card
}

Type-Safe Properties

Each card component should define its props interface:
<script setup lang="ts">
interface Props {
  // From common
  id: string;

  // Card-specific
  dataStoreRef?: string;
  title?: string;
  readonly?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  readonly: false
});
</script>

Card Communication

Parent → Child (Props)

Data flows down through props:
- type: tabs
  properties:
    readonly: true    # Passed to tabs
  children:
    - type: grid
      # Inherits readonly context

Child → Parent (Events)

Events bubble up:
<script setup>
const emit = defineEmits(['update', 'delete']);

function handleSave() {
  emit('update', newValue);
}
</script>

Shared State (Store)

Cards access shared state through Pinia stores:
<script setup>
import { useWorkspaceStore } from '~/store/useWorkspace';

const workspaceStore = useWorkspaceStore();
const { currentWorkspace } = storeToRefs(workspaceStore);
</script>

Creating New Cards

1. Create Vue Component

<!-- src/components/dataForm/cards/kodexa-form-card-my-new-card.vue -->
<script setup lang="ts">
interface Props {
  id: string;
  myProperty?: string;
}

const props = withDefaults(defineProps<Props>(), {
  myProperty: 'default'
});
</script>

<template>
  <div class="my-new-card">
    <h3>{{ props.myProperty }}</h3>
    <!-- Card content -->
  </div>
</template>

2. Create Metadata File

// src/components/dataForm/cards/kodexa-form-card-my-new-card.ts
export const componentMetadata = {
  label: "My New Card",
  description: "Does something cool",
  supportsChildren: false,
  defaultWidth: 6,
  defaultHeight: 4,
  options: [
    {
      name: "myProperty",
      label: "My Property",
      description: "Controls the thing",
      type: "string",
      required: false,
      default: "default"
    }
  ]
};

3. Test the Card

# test-form.yaml
name: "Test My New Card"
cards:
  - type: myNewCard
    id: test
    properties:
      myProperty: "test value"
    layout:
      x: 0
      y: 0
      w: 6
      h: 4

4. Document the Card

Create documentation following the card documentation template.

Best Practices

1. Single Responsibility

Each card should do one thing well:
Good Examples:
  • label - Shows text
  • dataStoreGrid - Shows grid data
Bad Example:
  • megaCard - Shows grid, tree, and form all in one

2. Composability

Design cards to work together:
# Good: Separate concerns
- type: cardPanel
  properties:
    title: "Customer Data"
  children:
    - type: dataStoreGrid
      # Grid focused on data display
    - type: exceptions
      # Exceptions focused on errors

3. Configuration Over Code

Use properties instead of hardcoding:
<!-- ❌ Bad: Hardcoded -->
<template>
  <div class="grid" style="height: 400px">
</template>

<!-- ✅ Good: Configurable -->
<template>
  <div class="grid" :style="{ height: `${props.height}px` }">
</template>

4. Consistent Naming

Follow the naming convention:
  • File: kodexa-form-card-{name}.vue
  • Type: {camelCaseName}
  • Metadata: kodexa-form-card-{name}.ts
  • Docs: {name}.md

5. Validation

Validate required properties:
<script setup>
const props = defineProps<{
  dataStoreRef: string; // Required, no default
  title?: string;       // Optional
}>();

// Validate on mount
onMounted(() => {
  if (!props.dataStoreRef) {
    console.error('dataStoreRef is required');
  }
});
</script>

Performance Considerations

1. Lazy Loading

Cards are loaded on-demand:
// Automatically handled by cards.ts
const cardModules = import.meta.glob("./*.vue", { eager: true });

2. Reactive Updates

Use computed properties for derived state:
<script setup>
const filteredData = computed(() => {
  return props.data.filter(item => item.visible);
});
</script>

3. Event Throttling

Throttle expensive operations:
<script setup>
import { throttle } from 'lodash';

const handleSearch = throttle((query) => {
  // Expensive search operation
}, 300);
</script>