Skip to content

Show-Hide System

Overview

The Show-Hide system provides runtime control of block visibility without re-rendering. It includes two operators: the basic ShowHide operator (immediate show/hide) and the ConditionalShowHide operator (state-driven, rule-based show/hide). Both follow a unidirectional data flow from Application-Frontend through Creative-Composer to Creative-Engine.


Architecture

System Layers

Application-Frontend (UI)
    | (stores config in op.properties.body)
Creative-Composer (Data Transformation)
    | (maps body -> payload)
Creative-Engine (Runtime Execution)
    | (reads payload, updates DataStore.runtimeHidden)
Visual Elements (Rendering)
    | (v-show="!isRuntimeHidden" hides elements)

Design Decisions

Why Vue Reactivity Instead of CSS Classes?

Creative-Engine uses class name obfuscation. A CSS class .hidden becomes .a1b2c3 after obfuscation, but JavaScript element.classList.add('hidden') adds the unobfuscated class -- so the selector never matches. Using Vue reactivity with v-if eliminates this problem entirely: no CSS classes or DOM manipulation needed.

Why v-show Instead of v-if?

Visual blocks use v-show="!isRuntimeHidden", which toggles display: none while keeping the component mounted in the DOM. This was changed from v-if for two reasons:

  • Animation reliability: ShowHide supports animated show/hide via AnimationMixin. With v-if, showing a block required Vue to create the component, run mounted(), and register the animation:showHide listener -- all within a single requestAnimationFrame. This race condition meant show-animations could silently fail. With v-show, the component is always mounted and listening, so animations trigger reliably.
  • No click leakage: Elements with display: none have no bounding box and cannot receive pointer events, so hidden blocks don't interfere with click analytics or user interaction.

Conversation operators (MessageHolder) still use v-if because they are created dynamically as the flow progresses and don't need pre-mounted animation listeners.

Why DataStore Instead of Direct DOM?

  • Single source of truth with automatic Vue reactivity
  • Works across iframe boundaries
  • No manual element queries or timing issues with iframe loading
  • Testable and debuggable

Data Flow

Operator Data Flow Chain (9 Steps)

1. APPLICATION-FRONTEND
   User configures operator in UI
   Data stored in: op.properties.body.yourData

2. CREATIVE-COMPOSER: Type Definitions (Input)
   File: jsonTypes.ts
   Defines: OperatorProperties.body.yourData

3. CREATIVE-COMPOSER: Data Transformation
   File: remapData.ts
   Maps: body.yourData -> comp.payload.yourData

4. CREATIVE-COMPOSER: Type Definitions (Output)
   File: newTypes.ts
   Defines: FlowComponentPayload.yourData

5. CREATIVE-ENGINE: Payload Interface
   File: FlowComponentInterface.ts
   Defines: payload.yourData type

6. CREATIVE-ENGINE: Operator Registration
   File: funcBlocks.ts
   Maps: 'YourOperator' -> YourOperator.init

7. CREATIVE-ENGINE: Operator Implementation
   File: YourOperator.ts
   Reads: payload.yourData
   Executes: Custom logic

8. CREATIVE-ENGINE: State Management (if needed)
   File: dataStore.ts
   Updates: Reactive state

9. CREATIVE-ENGINE: Component Integration (if needed)
   File: BlockMixin.ts or individual components
   Reacts: To state changes via computed properties

Every step is required for the chain to work. Missing one step breaks the entire flow.

Architecture Principles

  1. Unidirectional Data Flow -- Data flows in ONE direction: Frontend -> Composer -> Engine. Never send data backwards.
  2. Vue Reactivity Over DOM Manipulation -- Use reactive state (ref(), computed properties) instead of direct DOM manipulation. Works across iframe boundaries, updates automatically, and is testable.
  3. Type Safety -- Define types at every stage to catch errors early.
  4. Validation -- Always validate data exists before using it.

ShowHide Operator (Basic)

Frontend Configuration

File: /Application-Frontend/src/pages/Chatbots/components/CavaiFlow/operators/ShowHideOp.vue

User selects target blocks via TargetOpSelector. Targets are stored in op.properties.body.targets with structure { target: "t1", action: "hide" }.

javascript
addTarget(targetName) {
  if (!this.op.properties.body.targets) {
    this.$set(this.op.properties.body, 'targets', [])
  }
  this.op.properties.body.targets.push({
    target: targetName,
    action: 'hide',
  })
}

JSON structure:

json
{
  "showHide_0": {
    "properties": {
      "body": {
        "targets": [
          { "target": "t1", "action": "hide" },
          { "target": "b1", "action": "show" }
        ]
      }
    }
  }
}

Composer Transformation

File: /Creative-Composer/src/remapper/remapData.ts

typescript
} else if (opKey.includes('showHide') && body.targets) {
  comp.payload.targets = body.targets;
}

Type definitions required in both:

  • /Creative-Composer/src/remapper/jsonTypes.ts -- targets?: Array<{ target: string; action: 'hide' | 'show' }>
  • /Creative-Composer/src/remapper/newTypes.ts -- targets?: Array<{ target: string; action: 'hide' | 'show' }>

Engine Runtime

Operator Implementation

File: /Creative-Engine/src/components/blocks/functional/ShowHide.ts

typescript
export const ShowHide = {
  init(componentData: FlowComponentInterface[]): void {
    const targets = componentData[0]?.payload.targets
    if (!targets) return

    targets.forEach(({ target, action, animation }) => {
      if (!animation || !animation.effects || animation.effects.length === 0) {
        // No animation -- instant toggle
        if (action === 'hide') showHideFunctions.hideElement(target)
        else if (action === 'show') showHideFunctions.showElement(target)
        return
      }

      const { effects, duration = 300, easing = 'ease' } = animation

      // Conversation sub-elements (m1, c1, etc.) don't have AnimationMixin,
      // so they're animated directly via DOM with the Web Animations API.
      // Block elements have AnimationMixin and use event-based approach.
      if (action === 'show') {
        showHideFunctions.showElement(target)  // toggles display via v-show
        // Then emit animation event (component is already mounted and listening)
        DataStore.emitter.emit('animation:showHide', { target, action, effects, duration, easing })
      } else if (action === 'hide') {
        // Play animation first, THEN hide on completion
        DataStore.emitter.emit('animation:showHide', {
          target, action, effects, duration, easing,
          onComplete: () => showHideFunctions.hideElement(target),
        })
      }
    })
  },
}

Show/Hide Functions

File: /Creative-Engine/src/components/blocks/functional/Script.ts

Pure state management -- no DOM manipulation. Functions update DataStore.runtimeHidden reactive ref, and Vue reactivity handles the rest.

typescript
export const showHideFunctions = {
  hideElement: (abbreviation: string) => {
    DataStore.runtimeHidden.value[abbreviation] = true
  },
  showElement: (abbreviation: string) => {
    DataStore.runtimeHidden.value[abbreviation] = false
  },
}

DataStore State

File: /Creative-Engine/src/services/dataStore.ts

typescript
const defaultState = () => ({
  // Runtime visibility control (for ShowHide operator)
  // Maps block abbreviation (e.g., 'b1', 't1') to hidden state
  runtimeHidden: ref({} as Record<string, boolean>),
})

BlockMixin Integration

File: /Creative-Engine/src/mixins/BlockMixin.ts

Provides isRuntimeHidden computed property to all blocks:

typescript
isRuntimeHidden() {
  const { wrap, idBased } = this.blockClassNames

  // Conversation operators use idBased directly (e.g., 'm1', 'c1')
  // Block elements use wrap class (e.g., 'b1-wrap', 't1-wrap')
  const isConversation = /^[mclir]\d+$/.test(idBased)
  const lookupKey = isConversation ? idBased : wrap

  return DataStore.runtimeHidden.value[lookupKey] === true
}

Visual Elements

All visual block components (CreativeTextBlock.vue, CreativeButtonBlock.vue, CreativeGraphicBlock.vue, CreativeHtmlBlock.vue, CreativeFormBlock.vue, CreativeSliderBlock.vue) use v-show="!isRuntimeHidden" in their template via BlockMixin. This keeps the component mounted so AnimationMixin listeners are always active.

vue
<template>
  <div v-show="!isRuntimeHidden" :class="[blockClassNames.wrap]">
    <!-- Block content -->
  </div>
</template>

Conversation operators (MessageHolder.vue) still use v-if because they are created progressively as the flow advances.


ConditionalShowHide Operator (State-Driven)

Architecture Approach: Enum-Based

The conditional system uses an enum-based architecture for type safety, DRY code, IDE autocomplete, refactoring safety, and extensibility. Enum values serve as both display labels and keys.

Trade-off: Requires syncing enums between Application-Frontend and Creative-Engine.

Architecture Diagram

APPLICATION-FRONTEND
  types.ts          -- FormStateCondition, StateAction enums, ConditionalRule interface
  stateRegistry.ts  -- STATE_CONDITION_REGISTRY, helper functions
  ConditionalShowHideOp.vue -- UI for selecting target, conditions, actions
       |
       | JSON with enum values
       v
CREATIVE-COMPOSER
  remapData.ts      -- Maps body.rules -> payload.conditionalRules (passes enums through)
       |
       | Compiled payload
       v
CREATIVE-ENGINE
  StateConditions.ts         -- Mirror enums, STATE_KEY_MAP, ACTION_MAP
  ConditionalShowHide.ts     -- Reads payload, sets up Vue watchers on formStore
  formStore.ts (existing)    -- state.submitSuccess, state.submitError, state.isSubmitting

Type Definitions

File: /Application-Frontend/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts

typescript
export enum ConditionalBlockType {
  FORM = 'formProperties',
  VIDEO = 'videoProperties',
  SLIDER = 'sliderProperties',
}

export enum FormStateCondition {
  SUBMIT_SUCCESS = 'Form Submitted Successfully',
  SUBMIT_ERROR = 'Form Submission Error',
  IS_SUBMITTING = 'Form Submitting',
}

export enum VideoStateCondition {
  VIDEO_ENDED = 'Video Finished Playing',
  VIDEO_PAUSED = 'Video Paused',
  VIDEO_PLAYING = 'Video Playing',
}

export enum SliderStateCondition {
  LAST_SLIDE_REACHED = 'Last Slide Reached',
  FIRST_SLIDE_REACHED = 'First Slide Reached',
}

export enum StateAction {
  HIDE = 'Hide',
  SHOW = 'Show',
  HIDE_AFTER_DELAY = 'Hide After Delay',
}

export interface StateConditionDefinition {
  key: string                    // Internal key for formStore mapping
  label: string                  // Display label (from enum value)
  blockType: ConditionalBlockType
  availableActions: StateAction[]
}

export interface ConditionalRule {
  target: string                 // Block abbreviation (e.g., 'f1')
  condition: FormStateCondition | VideoStateCondition | SliderStateCondition
  action: StateAction
  delayMs?: number              // Required if action is HIDE_AFTER_DELAY
}

export type AnyStateCondition =
  | FormStateCondition
  | VideoStateCondition
  | SliderStateCondition

State Registry

File: /Application-Frontend/src/utils/stateRegistry.ts

Single source of truth mapping enum values to metadata (internal key, block type, available actions). Key helper functions:

  • getStateCondition(condition) -- get definition from enum value
  • getStatesForBlockType(blockType) -- get all conditions for a block type
  • getActionsForCondition(condition) -- get available actions for a condition
  • actionRequiresDelay(action) -- check if action needs delay parameter
  • getStateKey(condition) -- get internal formStore key from enum value
typescript
export const STATE_CONDITION_REGISTRY: Record<string, StateConditionDefinition> = {
  [FormStateCondition.SUBMIT_SUCCESS]: {
    key: 'submitSuccess',
    label: FormStateCondition.SUBMIT_SUCCESS,
    blockType: ConditionalBlockType.FORM,
    availableActions: [StateAction.HIDE, StateAction.HIDE_AFTER_DELAY],
  },
  [FormStateCondition.SUBMIT_ERROR]: {
    key: 'submitError',
    label: FormStateCondition.SUBMIT_ERROR,
    blockType: ConditionalBlockType.FORM,
    availableActions: [StateAction.SHOW],
  },
  [FormStateCondition.IS_SUBMITTING]: {
    key: 'isSubmitting',
    label: FormStateCondition.IS_SUBMITTING,
    blockType: ConditionalBlockType.FORM,
    availableActions: [StateAction.HIDE, StateAction.SHOW],
  },
  // Video and Slider states follow the same pattern
}

Operator Component

File: /Application-Frontend/src/pages/Chatbots/components/CavaiFlow/operators/ConditionalShowHideOp.vue

UI flow: select target block -> choose condition (filtered by block type) -> choose action (filtered by condition) -> configure delay if needed. Saves only complete, valid rules to op.properties.body.rules.

JSON structure:

json
{
  "conditionalShowHide_0": {
    "properties": {
      "body": {
        "target": "f1",
        "rules": [
          {
            "target": "f1",
            "condition": "Form Submitted Successfully",
            "action": "Hide",
            "delayMs": null
          }
        ]
      }
    }
  }
}

Composer Mapping

File: /Creative-Composer/src/remapper/remapData.ts

typescript
} else if (opKey.includes('conditionalShowHide') && body.rules) {
  comp.payload.conditionalRules = {
    target: body.target,
    rules: body.rules,
  }
}

Types added to both jsonTypes.ts and newTypes.ts with the conditionalRules structure.

Engine Implementation

State Conditions (Mirrored Enums)

File: /Creative-Engine/src/enums/StateConditions.ts

Mirrors all enums from Application-Frontend. Critical: these must stay in sync.

Provides two mapping objects:

  • STATE_KEY_MAP -- maps enum display values to internal formStore state keys (e.g., "Form Submitted Successfully" -> "submitSuccess")
  • ACTION_MAP -- maps enum display values to action function names (e.g., "Hide" -> "hide")

ConditionalShowHide Operator

File: /Creative-Engine/src/components/blocks/functional/ConditionalShowHide.ts

Reads payload.conditionalRules, sets up Vue watchers for each rule:

typescript
export const ConditionalShowHide = {
  init(componentData: FlowComponentInterface[]): void {
    const rules = (block?.payload as any)?.conditionalRules
    if (!rules || !rules.rules || rules.rules.length === 0) return

    rules.rules.forEach((rule) => {
      setupRuleWatcher(rule)
    })
  },
}

Each watcher monitors a formStore state key and triggers the corresponding action only on false -> true transitions:

typescript
watch(
  () => (formStore.state as any)[stateKey],
  (newValue, oldValue) => {
    if (newValue && !oldValue) {
      executeAction(rule.target, rule.action, rule.delayMs)
    }
  },
  { immediate: false }
)

Actions execute via showHideFunctions (same as basic ShowHide), with hideAfterDelay using setTimeout.

Registration

File: /Creative-Engine/src/utils/funcBlocks.ts

typescript
ConditionalShowHide: ConditionalShowHide.init,

Example Flow: Form Submission -> Hide Form

1. User configures:    Target: f1, Condition: "Form Submitted Successfully", Action: "Hide"
2. JSON stored:        body.rules = [{ target: "f1", condition: "Form Submitted Successfully", action: "Hide" }]
3. Composer compiles:  payload.conditionalRules = { target: "f1", rules: [...] }
4. Engine maps:        STATE_KEY_MAP["Form Submitted Successfully"] -> "submitSuccess"
5. Engine watches:     formStore.state.submitSuccess
6. Runtime:            User submits form -> submitSuccess = true -> watcher fires -> hideElement("f1") -> form hidden

Creating New Operators (Template)

Step-by-Step

  1. Application-Frontend -- Store config in op.properties.body.yourData using this.$set() for Vue 2 reactivity
  2. Composer Input Types -- Add to OperatorProperties.body in jsonTypes.ts
  3. Composer Mapping -- Add to remapData.ts if-else chain: opKey.includes('yourOperator') && body.yourData
  4. Composer Output Types -- Add to FlowComponentPayload in newTypes.ts
  5. Engine Payload Interface -- Add to payload in FlowComponentInterface.ts
  6. Engine Registration -- Add to funcBlocks.ts: YourOperator: YourOperator.init
  7. Engine Implementation -- Create operator file, read from componentData[0]?.payload.yourData
  8. Engine State (optional) -- Add reactive state to dataStore.ts using ref()
  9. Component Integration (optional) -- Add computed properties in BlockMixin.ts or individual components

Operator Patterns

PatternExampleDataStore?Component Integration?
Simple Data Pass-ThroughChangeText, ChangeImageNoNo
State ManagementShowHideYesYes (via computed)
Complex ProcessingMiddleware, ScriptMaybeMaybe

Extending Conditional Show/Hide

To add a new block type's conditions:

  1. Add enum in types.ts (Application-Frontend)
  2. Add registry entry in stateRegistry.ts (Application-Frontend)
  3. Mirror enum in StateConditions.ts (Creative-Engine)
  4. Add mapping in STATE_KEY_MAP (Creative-Engine)

No changes needed in the operator component or business logic.


Common Pitfalls

PitfallProblemSolution
Wrong property pathStoring in properties.yourData instead of properties.body.yourDataAlways use op.properties.body.yourData
Missing type definitionsTypeScript errors when accessing dataAdd types in jsonTypes.ts, newTypes.ts, AND FlowComponentInterface.ts
Missing Composer mappingData exists in JSON but payload.yourData is undefinedAdd mapping in remapData.ts with correct opKey.includes() check
Not rebuilding ComposerChanges don't take effectRun npm run build in Creative-Composer after changes
Wrong operator nameOperator not found in funcBlocksEnsure name matches across frontend file, remapData.ts, and funcBlocks key
Obfuscated class namesCSS classes don't workUse Vue reactivity instead of CSS classes
Enum sync driftConditional rules fail silentlyKeep enums identical between Application-Frontend and Creative-Engine

Debugging

Console Logs at Each Stage

javascript
// 1. Application-Frontend
console.log('Frontend storing:', this.op.properties.body.yourData)

// 2. Creative-Composer (remapData.ts)
console.log('Composer mapping:', body.yourData)
console.log('Payload after mapping:', comp.payload)

// 3. Creative-Engine (YourOperator.ts)
console.log('Engine received:', componentData[0]?.payload)
console.log('Your data:', yourData)

Debugging Checklist

  • [ ] Frontend: Data stored in op.properties.body.yourData, persists on navigation
  • [ ] Composer: Types in jsonTypes.ts and newTypes.ts, mapping in remapData.ts, rebuilt with npm run build
  • [ ] Engine: Type in FlowComponentInterface.ts, operator created, registered in funcBlocks.ts, console log confirms data received
  • [ ] Runtime: Compiled JSON has correct structure, preview works as expected

File Reference

Application-Frontend

PurposePath
ShowHide operator UI/src/pages/Chatbots/components/CavaiFlow/operators/ShowHideOp.vue
ConditionalShowHide operator UI/src/pages/Chatbots/components/CavaiFlow/operators/ConditionalShowHideOp.vue
Conditional type definitions/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/types.ts
State registry/src/utils/stateRegistry.ts
Operator registration/src/utils/_temp_buildercomponents.ts

Creative-Composer

PurposePath
Input types (from Frontend)/src/remapper/jsonTypes.ts
Output types (to Engine)/src/remapper/newTypes.ts
Data transformation/src/remapper/remapData.ts

Creative-Engine

PurposePath
ShowHide operator/src/components/blocks/functional/ShowHide.ts
ConditionalShowHide operator/src/components/blocks/functional/ConditionalShowHide.ts
State condition enums/maps/src/enums/StateConditions.ts
Shared show/hide functions/src/components/blocks/functional/Script.ts
Operator registration/src/utils/funcBlocks.ts
Global reactive state/src/services/dataStore.ts
Shared block functionality/src/mixins/BlockMixin.ts
Payload interface/src/interfaces/components/FlowComponent/FlowComponentInterface.ts
Visual elements/src/components/creative/VisualElements/*.vue

Internal documentation