{ } Lips v0.2.0

Built-in components

These tags are part of the template language. Except for <router>, they are compiled — they create no component instance and cost nothing at runtime beyond the work they describe.

<if> · <else-if> · <else>

<if( state.loggedIn )>
  <dashboard/>
</if>
<else>
  <login-form/>
</else>

A full chain:

<if( state.status === 'loading' )>
  <spinner/>
</if>
<else-if( state.status === 'error' )>
  <error-message error=state.error/>
</else-if>
<else-if( !state.rows.length )>
  <empty-state message="Nothing here yet"/>
</else-if>
<else>
  <data-table rows=state.rows/>
</else>

Exactly one branch is live at a time. Switching branches destroys the previous one — its effects are disposed, its nested components run onDestroy.

<for>

<!-- array -->
<for [item] in=state.items>
  <li>{item.name}</li>
</for>

<!-- array with index -->
<for [item, i] in=state.items>
  <li>#{i}: {item.name}</li>
</for>

<!-- object → key, value, index -->
<for [key, value, i] in=state.settings>
  <dt>{key}</dt><dd>{value}</dd>
</for>

<!-- Map → key, value, index -->
<for [id, layer] in=state.layers>
  <li>{layer.name}</li>
</for>

<!-- Set → value, index -->
<for [tag] in=state.tags>
  <span class="tag">{tag}</span>
</for>

<!-- numeric range, both ends inclusive; counts down if to < from -->
<for [n] from=1 to=5>
  <span>{n}</span>
</for>
Source Bracket arguments
Array [ item, index ]
Map [ key, value, index ]
Set [ value, index ]
Object [ key, value, index ]
from/to [ value ]

Keying with by

<for [todo] in=state.todos by="id">
  <todo-row todo=todo/>
</for>

<for [row] in=state.rows by=self.rowKey>
  <tr><td>{row.label}</td></tr>
</for>

by takes either a property path as a literal string, or an expression resolving to a function called with ( item, index ).

Without by, rows are identified by position: reordering rewrites their content. With by, Lips moves the existing DOM range instead — node identity and nested component state survive the reorder. Keys are naturally derived for Map and object sources.

<switch> · <case> · <default>

<switch( state.role )>
  <case is="admin"><admin-panel/></case>
  <case is="editor"><editor-panel/></case>
  <case is=[ 'viewer', 'guest' ]><read-only/></case>
  <default><login-form/></default>
</switch>

is accepts a single value or an array of values. Without a matching case, <default> renders — or nothing, if there is none.

<async> · <loading> · <then> · <catch>

<async await( self.fetchUser( state.userId ) )>
  <loading>
    <spinner/>
  </loading>
  <then [user]>
    <user-card user=user/>
  </then>
  <catch [error]>
    <p class="error">{error.message}</p>
  </catch>
</async>
  1. <loading> renders immediately, if present
  2. the awaited expression is evaluated
  3. on resolve, <then> replaces it, receiving the value
  4. on reject, <catch> replaces it, receiving the error

If the awaited expression re-evaluates before the previous promise settles, the stale result is discarded — no flicker, no out-of-order render.

<let> · <const>

<let doubled={ state.n * 2 }/>
<p>{doubled}</p>

<let subtotal={ state.price * state.qty }
     tax={ subtotal * 0.07 }
     total={ subtotal + tax }/>

<const TAX_RATE=0.07 CURRENCY="EUR"/>

Declared names are block-scoped and visible to every binding in that block — including bindings written above the declaration. They recompute reactively when their dependencies change.

<let ...state.options/> is not supported and reports LIPS-C013. A compiled expression has to know its scope names at compile time, and the keys of a spread are only known at runtime. Bind the object to a single name instead:

<let opts={ state.options }/>
<p>{opts.title}</p>

<log>

<log( state.user )/>
<log('rows:', state.rows.length, state.filter )/>

Calls console.log during render, renders nothing. Useful for inspecting scope inside a <for> or a macro body.

<router>

The only built-in that is a real component. It resolves the current URL against a route table and renders the matching page.

<router global routes=static.routes
        on-before(beforeNavigate)
        on-after(afterNavigate)
        on-not-found(notFound)/>
{
  _static: {
    routes: [
      { path: '/',                    template: Home, default: true },
      { path: '/about',               template: About },
      { path: '/users/:id',           template: UserDetail },
      { path: '/blog/:category/:slug', template: BlogPost }
    ]
  }
}

Each template is an ordinary component template object.

Input Meaning
routes array of { path, template, default? }
global sync with the browser URL and history

A page receives params and query as inputs:

// UserDetail
default: `<h1>User {input.params.id}</h1>`

Path segments beginning with : become named params and are percent-decoded. The query string is parsed with URLSearchParams.

Events

Event Payload
on-before { fromState, toState } — before navigating away
on-after { fromState, toState } — after the page resolves
on-not-found the unmatched path

With global, the router pushes history entries and listens for popstate. It also publishes a navigate function into context, so any component can route:

handler: {
  goToProfile(){ this.context.navigate('/users/42') }
}

<router> needs the template compiler, so it is only available from the full @lipsjs/lips entry — not from @lipsjs/lips/runtime. That build tree-shakes it out entirely.