{ } Lips v0.2.0

Context & macros

Context

Context is shared state on the Lips instance. Any component can read it; components that declare an interest get a hook when it changes.

const lips = new Lips({
  context: {
    theme: 'light',
    user: { id: 1, name: 'Guest' },
    permissions: [ 'read' ]
  }
})

Reading

Reading context.x in a template subscribes that binding to x — no declaration needed:

<div class="app theme-{context.theme}">
  <p>Welcome, {context.user.name}</p>
</div>

Reacting

Declaring context: [ … ] controls only when onContext fires:

const panel = {
  context: [ 'theme', 'user' ],

  handler: {
    onContext(){
      // runs when `theme` or `user` changes — not for other fields
      this.applyTheme( this.context.theme )
    }
  }
}

Writing

// from outside
lips.setContext('theme', 'dark')
lips.setContext({ user: { id: 2, name: 'Ada' }, permissions: [ 'read', 'write' ] })

// from inside a component
handler: {
  toggle(){ this.setContext('theme', this.context.theme === 'dark' ? 'light' : 'dark') }
}

Subscribing from code

// fires when any listed field changes
const stop = lips.watchContext([ 'theme' ], () => {
  document.body.dataset.theme = lips.getContext().theme
})

// same, but the callback receives the current values
lips.useContext([ 'theme', 'user' ], ctx => {
  console.log( ctx.theme, ctx.user )
})

stop()   // unsubscribe

Context is for genuinely cross-cutting values — theme, session, locale. Passing data down as inputs stays cheaper to reason about, and is just as fast.

Macros

A macro is a template fragment that is inlined at compile time. There is no component instance, no input wiring, and no lifecycle — the macro body becomes part of the calling template.

const toolbar = {
  macros: `
    <macro [icon, label, active] name="tool">
      <li class=(active ? 'tool on' : 'tool')>
        <i class="icon-{icon}"></i>
        <if( label )><span class="label">{label}</span></if>
      </li>
    </macro>

    <macro [label, value, required] name="field">
      <div class="field">
        <label>{label}{required ? ' *' : ''}</label>
        <input value=value required=required/>
      </div>
    </macro>`,

  default: `
    <ul class="toolbar">
      <tool icon="pen" label="Pen" active/>
      <tool icon="hand"/>
    </ul>

    <field label="Name" value=state.name required/>
    <field label="Phone" value=state.phone/>`
}

Arguments

Names declared in brackets become variables in the macro body. Anything not declared is still passed — reach it through arguments:

<macro [type, key] name="option">
  <li data-type=type data-key=key on-click( pick, type, key, arguments )>
    {arguments.label}
  </li>
</macro>

Undeclared arguments are undefined: falsy in a condition, attribute-removing, and rendered as an empty string.

Spread arguments

Call-site attributes are applied in source order, so spreads and explicit values override each other left to right, exactly like a JS object literal:

<!-- explicit `icon` wins -->
<tool ...state.defaults icon="pen"/>

<!-- the spread wins -->
<tool icon="pen" ...state.defaults/>

This makes the common list pattern read naturally:

<for [key, each] in=state.tools>
  <tool key=key ...each/>
</for>

Keys that disappear from the spread object are removed on update.

Macros vs components

  Macro Component
Cost inlined, zero runtime an instance with its own state
State none — it uses the caller’s scope its own state
Lifecycle none full lifecycle
Recursion not allowed (LIPS-C009) allowed
Scoped styles no yes

Reach for a macro when you are repeating markup. Reach for a component when the thing has behaviour or state of its own.

Reactivity primitives

The signal core is exported, so you can use it outside a template — for stores, derived values, or glue with non-Lips code.

import { signal, effect, untrack, reactive } from '@lipsjs/lips'

signal( value )

Returns [ read, write, touch ].

const [ count, setCount, touchCount ] = signal( 0 )

count()          // → 0
setCount( 5 )    // notifies subscribers
touchCount()     // notify without changing the value

effect( fn )

Runs fn immediately, tracks every signal it reads, and re-runs when any of them changes.

const stop = effect( () => {
  console.log('count is', count() )
})

stop.dispose()

Dependencies are re-tracked on every run, so a branch that stops reading a signal stops subscribing to it.

reactive( object, deep? )

Wraps a plain object in a per-key signal store.

const store = reactive({ user: null, items: [] })

effect( () => console.log( store.items.length ) )

store.items = [ 1, 2, 3 ]   // logs 3

Pass deep: true to track nested mutation — including Map and Set contents:

const store = reactive({ layers: new Map() }, true )

effect( () => console.log( store.layers.size ) )

store.layers.set('a', {})   // logs 1

untrack( fn )

Reads without subscribing.

effect( () => {
  const shown = visible()                 // tracked
  const all = untrack( () => items() )    // not tracked
})

Component state, input and context are all built on these primitives — nothing in a template has access to machinery your own code cannot use.