{ } Lips v0.2.0

Hot-swap

swap() replaces the template of a live component and patches only what actually differs. State survives, DOM nodes survive where they can, and nested components are carried across rather than remounted.

It exists for tools that revise a running UI: visual editors, live playgrounds, generated components, HMR.

import Lips, { compileTemplate } from '@lipsjs/lips'

const app = lips.render('editor', template).appendTo('#app')

const { ir } = compileTemplate( revisedSource )
const report = app.swap( ir )

What it does

swap() diffs the new IR against the live one by expression source, not by identity, so two independent compilations of related templates compare correctly.

Difference What happens
Nothing changed subtree kept wholesale, zero DOM work
A binding changed effects rewired onto the same nodes
A child block changed that block re-executed at its existing anchor
Static markup changed that block rebuilt — with its components salvaged

State lives outside the IR, so it survives by construction.

The report

const report = app.swap( ir )

report.changes
// [ { kind: 'binds', path: 'root' },
//   { kind: 'block', path: 'root/2' } ]

report.salvaged
// [ 'layer-panel', 'toolbar' ]

changes lists every region touched — kind is skeleton, binds or block, and path locates it in the block tree. An editor can use it to highlight exactly what a revision affected.

salvaged names the component instances carried through instead of remounted.

Instance salvage

When a revision changes a block’s static markup, that block has to be rebuilt. Rather than destroy everything inside it, Lips salvages the component instances: each is released from the region, offered to the fresh render, and re-homed there.

A salvaged component keeps its state, its DOM nodes, its handlers and its event listeners. Only its parent-side wiring — input expressions and event instructions — is rebuilt against the new call site. No lifecycle hook fires: from the component’s point of view, nothing happened.

// v1
`<div class="card"><stepper/></div>`

// v2 — the wrapper changed, the stepper did not
`<section class="card wide"><stepper/></section>`

The stepper keeps its count. Components the revision dropped are destroyed normally, with onDestroy and component:destroy.

The same applies when only a component’s own call site changes — <panel list=state.a/> becoming <panel list=state.b/> re-wires the instance instead of replacing it. Inputs the revision removed are cleared from it.

Identity

A key input decides which live instance a call site claims:

<!-- follows the key, wherever the revision moves it -->
<layer key=layer.id name=layer.name/>

<!-- follows position among same-name siblings -->
<layer name=layer.name/>

Without a key, components match by position among same-name components in render order — the rule keyless JSX lists already follow. Give regenerated components a stable key if a revision may reorder or relocate them; that is what lets an instance survive being moved somewhere else in the tree.

What still resets

Salvage preserves state and node identity, not DOM attachment. Nodes are detached and re-inserted, so anything that reacts to being re-parented still does — an <iframe> reloads, CSS transitions restart. Keyboard focus is restored automatically after a swap.

<if> and <for> bodies are not salvaged as blocks; only the components inside them are.

Working with generated templates

Because compilation is a plain function and diagnostics never throw, a bad revision is reportable rather than fatal:

const { ir, diagnostics } = compileTemplate( generatedSource )

const errors = diagnostics.filter( d => d.severity === 'error' )
if( errors.length ){
  // hand back line/col/code/hint — nothing was applied
  return showErrors( errors )
}

const { changes, salvaged } = app.swap( ir )
highlight( changes )

For untrusted generated markup, render the preview with mode: 'interpreted' so nothing is ever passed to Function. See Precompile & CSP.

Example: a live editor loop

import Lips, { compileTemplate } from '@lipsjs/lips'

const lips = new Lips()
const preview = lips.render('preview', { state: { count: 0 }, default: source })
preview.appendTo('#preview')

editor.on('change', next => {
  const { ir, diagnostics } = compileTemplate( next )

  if( diagnostics.some( d => d.severity === 'error' ) ){
    markProblems( diagnostics )
    return
  }

  const { changes, salvaged } = preview.swap( ir )

  flashRegions( changes )
  console.log(`kept ${salvaged.length} component instances`)
})

The preview keeps its state across every keystroke that compiles.