Appearance
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)1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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, runmounted(), and register theanimation:showHidelistener -- all within a singlerequestAnimationFrame. This race condition meant show-animations could silently fail. Withv-show, the component is always mounted and listening, so animations trigger reliably. - No click leakage: Elements with
display: nonehave 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 properties1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Every step is required for the chain to work. Missing one step breaks the entire flow.
Architecture Principles
- Unidirectional Data Flow -- Data flows in ONE direction: Frontend -> Composer -> Engine. Never send data backwards.
- 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. - Type Safety -- Define types at every stage to catch errors early.
- 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',
})
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
JSON structure:
json
{
"showHide_0": {
"properties": {
"body": {
"targets": [
{ "target": "t1", "action": "hide" },
{ "target": "b1", "action": "show" }
]
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Composer Transformation
File: /Creative-Composer/src/remapper/remapData.ts
typescript
} else if (opKey.includes('showHide') && body.targets) {
comp.payload.targets = body.targets;
}1
2
3
2
3
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),
})
}
})
},
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
},
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
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>),
})1
2
3
4
5
2
3
4
5
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
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
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>1
2
3
4
5
2
3
4
5
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.isSubmitting1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
| SliderStateCondition1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 valuegetStatesForBlockType(blockType)-- get all conditions for a block typegetActionsForCondition(condition)-- get available actions for a conditionactionRequiresDelay(action)-- check if action needs delay parametergetStateKey(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
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}
]
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,
}
}1
2
3
4
5
6
2
3
4
5
6
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)
})
},
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
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 }
)1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Actions execute via showHideFunctions (same as basic ShowHide), with hideAfterDelay using setTimeout.
Registration
File: /Creative-Engine/src/utils/funcBlocks.ts
typescript
ConditionalShowHide: ConditionalShowHide.init,1
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 hidden1
2
3
4
5
6
2
3
4
5
6
Creating New Operators (Template)
Step-by-Step
- Application-Frontend -- Store config in
op.properties.body.yourDatausingthis.$set()for Vue 2 reactivity - Composer Input Types -- Add to
OperatorProperties.bodyinjsonTypes.ts - Composer Mapping -- Add to
remapData.tsif-else chain:opKey.includes('yourOperator') && body.yourData - Composer Output Types -- Add to
FlowComponentPayloadinnewTypes.ts - Engine Payload Interface -- Add to payload in
FlowComponentInterface.ts - Engine Registration -- Add to
funcBlocks.ts:YourOperator: YourOperator.init - Engine Implementation -- Create operator file, read from
componentData[0]?.payload.yourData - Engine State (optional) -- Add reactive state to
dataStore.tsusingref() - Component Integration (optional) -- Add computed properties in
BlockMixin.tsor individual components
Operator Patterns
| Pattern | Example | DataStore? | Component Integration? |
|---|---|---|---|
| Simple Data Pass-Through | ChangeText, ChangeImage | No | No |
| State Management | ShowHide | Yes | Yes (via computed) |
| Complex Processing | Middleware, Script | Maybe | Maybe |
Extending Conditional Show/Hide
To add a new block type's conditions:
- Add enum in
types.ts(Application-Frontend) - Add registry entry in
stateRegistry.ts(Application-Frontend) - Mirror enum in
StateConditions.ts(Creative-Engine) - Add mapping in
STATE_KEY_MAP(Creative-Engine)
No changes needed in the operator component or business logic.
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Wrong property path | Storing in properties.yourData instead of properties.body.yourData | Always use op.properties.body.yourData |
| Missing type definitions | TypeScript errors when accessing data | Add types in jsonTypes.ts, newTypes.ts, AND FlowComponentInterface.ts |
| Missing Composer mapping | Data exists in JSON but payload.yourData is undefined | Add mapping in remapData.ts with correct opKey.includes() check |
| Not rebuilding Composer | Changes don't take effect | Run npm run build in Creative-Composer after changes |
| Wrong operator name | Operator not found in funcBlocks | Ensure name matches across frontend file, remapData.ts, and funcBlocks key |
| Obfuscated class names | CSS classes don't work | Use Vue reactivity instead of CSS classes |
| Enum sync drift | Conditional rules fail silently | Keep 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)1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Debugging Checklist
- [ ] Frontend: Data stored in
op.properties.body.yourData, persists on navigation - [ ] Composer: Types in
jsonTypes.tsandnewTypes.ts, mapping inremapData.ts, rebuilt withnpm run build - [ ] Engine: Type in
FlowComponentInterface.ts, operator created, registered infuncBlocks.ts, console log confirms data received - [ ] Runtime: Compiled JSON has correct structure, preview works as expected
File Reference
Application-Frontend
| Purpose | Path |
|---|---|
| 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
| Purpose | Path |
|---|---|
| Input types (from Frontend) | /src/remapper/jsonTypes.ts |
| Output types (to Engine) | /src/remapper/newTypes.ts |
| Data transformation | /src/remapper/remapData.ts |
Creative-Engine
| Purpose | Path |
|---|---|
| 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 |