@composi/core

CDN

Run in Browser without Build

It is possible to run @composi/core directly in the browser without a build step. This is a great way to quickly prototype an idea, or try out @composi/core with something you already have build. The advantage of the build step is it allows you to use JSX to define the markup for your components. Without a build step you have two choices: use the h function to define markup, or use the htm module to define markup. htm is a tiny module that allows you to use a JSX-like syntax in the browser by means of a tagged template. This is not the same as using ordinary template literals, as the result gets converted into virtual nodes that @composi/core can render to the DOM.

This technique takes advantage of the fact that modern browsers can import ES6 modules directly. You do need to let the browser know that the code you are importing is an ES6 module. You do this on the script tag by providing the attribute type='module'.

Setup for in Browser Use

To use @composi/core in the browser, create a folder and name it whatever you want. In it create an index.html file, and a JavaScript file. In this case we're going to name it app.js. You can also create a CSS file if you want. This means our project structure should look like this:

MyProject-|
    |-app.js
    |-index.html
    |-style.css

In app.js we are going to import three files: @composi/core, @composi/merge-objects, and htm. Notice how each import ends with ?module. This is to let the browser know that these are ES6 modules:

import { h, render, run, union } from 'https://unpkg.com/@composi/core@2.6.1/dist/composi-core.mjs?module'
import { clone } from 'https://unpkg.com/@composi/merge-objects@1.1.0/src/index.js?module'
import { htm } from 'https://unpkg.com/htm.mjs?module'

// Pass the @composi/core h function to htm,
// and capture the result in variable 'html'.
// We'll use this to define markup.
const html = htm.bind(h)

Next create basic html to import this code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header></header>
  <section></section>
  <script type='module' src="/app.js"></script>
</body>
</html>

Notice that the script tag has the type module. This is very important so that the browser knows it is important and ES6 module. For the CSS you can provide whatever you need to. We used absoltute paths here because this will need to be run with a server.

CSS

For this example, we're going to use the following CSS:

:root {
  --masthead-background-color:#f10;
  --masthead__header-color:#fff
}
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}
header {
  background-color:var(--masthead-background-color);
  padding:20px;
}
header h1 {
  color:var(--masthead__header-color);
  margin:0;font-size:2rem;
}

section {
  padding: 2rem;
}
.container {
  width: 350px;
}
p {
  display: flex;
  justify-content: space-between;
}
input {
  flex: 1;
  margin-right: 10px;
}
.list {
  list-style: none;
  border: solid 1px #ccc;
  margin: 0;
  padding: 0;
}
.list li {
  padding: 5px 10px;
  border-bottom: solid 1px #ccc;
  display: flex;
  flex-direction: row;
}
.list li:last-of-type {
  border: none;
}
.list li > span {
  flex: 1;
}
.delete-item {
  border: solid 1px red;
  background-color: white;
  color: red;
  transition: all .25s ease-out;
  cursor: pointer;
}
.delete-item:hover {
  color: white;
  background-color: red;
}
.add-item {
  border: solid 1px green;
  background-color: green;
  color: white;
  transition: all .25s ease-out;
  cursor: pointer;
}
.add-item:hover {
  color: green;
  background-color: white;
}

App.js

And now, for the app.js file we're going to use the following:

import { h, render, run, union } from 'https://unpkg.com/@composi/core@2.6.1/dist/composi-core.mjs?module'
import { clone } from 'https://unpkg.com/@composi/merge-objects@1.1.0/src/index.js?module'
import { htm } from 'https://unpkg.com/htm.mjs?module'

// Pass the @composi/core h function to htm,
// and capture the result in variable 'html'.
// We'll use this to define markup.
const html = htm.bind(h)

function Title({greeting}) {
  return html`
    <nav>
      <h1>Hello, ${greeting}!</h1>
    </nav>
  `
}
render(html`<${Title} greeting='Everyone'/>`, 'header')


const ref = {
  target: undefined
}

const Msg = union(['AddItem', 'DeleteItem'])
function List({state, send}) {
  function setTarget(input) {
    ref.target = input
  }
  let inputValue
  function getInputValue(value) {
    inputValue = value
  }
  return html`
    <div class='container'>
      <p>
        <input autofocus onmount=${input => setTarget(input)} value=${state.resetInput} type='text' onchange=${e => getInputValue(e.target.value)}>
        <button onclick=${() => send(Msg.AddItem(inputValue))} class='add-item'>Add</button>
      </p>
      <ul class='list'>
        ${
          state.fruits.map(fruit => html`<li key=${fruit.key}>
            <span>${fruit.value}</span>
            <button class='delete-item' onclick=${() => send(Msg.DeleteItem(fruit.key))}>X</button>
          </li>`)
        }
      </ul>
    </div>
  `
}

const state = {
  newKey: 104,
  resetInput: '',
  fruits: [
    {
      key: 101,
      value: 'Apples'
    },
    {
      key: 102,
      value: 'Oranges'
    },
    {
      key: 103,
      value: 'Bananas'
    }
  ]
}
function actions(msg, state) {
  const prevState = clone(state)
  return Msg.match(msg, {
    'AddItem': value => {
      if (!value) return [prevState]
      const key = prevState.newKey++
      prevState.fruits.push({key, value})
      ref.target.focus()
      return [prevState]
    },
    'DeleteItem': key => {
      const filteredFruits = prevState.fruits.filter(fruit => fruit.key != key)
      prevState.fruits = filteredFruits
      return [prevState]
    }
  })
}
const program = {
  init() {
    return [state]
  },
  view(state, send) {
    render(html`<${List} ...${{state, send}} />`, 'section')
  },
  update(msg, state) {
    return actions(msg, state)
  }
}

run(program)

Now all we need to do is run this. To do so we need a server. There are a number of ways to do this. I use http-server. You can install it from NPM:

npm i -g http-server

Run it in the project folder. When it starts it will indicate the url you can use to load in the browser. Copy and paste into the browser and voilá, you should have a fully operation @composi/core app running in the browser without a build process.

HTM

In the previous example we were using htm to define our component markup. This is a tiny module that lets you use tagged templates with a syntax similar to JSX. There are differences because it is ultimately a template literal. Here we will go over how to use HTM.

html``

You use html`` to define all your markup.

function Title() {
  return html`
    <nav>
      <h1>Hello, World!</h1>
    </nav>
  `
}

{} = ${}

Whereas JSX uses curly braces to mark off a variable to block of code, HTM used ${}. Using the previous example, we'll pass in value for the Hello World. Instead of the normal JSX curly braces, we precede them with the $ sign:

function Title({greeting}) {
  return html`
    <nav>
      <h1>Hello, ${greeting}!</h1>
    </nav>
  `
}

To do a JavaScript code block inside of the markup, we again use curly braces with a $ sign:

function List({state}) {
  return (
    html`<ul class='list'>
      ${
        state.fruits.map(fruit => html`<li key=${fruit.key}>${fruit.value}</li>`)
      }
    </ul>`
  )
}

<List/> = <${List}/>

To use a functional component as a tag, you need to enclose the function name inside curly braces with the $ sign:

render(<${List}/>, document.body)

{...{state}} = ...${{state}}

To descructure props in a tag, you start with the spread operator, followed by the values to use in curly braces with the $ sign:

render(<${List} ...${{state, send}}/>, document.body)

Notice the difference between this and JSX:

render(<List {...{state, send}}/>, document.body)

Summary

Using @composi/core along with htm in the browser provides and easy way to test out ideas and do quick prototyping. The small size of both @composi/core and htm make this approach very performant.