Appearance
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 withblockName- 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
creativeBlocksinto the group (not copied) parentfield on children is the source of truth for group membershipsyncGroupChildren()copies child block refs onto group object so CE can read them viagetSubBlocks()getTopLevelBlocks()filters out group children from the top-level block listnormalizeOrders()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
| Aspect | Form | Slider | Group |
|---|---|---|---|
| Sub-block storage | Properties on parent | Properties on parent + slides array | Properties on parent (moved from top-level) |
| Discovery | getSubBlocks() | getSubBlocks() | getSubBlocks() |
| Parent field | Yes (parent: 'formProperties') | Yes (parent: 'sliderProperties') | Yes (parent: 'groupProperties-1') |
| Order | Integer, renumbered | Integer | Integer, normalized |
| Naming | Triple numbering (blockName, typeIndex, displayName) | Standard (blockName, displayName) | Standard (blockName, displayName) |
| Creation | createFormInputBlock() -- custom | Template generators -- custom | createBlockGroup() -- custom |
| Duplication | uniquefyFormBlockNames() | uniquefySlideBlockNames() | duplicateBlock + syncGroupChildren |
| AF lines in blocks.ts | ~471 (form utilities) | ~155 (slider templates) | ~200 (group mutations) |
| CE utility | getFormInputBlocks() | getSubBlocks() | getSubBlocks() |
| Can contain | formInputProperties, formSubmitButton | Visual elements (text, graphic, button, html) | Visual elements (text, graphic, button, html) |
| Multiple parents | No (one formProperties per creative) | No (one sliderProperties per creative) | Yes (multiple groups allowed) |
Shared Foundation
All three patterns share:
- Object property storage -- sub-blocks are properties on the parent object
parentfield -- children reference their parent's blockNamegetSubBlocks(block)-- the universal discovery functionorderfield -- determines rendering/display orderblockNameas 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 needssyncGroupChildren - CE rendering -- each has its own component with different layout/style logic
What could be extracted
- Generic
addSubBlock(parent, childDefaults)mutation -- replacescreateFormInputBlock, parts of slider template creation, andmoveBlockToGroup - Generic
removeSubBlock(parent, childBlockName)mutation -- replaces form/slider/group-specific removal - 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
- Slider's dual storage (sub-block defaults + slides array overrides) is fundamentally different from form/group
- Form's triple numbering is deeply embedded in AF + CE rendering
- Group's block movement (between containers) has no analogue in form/slider
- Estimated effort: 4-6 weeks for full unification, high regression risk
- Value unclear: The divergences reflect genuine differences in how these block types work
Practical extraction targets (low risk)
Instead of unification, extract shared utilities:
Form utilities (471 lines) from blocks.ts to
src/store/utils/formBlockUtils.tscreateFormInputBlock,calculateFormInputTypeIndex,renumberFormInputDisplayNames,generateFormInputDisplayName,getFormInputType,formInputDefaultsByType, etc.- Pure functions, no Vuex dependency
Slider template generators (155 lines) from blocks.ts to
src/store/utils/sliderBlockUtils.tscreateSliderTemplateBlocks,uniquefySlideBlockNames,generateSliderDefaults, etc.- Pure functions, no Vuex dependency
Block ordering utilities to
src/store/utils/blockOrderUtils.tsnormalizeOrders,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)
- Extract form utilities and slider templates to separate files
- Implement fractional indexing -- this simplifies all three patterns by removing normalizeOrders
- Keep the three sub-block patterns separate; they serve different purposes
Medium term (if group block adoption grows)
- Create a shared
SubBlockContainerMixinfor the three container types - Standardize the CE rendering pattern (all three already use
getSubBlocks()) - Consider making
syncGroupChildrenunnecessary by having CE readparentfield directly
Long term (if form system is rewritten)
- Drop triple numbering -- use blockName as sole identifier, compute typeIndex on the fly
- Consider making form inputs visual elements (add to
VISUAL_ELEMENTSarray) - Unify form and group sub-block creation into a generic
addSubBlockmutation