Skip to content

Live CSS Preview During Scrub — Design Spec

Goal

Provide instant visual feedback in the preview iframe when scrubbing property values, without full engine rebuild. The preview updates reactively during drag; one full rebuild fires on mouseup to sync everything.

Architecture

Leverage the Creative Engine's existing Vue 3 reactivity. During scrub, update DataStore.creativeSettings.creativeBlocks directly in the iframe — Vue's reactive pipeline automatically recomputes the affected block's styles computed property, which triggers a watcher that updates the block's <style> tag. No custom CSS computation needed.

Data Flow

During scrub drag

InputField/OptionRow scrub
  → Section component → Vuex mutation → creativeBlocks changes
    → LocalBuildPreview watcher detects scrubbing-active class
      → sendBlockUpdate(): get changed block from Vuex
        → Dispatch CustomEvent('creative-block-update') to iframe
          → preview-iframe.ts receives event
            → Update DataStore.creativeSettings.creativeBlocks[blockName]
              → Vue 3 deep reactivity:
                  styles computed re-evaluates
                  → watcher fires
                    → object2Css(styles)
                      → styleElement.innerHTML = css
                        → Preview updates visually (no blinking)

On scrub end (mouseup)

MutationObserver detects scrubbing-active class removed
  → rebuildPending = true → localBuildCreative()
    → Full rebuild (unmount + buildCreative + mount) syncs everything

Changes Per File

Creative Engine

src/entry.ts — Export DataStore:

ts
export { DataStore } from './services/dataStore'

No other engine changes needed. All CSS mapping logic stays where it is.

Application-Frontend

src/preview-iframe.ts — New event listener for incremental block updates:

ts
import { DataStore } from '@cavai/creative-engine/src/services/dataStore'
import { set } from 'lodash-es'

document.addEventListener('creative-block-update', (e) => {
  if (!creative) return // Ignore if creative not mounted
  const { blockPath, blockData } = e.detail
  const blocks = DataStore.creativeSettings.creativeBlocks
  if (blockPath.includes('.')) {
    // Sub-block (e.g., formProperties.formInput-1)
    set(blocks, blockPath, blockData)
  } else {
    Object.assign(blocks[blockPath], blockData)
  }
})

src/pages/.../LocalBuildPreview.vue — Send block update during scrub:

ts
// In watcher, when scrubbing-active:
if (document.documentElement.classList.contains('scrubbing-active')) {
  this.rebuildPending = true
  this.sendBlockUpdate(value)
  return
}

// New method:
sendBlockUpdate(blocks) {
  const blockPath = this.$store.state.blocks.selectedBlockPath
  if (!blockPath) return
  const iframe = this.$refs.creativePreview
  if (!iframe?.contentDocument) return
  const block = get(blocks, blockPath)
  if (!block) return
  iframe.contentDocument.dispatchEvent(new CustomEvent('creative-block-update', {
    detail: { blockPath, blockData: JSON.parse(JSON.stringify(block)) }
  }))
}

Why This Works

  • DataStore.creativeSettings is created with Vue 3 reactive() — deep reactivity
  • Block components read from DataStore via provide/inject + computed properties
  • StyleAndClassNameGenerationMixin has a deep watcher on styles that regenerates CSS
  • The engine already knows which CSS property goes on which element (wrap/block/inner-wrap)
  • We just update the data — Vue handles everything else

Edge Cases

  • Sub-blocks (dotted paths like formProperties.formInput-1): Use lodash set() for nested assignment
  • Creative not mounted: Guard with if (!creative) return in event handler
  • Multiple properties changing: Vue batches reactive updates in the same microtask — no extra cost
  • Non-CSS properties (delay, duration, iterations): DataStore update is harmless — no visual CSS change, full rebuild on mouseup syncs animation state

What We Don't Change

  • Engine-internal CSS pipeline (getStyleProps, StyleComposer, object2Css, StyleTreeParser)
  • Existing creative-data-update event (still used for full rebuild)
  • useScrubInput / InputField scrub mechanics
  • MutationObserver rebuild-on-scrub-end logic (already committed)

Key Files

FileRepoChange
src/entry.tsCreative-EngineAdd DataStore export
src/preview-iframe.tsApplication-FrontendAdd creative-block-update event handler
src/pages/.../LocalBuildPreview.vueApplication-FrontendAdd sendBlockUpdate() method, wire into watcher

Internal documentation