A Better DOM API

What is HyperclayJS?

HyperclayJS is the JavaScript library that powers single-file HTML apps. It gives your HTML files the ability to overwrite themselves, toggles between admin and viewer modes, and blurs the line between document and application.

What is Hyperclay?

Hyperclay is a unique system that lets you directly shape HTML files and have changes persist live on the web.

Features:

  • Turn HTML files into apps
  • Share access, attach custom domains, allow signups
  • Create a simple CMS by turning HTML into a JSON API
  • Sync apps from Hyperclay Local directly online

Build admin controls that are automatically stripped before saving and restored when you return, so regular users only ever see a clean static page.

One HTML file, full application.

Save (core)

Hyperclay pages update themselves. The save state is tracked via a savestatus attribute on the <html> element.

savePage()

Persist page's HTML to a hyperclay server

hyperclay.savePage(callback);
Click to trigger save

save-system

CMD+S, [trigger-save] button, savestatus attribute

<html savestatus="saved">
<!-- saved | saving | dirty | error -->
Type here to mark dirty
savestatus: saved

autosave

Auto-save on DOM mutations

// Automatic - debounces saves
// after DOM changes
Edit me, pause, watch auto-save
Waiting for edits...

save-toast

Toast on save events

// Shows toast on
// hyperclay:save-* events

[onaftersave]

Run JS after save events

<div onaftersave="this.textContent =
  event.detail.status">
Waiting for save event...

cacheBust()

Cache-bust href/src attributes

<link href="styles.css"
  onaftersave="cacheBust(this)">
Click to see URL update
Edit Mode (core)

Edit mode is a state defined by an editmode attribute on the <html> element. When true, admin controls are usable/visible; when false, they are disabled/hidden.

toggleEditMode()

Switch edit mode on/off (affects other demos)

hyperclay.toggleEditMode();
hyperclay.isEditMode;
editmode: true

[edit-mode-contenteditable]

Only editable in edit mode, otherwise readonly

<div edit-mode-contenteditable>
  Editable in edit mode
</div>
Only editable in edit mode, otherwise readonly

[edit-mode-input]

Inputs/selects editable only in edit mode

<input edit-mode-input>
<select edit-mode-input>

[edit-mode-onclick]

onclick handlers active only in edit mode

<button edit-mode-onclick
  onclick="alert('Admin!')">
Only works in edit mode

[edit-mode-resource]

Scripts/styles active only in edit mode

<style edit-mode-resource>
  .edit-outline { outline: 2px dashed blue; }
</style>
Dashed outline in edit mode

[persist]

Persist form values to DOM attributes

<input persist>
<select persist>
Values will appear here
DOM Helpers

Properties added to all elements that patch two gaps in the native DOM: traversing intuitively through the natural hierarchy, and getting/setting values in one short command.

el.val

Get/set attribute values on nearest matching element

<input a oninput="this.val.sum =
  Number(this.val.a) + Number(this.val.b)">
+ =

el.text

Get/set innerText on nearest matching element

<button onclick="this.text.counter--">-</button>
<div counter>0</div>
0

el.exec

Execute code stored in an attribute

<div confirm-delete="consent('Delete?',
  () => this.remove())">
Deletable Item
Click delete to trigger consent

el.nearest

Nearest matching element (searches siblings, ancestors, and their descendants)

<span paintable>Item</span>
<button onclick="this.nearest.paintable
  .style.background = randomColor()">
Paintable Item
Click paint to color the sibling badge
Option Visibility

Show or hide elements based on the value of an attribute on an ancestor element. Uses the option: prefix.

option:savestatus

Show elements based on hyperclay save state

<span option:savestatus="saved">
  ✓ Saved
</span>
✓ Saved Saving... Unsaved

option:editmode

Show elements only in hyperclay edit mode

<button option:editmode="true">
  Edit Tools
</button>
Edit Tools View Only

option:status (custom)

Show elements based on any custom attribute

<div status="pending">
  <span option:status="completed">
⏳ Pending ✓ Completed
Event Attributes

Custom event attributes that extend the native on* pattern to handle rendering, cloning, click-away, and drag-and-drop sorting.

[onrender]

Execute JS when element renders

<span onrender="this.textContent =
  new Date().toLocaleTimeString()">

[onclone]

Execute JS when element is cloned

<div onclone="this.style.background =
  randomColor()">

[onclickaway]

Trigger code when clicking outside element

<div onclick="reset"
     onclickaway="trigger">
Click outside me

[sortable]

Drag-and-drop reordering with onsorted callback

<ul sortable onsorted="...">
  <li>Item</li>
</ul>
  • Drag me (1)
  • Drag me (2)
  • Drag me (3)
Drag items to reorder
All()

The tersest way to work with multiple elements. All.x selects by attribute or class, native array methods transform them, and any DOM method applies to all at once.

Close Menus

Select all elements by attribute and modify them

All.menu.classList.add('hidden');

Content Mirror

Sync content across elements with same identifier

All.sync_card.forEach(c =>
  c.val.project_name === this.val.project_name
  && c.innerHTML !== this.innerHTML
  && (c.innerHTML = this.innerHTML))
Edit me - I sync with below
Edit me - I sync with above

Filter by Category

Filter elements with a dropdown

All.card.forEach(c =>
  c.classList.toggle('muted', ...)
)
Work
Personal
Work
Mutation

Run code when the DOM changes, scoped to a single element or the entire page.

[onmutation]

Trigger code when this element or its children change

<div onmutation="console.log('changed')">
  <span contenteditable>Edit</span>
</div>
Edit me to trigger mutation
Edit the text above

[onglobalmutation]

Trigger code when any element on the page changes

<span onglobalmutation="this.textContent =
  All('li').length">0</span>
    Total items: 0

    Mutation.onAddElement

    Programmatic observer for added elements

    Mutation.onAddElement({
      selectorFilter: '.item'
    }, changes => ...);
    Click to add watched elements
    Utilities

    Standalone helpers for common tasks: DOM morphing, clipboard, slugs, query params, and style injection.

    HyperMorph

    DOM morphing with content-based matching

    HyperMorph.morph(el, newHtml);
    A
    A
    A

    insertStyles()

    Inject CSS (external or inline)

    // External stylesheet
    insertStyles('./test-style.css');
    
    // Inline CSS with name
    insertStyles('my-theme', '.foo { color: red }');
    Click to inject

    slugify()

    URL-friendly slug generator

    slugify('Hello World!')
    // "hello-world"

    copyToClipboard()

    Copy text to clipboard

    copyToClipboard('Hello!');

    query

    Parse URL search params

    // ?name=dev
    query.name // "dev"
    (no params)
    UI Components

    The modals and prompts that should ship with the browser: ask for input, confirm an action, show a message.

    theModal

    Parent module for ask/tell/consent/snippet

    themodal.html = '<h2>Custom</h2>';
    themodal.open();

    ask()

    Edit attributes inline with ask() and el.nearest

    ask('URL:', res => {
      this.nearest.link.href = res;
    })
    example.com

    tell()

    Display informational message, returns promise

    await tell('Welcome!', 'Info');

    snippet()

    Show code snippet with copy button

    await snippet('Install', code, 'Copy');

    toast()

    Show notification messages

    toast('Saved!', 'success');
    Form Helpers

    What native forms are missing: auto-growing textareas and serialization that handles nested objects and arrays.

    getDataFromForm()

    Serialize form to object

    const data = getDataFromForm(form);
    Click serialize

    [autosize]

    Textarea grows with content

    <textarea autosize></textarea>
    Press Enter to grow

    [prevent-enter]

    Prevent Enter key, useful for inline editing with contenteditable elements

    <span contenteditable prevent-enter>
    No newlines here - Enter does nothing
    Communication

    Send form data and upload files to Hyperclay servers with one function call.

    sendMessage()

    Submit form data to /message endpoint

    await sendMessage(event, 'Sent!');
    Submit to test

    uploadFile()

    Upload file with progress toasts, copies URL to clipboard

    <input type="file" onchange="uploadFile(event)">
    Select a file