Skip to content

500 bytes web app micro-framework with a syntax you already know

Notifications You must be signed in to change notification settings

zheksoon/htm-ally

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

htm-ally brotli size

500 bytes web app micro-framework with a syntax you already know

  • Just HTML, nothing new to learn
  • No need for transpiler or compiler
  • Components, mount, unmount, and update hooks
  • Element refs, event handlers, attributes, and styles
  • Class helper
  • Fragments and arrays
  • XSS safe

Install

npm install --save htm-ally

yarn add htm-ally

and then:

import { html } from "htm-ally";

// or directly from unpkg

import { html } from "https://unpkg.com/htm-ally?module";

Usage

The output of html is just an HTMLFragment, so you can use it anywhere you can use a fragment:

const fragment = html`<div>hello</div>`;

document.getElementById("app").append(fragment);

Nesting fragments (or arrays of fragments) are supported:

const makeNested = (number) => html`
  <h1>hello</h1>
  <div>
    <p>${number}</p>
  </div>
`;

const nested = makeNested(100);

const fragment = html`
  <div>
    ${nested}
    ${[1, 2, 3].map(makeNested)}
  </div>
`;

Just insert interpolation with an object to define attributes and event handlers. Other attributes of the element are preserved:

const onclick = () => alert("clicked");

const fragment = html`
  <button id="button" ${{ onclick, class: "btn", disabled: false }}>
    Click me
  </button>
`;

Classes can also be defined as arrays or objects:

const primary = true;
const isDisabled = false;

const classNames = ["btn", primary && "primary", { disabled: isDisabled }];

const fragment = html`
  <button ${{ class: classNames }}>Click</button>
`;

Styles are defined as an object (with camelCase):

const fragment = html`
  <button ${{ style: { color: "red", fontWeight: "bold" } }}>Click</button>
`;

Refs can be defined with a function:

const inputRef = (el) => (inputRef.el = el);

const onChange = () => {
  console.log(inputRef.el.value);
};

const fragment = html`
  <input type="text" ${{ ref: inputRef, onchange: onChange }} />
`;

Components are just functions that return a fragment

const Component = () => html`<div>hello</div>`;

const fragment = html`<div>${Component}</div>`;

Components have access to an element it's mounted to (a slot element). The element contains update, mount, and unmount functions:

let count = 0;

const Counter = (parent) => {
  const onClick = () => {
    count += 1;
    parent.update();
  };

  return html`<button ${{ onclick: onClick }}>Count: ${count}</button>`;
};

document.getElementById("app").append(html`${Counter}`);

For the mount and unmount hooks, see the Timer example.

Note: text input components cannot be updated with the parent.update() as the cursor position will be reset.

Components can have a local state in a closure or on the parent node:

const Counter = (initialValue) => {
  let count = initialValue;

  return (parent) => {
    const onClick = () => {
      count += 1;
      parent.update();
    };

    return html`<button ${{ onclick: onClick }}>Count: ${count}</button>`;
  }
};

document.getElementById("app").append(html`${Counter(100)}`);

The state isn't preserved when the parent component updates (as there is no reconciliation), so it's recommended to use a global state to store states for each component.

Examples

All examples can be found in the CodeSandbox: link

Counter

Shows event handlers, attributes, component update

import { html } from "htm-ally";

let count = 0;

const Counter = (parent) => {
  const inc = () => {
    count += 1;
    parent.update();
  };

  const dec = () => {
    count -= 1;
    parent.update();
  };

  return html`
    <button ${{ onclick: dec, disabled: count <= 0 }}>-</button>
    ${count}
    <button ${{ onclick: inc, disabled: count >= 10 }}>+</button>
  `;
};

document.getElementById("app").append(html`${Counter}`);

Todo list

Shows ref usage, array map, and component update.

let todos = [];

const TodoList = () => {
  let inputRef = (el) => (inputRef.el = el);

  return (parent) => {
    const addTodo = () => {
      const text = inputRef.el.value;
      inputRef.el.value = "";
      todos.push(text);
      parent.update();
    };

    return html`
      <input type="text" ${{ ref: inputRef }} />
      <button ${{ onclick: addTodo }}>Add</button>
      <ul>
        ${todos.map((text) => html`<li>${text}</li>`)}
      </ul>
    `;
  };
};

document.getElementById("app").append(html`${TodoList()}`);

Timer

Shows mount and unmount hooks

let show = false;

const TimerToggle = (parent) => {
  const toggleShow = () => {
    show = !show;
    parent.update();
  };

  return html`
    <button ${{ onclick: toggleShow }}>${show ? "Hide" : "Show"}</button>
    ${showTimer && Timer}
  `;
};

let interval;
let time = 0;

const Timer = (parent) => {
  // start timer on mount
  parent.mount = () => {
    interval = setInterval(() => {
      time += 1;
      parent.update();
    }, 1000);

    time = 0;
    parent.update();
  };

  // stop timer on unmount
  parent.unmount = () => {
    clearInterval(interval);
  };

  return html`Timer: ${time}s`;
};

document.getElementById("app").append(html`${TimerToggle}`);

Advanced Todo list

See the CodeSanbox

Author

Eugene Daragan

License

MIT

About

500 bytes web app micro-framework with a syntax you already know

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published