Skip to content

echox-js/echox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

38 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

EchoX

The fast, 3KB JavaScript framework for "echoing" reactive UI with tagged templates, inspired by Hypertext Literal.

  • Fast - no virtual DOM, no extensive diffing, pure fine-grained reactivity
  • Small - no transpiling or compiling, zero dependencies, 3KB (gzip)
  • Simple - as simple as innerHTML

Note

The current release is merely a proof of concept and is not ready for production. The next branch is implementing the new proposal API for production use. Feel free to join the discussion and contribute!

Getting Started

EchoX is typically installed via a package manager such as Yarn or NPM.

$ npm install echox

EchoX can then imported as a namespace:

import * as X from "echox";

const node = X.html`<define count=${X.state(0)}>
  <button @click=${(d) => d.count++}>πŸ‘</button>
  <button @click=${(d) => d.count--}>πŸ‘Ž</button>
  <span>${(d) => d.count}</span>
</define>`;

document.body.append(node);

EchoX is also available as a UMD bundle for legacy browsers.

<script src="https://cdn.jsdelivr.net/npm/echox"></script>
<script>
  const node = X.html`...`;

  document.body.append(node);
</script>

Reading the core concepts to learn more:

Template Interpolations

EchoX uses tagged template literal to declare UI, which renders the specified markup as an element.

import * as X from "echox";

const node = X.html`<h1>hello world</h1>`;

document.body.append(node);

A string, boolean, null, undefined can be interpolated to an attribute:

// Interpolate number attribute.
X.html`<h1 id=${"id" + Math.random()}></h1>`;

// Interpolate boolean attribute.
X.html`<input checked=${true}></input>`;

If the interpolated data value is a node, it is inserted into the result at the corresponding location.

X.html`<h1>${document.createText('hello world')}</h1>

It is also possible to interpolate iterables of nodes into element.

X.html`<ul>${["A", "B", "C"].map((d) => X.html`<li>${d}</li>`)}</ul>`;

# X.html(markup, ...interpolations)

If only one arguments is specified, render and return the specified component. Otherwise tenders the specified markups and interpolations as an element.

State Bindings

For stateful UI, a wrapped define tag is required for defining some properties related to reactivity. Each state-derived property or child node should be specified as a callback, which is invoked on an object containing all the properties defined on the define tag.

// state-derived property
X.html`<define count=${X.state(0)}>
  <span>${(d) => d.count}</span>
  <button @click=${(d) => d.count++}></button>
</define>`;

// state-derived child node
X.html`<define color=${X.state(0)}>
  <span style=${(d) => `background: ${d.color}`}>hello</span>
  <input value=${(d) => d.color} @input=${(d, e) => (d.color = e.target.value)} />
</define>`;

If a state is non-primitive, it should be specified as a callback returning the state.

// array state
X.html`<define letters=${X.state(() => ["A", "B", "C"])}></define>`;

// object state
X.html`<define info=${X.state(() => ({name: "jim", age: 22}))}></define>`;

A computed state also can be specified as a callback, calling on all reactive properties and return a new state.

X.html`<define message=${X.state("hello")} reversed=${X.state((d) => d.message.split("").reverse().join(""))}>
  <input value=${(d) => d.message} @input=${(d, e) => (d.message = e.target.value)} />
  <p>Reversed: ${(d) => d.reversed}</p>
</define>`;

Please notes that the name of a reactive property must be kebab case in defined tag, but convert to camel case when accessing.

X.html`<define must-kebab-case=${X.state("hi")}>${(d) => d.museKebabCase}</define>`;

# X.state(value)

Returns a state.

Class and Style Bindings

Class and style are just like other properties:

X.html`<define random=${Math.random()} >
  <span 
    class=${(d) => (d.random > 0.5 ? "red" : null)}
    style=${(d) => (d.random > 0.5 ? `background: ${d.color}` : null)}
  >
    hello
  </span>
</define>`;

But X.cx and X.css make it easier to style conditionally . With them, now say:

X.html`<define random=${Math.random()} >
  <span
    class=${(d) => X.cx({red: d.random > 0.5})}
    style=${(d) => X.css(d.random > 0.5 && {background: d.color})}
  >
    hello
  </span>
</define>`;

Multiple class objects and style objects can be specified and only truthy strings will be applied:

// class: 'a b d'
// style: background: blue
X.html`<define>
  <span class=${X.cx(null, "a", undefined, new Date(), {b: true}, {c: false, d: true, e: new Date()})}> Hello </span>
  <span style=${X.css({background: "red"}, {background: "blue"}, false && {background: "yellow"})}> World </span>
</define>`;

# X.cx(...classObjects)

Returns a string joined by all the attribute names defined in the specified classObjects with truthy string values.

# X.css(...styleObjects)

Returns a string joined by all the attributes names defined in the merged specified styleObjects with truthy string values.

Event Handling

Using @ directive to bind a event with the specified event handler, which is calling on all reactive properties and native event object.

X.html`<define count=${X.state(0)}>
  <button @click=${(d) => d.count++}>πŸ‘</button>
  <button @click=${(d) => d.count--}>πŸ‘Ž</button>
  <span>${(d) => d.count}</span>
</define>`;

// Event is the second parameter.
X.html`<define color=${X.state(0)}>
  <span style=${(d) => `background: ${d.color}`}>hello</span>
  <input value=${(d) => d.color} @input=${(d, e) => (d.color = e.target.value)} />
</define>`;

List Rendering

Memorized list rendering is achieved by for tag. The each property is required in for tag to specify the iterable state. Some rest item parameters are called on the stateful binds in for tag, accessing item and index by item.$value and item.$index respectively.

X.html`<define dark=${state(false)} blocks=${state([1, 2, 3])}>
  <ul>
    <for each=${(d) => d.blocks}>
      <li>${(d, item) => item.$index}-${item.$value}</li>
    </for>
  </ul>
</define>`;

Conditional Rendering

Memorized conditional rendering is achieved by if, elif and else tags. The expr property is required in if and elif tags, displaying the child nodes with the specified callback evaluating to true.

X.html`<define count=${state(0)} random=${state(Math.random())}>
  <if expr=${(d) => d.random < 0.3}>
    <span>A</span>
  </if>
  <elif expr="${(d) => d.random < 0.6}}">
    <span>B</span>
  </elif>
  <else>
    <span>C</span>
  </else>
</define>`;

Effect

Effects can be defined using X.effect, which is be called before DOM elements are mounted and after dependent states are updated. An optional callback can be returned to dispose of allocated resources.

const f = (d) => ("0" + d).slice(-2);

X.html`<define
  date=${X.state(new Date())}
  ${X.effect(() => console.log(`I'm a new time component.`))}
  ${X.effect((d) => {
    const timer = setInterval(() => (d.date = new Date()), 1000);
    return () => clearInterval(timer);
  })}
  >
  <span>${({date}) => `${f(date.getHours())}:${f(date.getMinutes())}:${f(date.getSeconds())}`}</span>
</define>`;

# X.effect(effect)

Returns a effect.

Ref Bindings

Accessing a DOM element in effect.

X.html`<define
  div=${X.ref()}
  ${X.effect((d) => d.div && (d.div.textContent = "hello"))}
>
  <div ref="div"></div>
</define>`;

# X.ref()

Returns a ref.

Component

A component can be defined a component using X.component and registered it in define tag. Please notes that the name of component should always be kebab case.

const ColorLabel = X.component`<define color=${X.prop("steelblue")} text=${X.prop()}>
  <span>${(d) => d.text}</span>
</define>`;

X.html`<define color-label=${ColorLabel}>
  <color-label color="red" text="hello world"></color-label>
</define>`;

A component can be rendered directly by X.html.

const App = component`<define count=${state(0)}></define>`;

html(App);

# X.component(markup, ...interpolations)

Returns a component.

# X.prop([defaultValue])

Returns a prop.

Composable

Some reusable logic can be defined using X.composable and accessed through the specified namespace.

const useMouse = X.composable`<define 
  x=${X.state(0)} 
  y=${X.state(0)}
  log=${X.method((d) => console.log(d.x, d.y))} 
  ${X.effect((d) => {
    const update = ({clientY, clientX}) => ((d.x = clientX), (d.y = clientY));
    window.addEventListener("mousemove", update);
    return () => (window.removeEventListener("mousemove", update), console.log("remove"));
  })}>
</define>`;

X.html`<define mouse=${useMouse}>
  <button @click=${(d) => d.mouse.log()}>Log</button>
  <span>${(d) => `(${d.mouse.x}, ${d.mouse.y})`}</span>
</define>`;

# X.composable(strings, ...interpolations)

Returns a reusable composable.

Store

A global and single-instance store can be defined using X.store and accessed through the specified namespace.

// store.js
export createStore = store`<define
  value=${state(0)}
  increment=${method((d) => d.value++)}
  decrement=${method((d) => d.value--)}>
</define>`;
// counter.js
import {createStore} from "./store.js";

X.component`<define counter=${createStore()}>
  <button @click=${(d) => d.counter.count++}>πŸ‘</button>
  <button @click=${(d) => d.counter.count--}>πŸ‘Ž</button>
  <span>${(d) => d.counter.count}</span>
</define>`;

# X.store(strings, ...interpolations)

Returns a global and single-instance store.

About

The fast, 3KB JavaScript framework for "echoing" reactive UI.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published