{ } Lips v0.2.0

Getting started

Installation

Lips ships as an ES module. There is no UMD or global-script build — use <script type="module"> or a bundler.

From a CDN

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Lips App</title>
</head>
<body>
  <div id="app"></div>

  <script type="module">
    import Lips from 'https://cdn.jsdelivr.net/npm/@lipsjs/lips/+esm'
    // your code here
  </script>
</body>
</html>

From npm

npm install @lipsjs/lips
import Lips from '@lipsjs/lips'

Entry points

Import Contents gzip
@lipsjs/lips full — runtime, parser/compiler, styles, router ~21 KB
@lipsjs/lips/runtime precompiled-only — no parser/compiler ~13 KB
@lipsjs/lips/precompile build-time helpers and the Vite/Rollup plugin
@lipsjs/lips/dev unminified full build, for debugging

The runtime entry has no template compiler, so every template must arrive precompiled. It cannot parse a default string.

Your first component

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Lips Counter</title>
</head>
<body>
  <div id="app"></div>

  <script type="module">
    import Lips from 'https://cdn.jsdelivr.net/npm/@lipsjs/lips/+esm'

    const lips = new Lips()

    const counter = {
      state: {
        count: 0
      },

      handler: {
        increment(){ this.state.count++ },
        decrement(){ this.state.count > 0 && this.state.count-- }
      },

      default: `
        <div class="counter">
          <h2>Counter: {state.count}</h2>
          <button class="up" on-click(increment)>+</button>
          <button class="down" on-click(decrement)>-</button>
        </div>`,

      stylesheet: `
        .counter {
          font-family: sans-serif;
          text-align: center;
          padding: 1rem;
          border: 1px solid #ccc;
          border-radius: 4px;
          max-width: 300px;
        }
        button {
          padding: 0.5rem 1rem;
          margin: 0 0.5rem;
          cursor: pointer;
        }`
    }

    lips.render('counter', counter).appendTo('#app')
  </script>
</body>
</html>

That covers most of the framework’s surface already:

render vs root

// Render a named component and place it yourself
const c = lips.render('counter', counter)
c.appendTo('#app')

// Or mount an application root in one call
lips.root(appTemplate, '#app')

render( name, template, input? ) returns a component handle. root( template, selector ) renders and appends in one step, and keeps a reference so lips.dispose() can tear the whole tree down.

The name you pass to render() is also the stylesheet scope. Two components rendered under the same name share one injected stylesheet.

Registering components

A component must be registered before a template can refer to it by tag name.

lips.register('user-card', userCard)
lips.register('todo-list', todoList)

lips.root({
  default: `
    <div class="app">
      <user-card name="Ada"/>
      <todo-list/>
    </div>`
}, '#app')

Registration is looked up lazily at render time, so order does not matter — you can register a child after the parent that uses it.

lips.has('user-card')       // → true
lips.unregister('user-card')

Project structure

For anything beyond a page, split components into modules. Lips has no opinion about your file layout; a plain object export is all it needs.

my-lips-app/
├── index.html
├── app.js
└── components/
    ├── header.js
    ├── footer.js
    └── todo-list.js
// app.js
import Lips from '@lipsjs/lips'

import header from './components/header.js'
import footer from './components/footer.js'
import todoList from './components/todo-list.js'

const lips = new Lips({
  context: { theme: 'light', user: null }
})

lips.register('app-header', header)
lips.register('app-footer', footer)
lips.register('todo-list', todoList)

lips.root({
  default: `
    <div class="app">
      <app-header/>
      <main><todo-list/></main>
      <app-footer/>
    </div>`,
  stylesheet: `
    .app { display: flex; flex-direction: column; min-height: 100vh }
    main { flex: 1; padding: 1rem }`
}, '#app')
// components/todo-list.js
export default {
  state: {
    todos: [
      { id: 1, text: 'Learn Lips', done: false },
      { id: 2, text: 'Build an app', done: false }
    ],
    draft: ''
  },

  handler: {
    add(){
      const text = this.state.draft.trim()
      if( !text ) return

      this.state.todos.push({ id: Date.now(), text, done: false })
      this.state.draft = ''
    },
    toggle( id ){
      const todo = this.state.todos.find( t => t.id === id )
      todo && ( todo.done = !todo.done )
    }
  },

  default: `
    <div class="todo-list">
      <h2>Todo</h2>

      <div class="add">
        <input type="text"
               placeholder="Add a todo"
               value=state.draft
               on-input( e => state.draft = e.target.value )/>
        <button on-click(add)>Add</button>
      </div>

      <ul>
        <for [todo] in=state.todos by="id">
          <li class=(todo.done ? 'item done' : 'item')>
            <input type="checkbox" checked=todo.done on-change(toggle, todo.id)/>
            <span>{todo.text}</span>
          </li>
        </for>
      </ul>
    </div>`,

  stylesheet: `
    .todo-list { max-width: 500px; margin: 0 auto }
    .add { display: flex; gap: .5rem; margin-bottom: 1rem }
    .add input { flex: 1; padding: .5rem }
    .item { display: flex; align-items: center; gap: .5rem; padding: .5rem 0 }
    .item.done span { text-decoration: line-through; opacity: .6 }`
}

Note by="id" on the <for>. It keys the list so DOM nodes and nested component state survive a reorder. See keyed lists.

Single-file components

With a bundler you can write .lips files instead — script on top, template below — and have them compiled at build time:

const state = { count: 0 }

const handler = {
  increment(){ this.state.count++ }
}

<div class="counter">
  <h2>{state.count}</h2>
  <button on-click(increment)>+</button>
</div>

See Precompile & CSP for the plugin setup.

Next