Skip to content

Sub-Block Patterns Comparison: Form, Slider, and Group

Overview

Three block types in Cavai use sub-blocks (child blocks stored inside a parent). Each evolved independently and uses a different pattern. This document compares them side-by-side and identifies potential unification opportunities.

The Three Patterns

Form: Object Properties + Triple Numbering

Storage: Children stored as properties on formProperties object

json
{
  "formProperties": {
    "blockName": "formProperties",
    "formInputProperties-1": { "blockName": "formInputProperties-1", "type": "text", "order": 0, "parent": "formProperties", "typeIndex": 1 },
    "formInputProperties-2": { "blockName": "formInputProperties-2", "type": "email", "order": 1, "parent": "formProperties", "typeIndex": 1 },
    "formSubmitButtonProperties": { "blockName": "formSubmitButtonProperties", "order": 2, "parent": "formProperties" }
  }
}

Key characteristics:

  • Sub-blocks stored as object properties on parent (the standard pattern)
  • Triple numbering system: blockName suffix (-1), typeIndex (per-type counter), displayName (user-facing)
  • getSubBlocks(formBlock) discovers children by filtering for properties with blockName
  • 11 input types: text, email, phone, date, time, number, checkbox, file, select, hidden, textarea
  • Submit button is a special sub-block (formSubmitButtonProperties) with static key
  • renumberFormInputDisplayNames() called after every mutation (add/remove/reorder)
  • Creation flow: createFormInputBlock() -- 471 lines of form utilities in blocks.ts

CE rendering: CreativeFormBlock.vue uses getFormInputBlocks() utility from blockUtils.ts

Slider: Array Overrides + Visual Element Children

Storage: Children stored as properties on sliderProperties, but slides use an override array

json
{
  "sliderProperties": {
    "blockName": "sliderProperties",
    "slides": [
      {
        "graphicProperties-1": { "src": "override-url.jpg" },
        "textProperties-1": { "text": "Slide 1 text" }
      },
      {
        "graphicProperties-1": { "src": "different-url.jpg" },
        "textProperties-1": { "text": "Slide 2 text" }
      }
    ],
    "graphicProperties-1": { "blockName": "graphicProperties-1", "blockType": "graphicProperties", "order": 0, "parent": "sliderProperties" },
    "textProperties-1": { "blockName": "textProperties-1", "blockType": "textProperties", "order": 1, "parent": "sliderProperties" }
  }
}

Key characteristics:

  • Visual element sub-blocks (text, graphic, button, html) stored as object properties -- same as form
  • Slide override pattern: slides[] array contains objects keyed by sub-block blockNames, holding ONLY changed properties
  • CE merges sub-block defaults with per-slide overrides at render time
  • uniquefySlideBlockNames() regenerates ALL sub-block names on duplication
  • No typeIndex -- simple order-based rendering
  • 155 lines of slider template generators in blocks.ts

CE rendering: CreativeSliderBlock.vue uses getSubBlocks() from blockUtils.ts, then merges with slides[currentSlide] overrides

Group: Object Properties + syncGroupChildren

Storage: Children moved from top-level into group object, plus a parent field

json
{
  "groupProperties-1": {
    "blockName": "groupProperties-1",
    "blockType": "blockGroupProperties",
    "textProperties-1": { "blockName": "textProperties-1", "parent": "groupProperties-1", "order": 0 },
    "buttonProperties-1": { "blockName": "buttonProperties-1", "parent": "groupProperties-1", "order": 1 }
  }
}

Key characteristics:

  • Children are MOVED from top-level creativeBlocks into the group (not copied)
  • parent field on children is the source of truth for group membership
  • syncGroupChildren() copies child block refs onto group object so CE can read them via getSubBlocks()
  • getTopLevelBlocks() filters out group children from the top-level block list
  • normalizeOrders() called after every group mutation to reassign 0, 1, 2...
  • Only visual elements can be grouped (text, graphic, button, html)

CE rendering: CreativeGroupBlock.vue uses getSubBlocks() from blockUtils.ts, renders children sorted by order

Side-by-Side Comparison

AspectFormSliderGroup
Sub-block storageProperties on parentProperties on parent + slides arrayProperties on parent (moved from top-level)
DiscoverygetSubBlocks()getSubBlocks()getSubBlocks()
Parent fieldYes (parent: 'formProperties')Yes (parent: 'sliderProperties')Yes (parent: 'groupProperties-1')
OrderInteger, renumberedIntegerInteger, normalized
NamingTriple numbering (blockName, typeIndex, displayName)Standard (blockName, displayName)Standard (blockName, displayName)
CreationcreateFormInputBlock() -- customTemplate generators -- customcreateBlockGroup() -- custom
DuplicationuniquefyFormBlockNames()uniquefySlideBlockNames()duplicateBlock + syncGroupChildren
AF lines in blocks.ts~471 (form utilities)~155 (slider templates)~200 (group mutations)
CE utilitygetFormInputBlocks()getSubBlocks()getSubBlocks()
Can containformInputProperties, formSubmitButtonVisual elements (text, graphic, button, html)Visual elements (text, graphic, button, html)
Multiple parentsNo (one formProperties per creative)No (one sliderProperties per creative)Yes (multiple groups allowed)

Shared Foundation

All three patterns share:

  1. Object property storage -- sub-blocks are properties on the parent object
  2. parent field -- children reference their parent's blockName
  3. getSubBlocks(block) -- the universal discovery function
  4. order field -- determines rendering/display order
  5. blockName as identifier -- unique across the creative

Divergences

Form: Complexity from type awareness

Form is the most complex because it has 11 input types with per-type numbering (typeIndex). When you reorder form inputs, renumberFormInputDisplayNames() recalculates which "Email #1" is which. This creates the blockName vs typeIndex mismatch documented in block-ordering-and-reordering-system.md.

Slider: Complexity from override pattern

Slider is unique in having the slides[] array -- an additional data structure that sits ALONGSIDE the sub-block properties. Sub-blocks define defaults; slides define per-slide overrides. This dual storage means slider sub-blocks serve a different role: they're templates, not instances.

Group: Complexity from block movement

Groups move blocks between containers (top-level to group, group to top-level). This is the only pattern where sub-blocks exist at top-level before becoming children. syncGroupChildren is needed because the source of truth (children with parent field at top-level) must be mirrored as properties on the group for CE to discover them.

Unification Analysis

Could these share a common sub-block system?

Shared interface (possible):

typescript
interface SubBlockContainer {
  blockName: string
  allowedSubBlocks: string[]
  subBlocksReorderable: boolean
  // Children stored as properties with blockName + parent + order
}

All three already satisfy this interface. The differences are in:

  • Creation logic -- form has type selection, slider has template generation, group has block movement
  • Post-mutation hooks -- form needs renumberFormInputDisplayNames, slider needs slide array sync, group needs syncGroupChildren
  • CE rendering -- each has its own component with different layout/style logic

What could be extracted

  1. Generic addSubBlock(parent, childDefaults) mutation -- replaces createFormInputBlock, parts of slider template creation, and moveBlockToGroup
  2. Generic removeSubBlock(parent, childBlockName) mutation -- replaces form/slider/group-specific removal
  3. Generic reorderSubBlocks(parent, blockName, newOrder) mutation -- replaces form/slider/group-specific reordering

Savings estimate: ~150-200 lines of shared mutation code, but each block type would still need ~100-200 lines of type-specific logic (form type numbering, slider overrides, group movement).

Why full unification isn't practical right now

  1. Slider's dual storage (sub-block defaults + slides array overrides) is fundamentally different from form/group
  2. Form's triple numbering is deeply embedded in AF + CE rendering
  3. Group's block movement (between containers) has no analogue in form/slider
  4. Estimated effort: 4-6 weeks for full unification, high regression risk
  5. Value unclear: The divergences reflect genuine differences in how these block types work

Practical extraction targets (low risk)

Instead of unification, extract shared utilities:

  1. Form utilities (471 lines) from blocks.ts to src/store/utils/formBlockUtils.ts

    • createFormInputBlock, calculateFormInputTypeIndex, renumberFormInputDisplayNames, generateFormInputDisplayName, getFormInputType, formInputDefaultsByType, etc.
    • Pure functions, no Vuex dependency
  2. Slider template generators (155 lines) from blocks.ts to src/store/utils/sliderBlockUtils.ts

    • createSliderTemplateBlocks, uniquefySlideBlockNames, generateSliderDefaults, etc.
    • Pure functions, no Vuex dependency
  3. Block ordering utilities to src/store/utils/blockOrderUtils.ts

    • normalizeOrders, getTopLevelBlocks, syncGroupChildren
    • Will be simplified further by fractional indexing

Combined savings: blocks.ts shrinks from ~1384 lines to ~700 lines, with no behavior change.

Recommendations

Short term (with fractional indexing)

  1. Extract form utilities and slider templates to separate files
  2. Implement fractional indexing -- this simplifies all three patterns by removing normalizeOrders
  3. Keep the three sub-block patterns separate; they serve different purposes

Medium term (if group block adoption grows)

  1. Create a shared SubBlockContainerMixin for the three container types
  2. Standardize the CE rendering pattern (all three already use getSubBlocks())
  3. Consider making syncGroupChildren unnecessary by having CE read parent field directly

Long term (if form system is rewritten)

  1. Drop triple numbering -- use blockName as sole identifier, compute typeIndex on the fly
  2. Consider making form inputs visual elements (add to VISUAL_ELEMENTS array)
  3. Unify form and group sub-block creation into a generic addSubBlock mutation

Internal documentation