mirror of https://github.com/n2geoff/um
Compare commits
No commits in common. "main" and "v0.4.0" have entirely different histories.
|
@ -1,19 +0,0 @@
|
|||
name: Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Publish package
|
||||
run: npx jsr publish
|
17
README.md
17
README.md
|
@ -7,19 +7,11 @@ Um, is an experimental composable UI builder that takes ideas from early [hypera
|
|||
Um, because you should think about, um, NOT using it.
|
||||
|
||||
## Features
|
||||
- Real DOM
|
||||
- [Non-destructive (re)Rendering](https://github.com/bryhoyt/emerj)
|
||||
- No Virtual Dom
|
||||
- No Build System
|
||||
- No Over Engineering
|
||||
- ~2kb minified
|
||||
|
||||
## Install
|
||||
|
||||
Via JSDelivr CDN
|
||||
|
||||
```js
|
||||
import {app,h} from "https://cdn.jsdelivr.net/gh/n2geoff/um/dist/um.min.js";
|
||||
```
|
||||
- ~1kb minified
|
||||
- Totally INEFFICIENT rendering (at scale)
|
||||
|
||||
## Overview
|
||||
|
||||
|
@ -81,5 +73,4 @@ The `h()` is an **optional** hypertext build utility that weighs in around **~25
|
|||
|
||||
### TODO
|
||||
|
||||
- Some tag attributes do not work, like rowspan on td
|
||||
- Rethink State Management, might be ok
|
||||
- Improve Update
|
||||
|
|
11
deno.json
11
deno.json
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "@n2geoff/um",
|
||||
"version": "0.5.0",
|
||||
"exports": "./index.js",
|
||||
"tasks": {
|
||||
"dev": "deno run --watch index.js",
|
||||
"format": "deno run -A npm:rollup index.js --file dist/um.js --format esm",
|
||||
"minify": "deno run -A npm:terser dist/um.js -c -o dist/um.min.js --source-map url",
|
||||
"build": "deno task format && deno task minify"
|
||||
}
|
||||
}
|
173
deno.lock
173
deno.lock
|
@ -1,173 +0,0 @@
|
|||
{
|
||||
"version": "3",
|
||||
"packages": {
|
||||
"specifiers": {
|
||||
"npm:rollup": "npm:rollup@4.18.0",
|
||||
"npm:terser": "npm:terser@5.31.0"
|
||||
},
|
||||
"npm": {
|
||||
"@jridgewell/gen-mapping@0.3.5": {
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "@jridgewell/set-array@1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "@jridgewell/sourcemap-codec@1.4.15",
|
||||
"@jridgewell/trace-mapping": "@jridgewell/trace-mapping@0.3.25"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri@3.1.2": {
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@jridgewell/set-array@1.2.1": {
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@jridgewell/source-map@0.3.6": {
|
||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "@jridgewell/gen-mapping@0.3.5",
|
||||
"@jridgewell/trace-mapping": "@jridgewell/trace-mapping@0.3.25"
|
||||
}
|
||||
},
|
||||
"@jridgewell/sourcemap-codec@1.4.15": {
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@jridgewell/trace-mapping@0.3.25": {
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "@jridgewell/resolve-uri@3.1.2",
|
||||
"@jridgewell/sourcemap-codec": "@jridgewell/sourcemap-codec@1.4.15"
|
||||
}
|
||||
},
|
||||
"@rollup/rollup-android-arm-eabi@4.18.0": {
|
||||
"integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-android-arm64@4.18.0": {
|
||||
"integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-darwin-arm64@4.18.0": {
|
||||
"integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-darwin-x64@4.18.0": {
|
||||
"integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.18.0": {
|
||||
"integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.18.0": {
|
||||
"integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-gnu@4.18.0": {
|
||||
"integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-musl@4.18.0": {
|
||||
"integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": {
|
||||
"integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.18.0": {
|
||||
"integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-s390x-gnu@4.18.0": {
|
||||
"integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-x64-gnu@4.18.0": {
|
||||
"integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-linux-x64-musl@4.18.0": {
|
||||
"integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-win32-arm64-msvc@4.18.0": {
|
||||
"integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-win32-ia32-msvc@4.18.0": {
|
||||
"integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@rollup/rollup-win32-x64-msvc@4.18.0": {
|
||||
"integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"@types/estree@1.0.5": {
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"acorn@8.11.3": {
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"buffer-from@1.1.2": {
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"commander@2.20.3": {
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"fsevents@2.3.3": {
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"rollup@4.18.0": {
|
||||
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
|
||||
"dependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "@rollup/rollup-android-arm-eabi@4.18.0",
|
||||
"@rollup/rollup-android-arm64": "@rollup/rollup-android-arm64@4.18.0",
|
||||
"@rollup/rollup-darwin-arm64": "@rollup/rollup-darwin-arm64@4.18.0",
|
||||
"@rollup/rollup-darwin-x64": "@rollup/rollup-darwin-x64@4.18.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "@rollup/rollup-linux-arm-gnueabihf@4.18.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "@rollup/rollup-linux-arm-musleabihf@4.18.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "@rollup/rollup-linux-arm64-gnu@4.18.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "@rollup/rollup-linux-arm64-musl@4.18.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "@rollup/rollup-linux-powerpc64le-gnu@4.18.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "@rollup/rollup-linux-riscv64-gnu@4.18.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "@rollup/rollup-linux-s390x-gnu@4.18.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "@rollup/rollup-linux-x64-gnu@4.18.0",
|
||||
"@rollup/rollup-linux-x64-musl": "@rollup/rollup-linux-x64-musl@4.18.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "@rollup/rollup-win32-arm64-msvc@4.18.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "@rollup/rollup-win32-ia32-msvc@4.18.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "@rollup/rollup-win32-x64-msvc@4.18.0",
|
||||
"@types/estree": "@types/estree@1.0.5",
|
||||
"fsevents": "fsevents@2.3.3"
|
||||
}
|
||||
},
|
||||
"source-map-support@0.5.21": {
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dependencies": {
|
||||
"buffer-from": "buffer-from@1.1.2",
|
||||
"source-map": "source-map@0.6.1"
|
||||
}
|
||||
},
|
||||
"source-map@0.6.1": {
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"terser@5.31.0": {
|
||||
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "@jridgewell/source-map@0.3.6",
|
||||
"acorn": "acorn@8.11.3",
|
||||
"commander": "commander@2.20.3",
|
||||
"source-map-support": "source-map-support@0.5.21"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remote": {}
|
||||
}
|
|
@ -1,128 +1,32 @@
|
|||
/*! Emerj v1.0.0 | MIT LICENSE | https://github.com/bryhoyt/emerj */
|
||||
var diff = {
|
||||
attrs(elem) {
|
||||
const attrs = {};
|
||||
for (let i=0; i < elem.attributes.length; i++) {
|
||||
const attr = elem.attributes[i];
|
||||
attrs[attr.name] = attr.value;
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
nodesByKey(parent, makeKey) {
|
||||
const map = {};
|
||||
for (let j=0; j < parent.childNodes.length; j++) {
|
||||
const key = makeKey(parent.childNodes[j]);
|
||||
if (key) map[key] = parent.childNodes[j];
|
||||
}
|
||||
return map;
|
||||
},
|
||||
merge(base, modified, opts) {
|
||||
/* Merge any differences between base and modified back into base.
|
||||
/**
|
||||
* HTML Tag Scripting Function
|
||||
*
|
||||
* Operates only the children nodes, and does not change the root node or its
|
||||
* attributes.
|
||||
* Generates new DOM element(s) from a tag, attributes
|
||||
*
|
||||
* Conceptually similar to React's reconciliation algorithm:
|
||||
* https://facebook.github.io/react/docs/reconciliation.html
|
||||
* @param {String} tag - tag name
|
||||
* @param {Object|String|Array} args - attributes, text or array of child elements
|
||||
*
|
||||
* I haven't thoroughly tested performance to compare to naive DOM updates (i.e.
|
||||
* just updating the entire DOM from a string using .innerHTML), but some quick
|
||||
* tests on a basic DOMs were twice as fast -- so at least it's not slower in
|
||||
* a simple scenario -- and it's definitely "fast enough" for responsive UI and
|
||||
* even smooth animation.
|
||||
*
|
||||
* The real advantage for me is not so much performance, but that state & identity
|
||||
* of existing elements is preserved -- text typed into an <input>, an open
|
||||
* <select> dropdown, scroll position, ad-hoc attached events, canvas paint, etc,
|
||||
* are preserved as long as an element's identity remains.
|
||||
*
|
||||
* See https://korynunn.wordpress.com/2013/03/19/the-dom-isnt-slow-you-are/
|
||||
* @returns {HTMLElement} The created DOM element(s)
|
||||
*/
|
||||
opts = opts || {};
|
||||
opts.key = opts.key || (node => node.id);
|
||||
function h(tag, ...args) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
if (typeof modified === 'string') {
|
||||
const html = modified;
|
||||
// Make sure the parent element of the provided HTML is of the same type as
|
||||
// `base`'s parent. This matters when the HTML contains fragments that are
|
||||
// only valid inside certain elements, eg <td>s, which must have a <tr>
|
||||
// parent.
|
||||
modified = document.createElement(base.tagName);
|
||||
modified.innerHTML = html;
|
||||
}
|
||||
// support all scalar values as TextNodes
|
||||
const isScalar = (value) => ["boolean", "string", "number"].includes(typeof value);
|
||||
|
||||
// Naively recurse into the children, if any, replacing or updating new
|
||||
// elements that are in the same position as old, deleting trailing elements
|
||||
// when the new list contains fewer children, or appending new elements if
|
||||
// it contains more children.
|
||||
//
|
||||
// For re-ordered children, the `id` attribute can be used to preserve identity.
|
||||
args.forEach((arg) => {
|
||||
if (isScalar(arg)) {
|
||||
el.appendChild(document.createTextNode(arg));
|
||||
} else if (Array.isArray(arg)) {
|
||||
el.append(...arg);
|
||||
} else {
|
||||
Object.assign(el, arg);
|
||||
}
|
||||
});
|
||||
|
||||
// Loop through .childNodes, not just .children, so we compare text nodes (and
|
||||
// comment nodes, fwiw) too.
|
||||
|
||||
const nodesByKey = {old: this.nodesByKey(base, opts.key),
|
||||
new: this.nodesByKey(modified, opts.key)};
|
||||
|
||||
let idx;
|
||||
for (idx=0; modified.firstChild; idx++) {
|
||||
const newNode = modified.removeChild(modified.firstChild);
|
||||
if (idx >= base.childNodes.length) {
|
||||
// It's a new node. Append it.
|
||||
base.appendChild(newNode);
|
||||
continue;
|
||||
return el;
|
||||
}
|
||||
|
||||
let baseNode = base.childNodes[idx];
|
||||
|
||||
// If the children are indexed, then make sure to retain their identity in
|
||||
// the new order.
|
||||
const newKey = opts.key(newNode);
|
||||
if (opts.key(baseNode) || newKey) {
|
||||
// If the new node has a key, then either use its existing match, or insert it.
|
||||
// If not, but the old node has a key, then make sure to leave it untouched and insert the new one instead.
|
||||
// Else neither node has a key. Just overwrite old with new.
|
||||
const match = (newKey && newKey in nodesByKey.old)? nodesByKey.old[newKey]: newNode;
|
||||
if (match !== baseNode) {
|
||||
baseNode = base.insertBefore(match, baseNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (baseNode.nodeType !== newNode.nodeType || baseNode.tagName !== newNode.tagName) {
|
||||
// Completely different node types. Just update the whole subtree, like React does.
|
||||
base.replaceChild(newNode, baseNode);
|
||||
} else if ([Node.TEXT_NODE, Node.COMMENT_NODE].indexOf(baseNode.nodeType) >= 0) {
|
||||
// This is the terminating case of the merge() recursion.
|
||||
if (baseNode.textContent === newNode.textContent) continue; // Don't write if we don't need to.
|
||||
baseNode.textContent = newNode.textContent;
|
||||
} else if (baseNode !== newNode) { // Only need to update if we haven't just inserted the newNode in.
|
||||
// It's an existing node with the same tag name. Update only what's necessary.
|
||||
// First, make dicts of attributes, for fast lookup:
|
||||
const attrs = {base: this.attrs(baseNode), new: this.attrs(newNode)};
|
||||
for (const attr in attrs.base) {
|
||||
// Remove any missing attributes.
|
||||
if (attr in attrs.new) continue;
|
||||
baseNode.removeAttribute(attr);
|
||||
}
|
||||
for (const attr in attrs.new) {
|
||||
// Add and update any new or modified attributes.
|
||||
if (attr in attrs.base && attrs.base[attr] === attrs.new[attr]) continue;
|
||||
baseNode.setAttribute(attr, attrs.new[attr]);
|
||||
}
|
||||
// Now, recurse into the children. If the only children are text, this will
|
||||
// be the final recursion on this node.
|
||||
this.merge(baseNode, newNode);
|
||||
}
|
||||
}
|
||||
while (base.childNodes.length > idx) {
|
||||
// If base has more children than modified, delete the extras.
|
||||
base.removeChild(base.lastChild);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/*! Um v0.5.0 | MIT LICENSE | https://github.com/n2geoff/um */
|
||||
|
||||
/**
|
||||
* App Builder
|
||||
*
|
||||
|
@ -139,10 +43,10 @@ var diff = {
|
|||
*/
|
||||
function app(opts) {
|
||||
// initial setup
|
||||
const state = check(opts.state, {});
|
||||
const view = check(opts.view, () => null);
|
||||
const actions = check(opts.actions, {});
|
||||
const mount = opts.mount || "body";
|
||||
let state = check(opts.state, {});
|
||||
let view = check(opts.view, () => null);
|
||||
let actions = check(opts.actions, {});
|
||||
let mount = opts.mount || "body";
|
||||
|
||||
/**
|
||||
* simple type validation check
|
||||
|
@ -179,7 +83,7 @@ function app(opts) {
|
|||
|
||||
/** update dom */
|
||||
const update = () => {
|
||||
diff.merge(document.querySelector(mount), view(state, actions));
|
||||
document.querySelector(mount).replaceChildren(view(state, actions));
|
||||
};
|
||||
|
||||
// mount view
|
||||
|
@ -190,37 +94,4 @@ function app(opts) {
|
|||
return {state,update}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML Tag Scripting Function
|
||||
*
|
||||
* Generates new DOM element(s) from a tag, attributes
|
||||
*
|
||||
* @param {String} tag - tag name
|
||||
* @param {Object|String|Array} args - attributes, text or array of child elements
|
||||
*
|
||||
* @returns {HTMLElement} The created DOM element(s)
|
||||
*/
|
||||
function h(tag, ...args) {
|
||||
const el = document.createElement(tag);
|
||||
|
||||
// support all scalar values as TextNodes
|
||||
const isScalar = (value) => ["boolean", "string", "number"].includes(typeof value);
|
||||
|
||||
for(let i = 0; i < args.length; i++) {
|
||||
if (isScalar(args[i])) {
|
||||
el.appendChild(document.createTextNode(args[i]));
|
||||
} else if (Array.isArray(args[i])) {
|
||||
el.append(...args[i]);
|
||||
} else {
|
||||
for(const [k,v] of Object.entries(args[i])) {
|
||||
// if not both ways, some attributes do not render
|
||||
el.setAttribute(k, v);
|
||||
el[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
export { app, h };
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/*! Emerj v1.0.0 | MIT LICENSE | https://github.com/bryhoyt/emerj */
|
||||
var diff={attrs(elem){const attrs={};for(let i=0;i<elem.attributes.length;i++){const attr=elem.attributes[i];attrs[attr.name]=attr.value}return attrs},nodesByKey(parent,makeKey){const map={};for(let j=0;j<parent.childNodes.length;j++){const key=makeKey(parent.childNodes[j]);key&&(map[key]=parent.childNodes[j])}return map},merge(base,modified,opts){if((opts=opts||{}).key=opts.key||(node=>node.id),"string"==typeof modified){const html=modified;(modified=document.createElement(base.tagName)).innerHTML=html}const nodesByKey={old:this.nodesByKey(base,opts.key),new:this.nodesByKey(modified,opts.key)};let idx;for(idx=0;modified.firstChild;idx++){const newNode=modified.removeChild(modified.firstChild);if(idx>=base.childNodes.length){base.appendChild(newNode);continue}let baseNode=base.childNodes[idx];const newKey=opts.key(newNode);if(opts.key(baseNode)||newKey){const match=newKey&&newKey in nodesByKey.old?nodesByKey.old[newKey]:newNode;match!==baseNode&&(baseNode=base.insertBefore(match,baseNode))}if(baseNode.nodeType!==newNode.nodeType||baseNode.tagName!==newNode.tagName)base.replaceChild(newNode,baseNode);else if([Node.TEXT_NODE,Node.COMMENT_NODE].indexOf(baseNode.nodeType)>=0){if(baseNode.textContent===newNode.textContent)continue;baseNode.textContent=newNode.textContent}else if(baseNode!==newNode){const attrs={base:this.attrs(baseNode),new:this.attrs(newNode)};for(const attr in attrs.base)attr in attrs.new||baseNode.removeAttribute(attr);for(const attr in attrs.new)attr in attrs.base&&attrs.base[attr]===attrs.new[attr]||baseNode.setAttribute(attr,attrs.new[attr]);this.merge(baseNode,newNode)}}for(;base.childNodes.length>idx;)base.removeChild(base.lastChild)}};
|
||||
/*! Um v0.5.0 | MIT LICENSE | https://github.com/n2geoff/um */function app(opts){const state=check(opts.state,{}),view=check(opts.view,(()=>null)),actions=check(opts.actions,{}),mount=opts.mount||"body";function check(value,type){return typeof value==typeof type?value:type}const update=()=>{diff.merge(document.querySelector(mount),view(state,actions))};return opts.view&&mount&&function(data,actions){Object.entries(actions).forEach((([name,action])=>{"function"==typeof action&&(actions[name]=(...args)=>{Object.assign(state,action(data,...args)),update()})})),update()}(state,actions),{state:state,update:update}}function h(tag,...args){const el=document.createElement(tag);for(let i=0;i<args.length;i++)if(value=args[i],["boolean","string","number"].includes(typeof value))el.appendChild(document.createTextNode(args[i]));else if(Array.isArray(args[i]))el.append(...args[i]);else for(const[k,v]of Object.entries(args[i]))el.setAttribute(k,v),el[k]=v;var value;return el}export{app,h};
|
||||
/*! Um v:0.4.0 | MIT LICENSE | https://github.com/n2geoff/um */
|
||||
function h(tag,...args){const el=document.createElement(tag);return args.forEach((arg=>{["boolean","string","number"].includes(typeof arg)?el.appendChild(document.createTextNode(arg)):Array.isArray(arg)?el.append(...arg):Object.assign(el,arg)})),el}function app(opts){let state=check(opts.state,{}),view=check(opts.view,(()=>null)),actions=check(opts.actions,{}),mount=opts.mount||"body";function check(value,type){return typeof value==typeof type?value:type}const update=()=>{document.querySelector(mount).replaceChildren(view(state,actions))};return opts.view&&mount&&function(data,actions){Object.entries(actions).forEach((([name,action])=>{"function"==typeof action&&(actions[name]=(...args)=>{Object.assign(state,action(data,...args)),update()})})),update()}(state,actions),{state:state,update:update}}export{app,h};
|
||||
//# sourceMappingURL=true
|
|
@ -1 +1 @@
|
|||
{"version":3,"names":["diff","attrs","elem","i","attributes","length","attr","name","value","nodesByKey","parent","makeKey","map","j","childNodes","key","merge","base","modified","opts","node","id","html","document","createElement","tagName","innerHTML","old","this","new","idx","firstChild","newNode","removeChild","appendChild","baseNode","newKey","match","insertBefore","nodeType","replaceChild","Node","TEXT_NODE","COMMENT_NODE","indexOf","textContent","removeAttribute","setAttribute","lastChild","app","state","check","view","actions","mount","type","update","querySelector","data","Object","entries","forEach","action","args","assign","dispatch","h","tag","el","includes","createTextNode","Array","isArray","append","k","v"],"sources":["dist/um.js"],"mappings":"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","ignoreList":[]}
|
||||
{"version":3,"names":["h","tag","args","el","document","createElement","forEach","arg","includes","appendChild","createTextNode","Array","isArray","append","Object","assign","app","opts","state","check","view","actions","mount","value","type","update","querySelector","replaceChildren","data","entries","name","action","dispatch"],"sources":["dist/um.js"],"mappings":"AAUA,SAASA,EAAEC,OAAQC,MACf,MAAMC,GAAKC,SAASC,cAAcJ,KAelC,OAVAC,KAAKI,SAASC,MAFc,CAAC,UAAW,SAAU,UAAUC,gBAG3CD,KACTJ,GAAGM,YAAYL,SAASM,eAAeH,MAChCI,MAAMC,QAAQL,KACrBJ,GAAGU,UAAUN,KAEbO,OAAOC,OAAOZ,GAAII,IACtB,IAGGJ,EACX,CAgBA,SAASa,IAAIC,MAET,IAAIC,MAAUC,MAAMF,KAAKC,MAAO,CAAC,GAC7BE,KAAUD,MAAMF,KAAKG,MAAM,IAAM,OACjCC,QAAUF,MAAMF,KAAKI,QAAS,CAAC,GAC/BC,MAAUL,KAAKK,OAAS,OAS5B,SAASH,MAAMI,MAAOC,MAClB,cAAcD,cAAiBC,KAAOD,MAAQC,IAClD,CAyBA,MAAMC,OAAS,KACXrB,SAASsB,cAAcJ,OAAOK,gBAAgBP,KAAKF,MAAOG,SAAS,EAQvE,OAJIJ,KAAKG,MAAQE,OAtBjB,SAAkBM,KAAMP,SACpBP,OAAOe,QAAQR,SAASf,SAAQ,EAAEwB,KAAMC,WACd,mBAAXA,SACPV,QAAQS,MAAQ,IAAI5B,QAEhBY,OAAOC,OAAOG,MAAOa,OAAOH,QAAS1B,OAGrCuB,QAAQ,EAEhB,IAGJA,QACJ,CASIO,CAASd,MAAOG,SAGb,CAACH,YAAMO,cAClB,QAEST,IAAKhB","ignoreList":[]}
|
|
@ -1,85 +0,0 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UM | Todo Example</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/n2geoff/stylelite/dist/stylelite.min.css">
|
||||
<style>
|
||||
button.link {
|
||||
height: 1rem;
|
||||
padding: .25rem;
|
||||
}
|
||||
div, span {margin-top: .25rem; margin-bottom: .25rem;}
|
||||
label>* {vertical-align: top;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="app"></div>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import {app, h} from "../index.js";
|
||||
|
||||
const $ = document.querySelector.bind(document);
|
||||
|
||||
const todo = app({
|
||||
state: {todos: [], value: ""},
|
||||
actions: {
|
||||
submit: (state, event) => {
|
||||
if(event.key === "Enter") {
|
||||
const value = $("#todo").value;
|
||||
if(String(value).trim()) {
|
||||
$("#todo").value = "";
|
||||
return {...state, todos: [...state.todos, value]}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
view: (state, actions) => {
|
||||
return h("main", [
|
||||
h("h1", {style:"text-align: center"}, "Todos"),
|
||||
h("div", [
|
||||
h("input", {
|
||||
id: "todo",
|
||||
placeholder: "What needs to be done?",
|
||||
onkeypress: actions.submit,
|
||||
value: state.value
|
||||
}),
|
||||
]),
|
||||
h("div", state.todos.map((i) => {
|
||||
return h("div", {style: "border-bottom: 1px solid #CCC"}, [
|
||||
h("label", [
|
||||
h("input", {type: "checkbox"}),
|
||||
h("span", i)
|
||||
])
|
||||
])
|
||||
})
|
||||
),
|
||||
h("hr"),
|
||||
h("div",{class: "grid"}, [
|
||||
h("div",{style: "margin-left: 1rem"} ,`${state.todos.length} items left`),
|
||||
h("div",{class: "grid"}, [
|
||||
h("button",{class: "link"}, "All"),
|
||||
h("button",{class: "link"}, "Active"),
|
||||
h("button",{class: "link"}, "Completed"),
|
||||
]),
|
||||
h("div", [
|
||||
h("button",{class: "link", style: "float:right"}, "Clear Completed")
|
||||
]),
|
||||
]),
|
||||
h("hr"),
|
||||
]);
|
||||
},
|
||||
mount: "#app"
|
||||
});
|
||||
|
||||
// Update State outside Creation
|
||||
// todo.state.todos[0] = "Hello";
|
||||
// todo.update();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
5
index.js
5
index.js
|
@ -1,5 +0,0 @@
|
|||
import app from "./src/app.js";
|
||||
import h from "./src/tag.js";
|
||||
|
||||
export {app};
|
||||
export {h};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "@n2geoff/um",
|
||||
"version": "0.4.0",
|
||||
"exports": "./src/index.js"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
build:
|
||||
bunx rollup src/index.js --file dist/um.js --format esm
|
||||
|
||||
minify:
|
||||
bunx terser dist/um.js -c -o dist/um.min.js --source-map url
|
||||
|
||||
all: build minify
|
14
src/app.js
14
src/app.js
|
@ -1,7 +1,3 @@
|
|||
import diff from "./emerj.js";
|
||||
|
||||
/*! Um v0.5.0 | MIT LICENSE | https://github.com/n2geoff/um */
|
||||
|
||||
/**
|
||||
* App Builder
|
||||
*
|
||||
|
@ -18,10 +14,10 @@ import diff from "./emerj.js";
|
|||
*/
|
||||
export default function app(opts) {
|
||||
// initial setup
|
||||
const state = check(opts.state, {});
|
||||
const view = check(opts.view, () => null);
|
||||
const actions = check(opts.actions, {});
|
||||
const mount = opts.mount || "body";
|
||||
let state = check(opts.state, {});
|
||||
let view = check(opts.view, () => null);
|
||||
let actions = check(opts.actions, {});
|
||||
let mount = opts.mount || "body";
|
||||
|
||||
/**
|
||||
* simple type validation check
|
||||
|
@ -58,7 +54,7 @@ export default function app(opts) {
|
|||
|
||||
/** update dom */
|
||||
const update = () => {
|
||||
diff.merge(document.querySelector(mount), view(state, actions));
|
||||
document.querySelector(mount).replaceChildren(view(state, actions));
|
||||
}
|
||||
|
||||
// mount view
|
||||
|
|
122
src/emerj.js
122
src/emerj.js
|
@ -1,122 +0,0 @@
|
|||
/*! Emerj v1.0.0 | MIT LICENSE | https://github.com/bryhoyt/emerj */
|
||||
export default {
|
||||
attrs(elem) {
|
||||
const attrs = {};
|
||||
for (let i=0; i < elem.attributes.length; i++) {
|
||||
const attr = elem.attributes[i];
|
||||
attrs[attr.name] = attr.value;
|
||||
}
|
||||
return attrs;
|
||||
},
|
||||
nodesByKey(parent, makeKey) {
|
||||
const map = {};
|
||||
for (let j=0; j < parent.childNodes.length; j++) {
|
||||
const key = makeKey(parent.childNodes[j]);
|
||||
if (key) map[key] = parent.childNodes[j];
|
||||
}
|
||||
return map;
|
||||
},
|
||||
merge(base, modified, opts) {
|
||||
/* Merge any differences between base and modified back into base.
|
||||
*
|
||||
* Operates only the children nodes, and does not change the root node or its
|
||||
* attributes.
|
||||
*
|
||||
* Conceptually similar to React's reconciliation algorithm:
|
||||
* https://facebook.github.io/react/docs/reconciliation.html
|
||||
*
|
||||
* I haven't thoroughly tested performance to compare to naive DOM updates (i.e.
|
||||
* just updating the entire DOM from a string using .innerHTML), but some quick
|
||||
* tests on a basic DOMs were twice as fast -- so at least it's not slower in
|
||||
* a simple scenario -- and it's definitely "fast enough" for responsive UI and
|
||||
* even smooth animation.
|
||||
*
|
||||
* The real advantage for me is not so much performance, but that state & identity
|
||||
* of existing elements is preserved -- text typed into an <input>, an open
|
||||
* <select> dropdown, scroll position, ad-hoc attached events, canvas paint, etc,
|
||||
* are preserved as long as an element's identity remains.
|
||||
*
|
||||
* See https://korynunn.wordpress.com/2013/03/19/the-dom-isnt-slow-you-are/
|
||||
*/
|
||||
opts = opts || {};
|
||||
opts.key = opts.key || (node => node.id);
|
||||
|
||||
if (typeof modified === 'string') {
|
||||
const html = modified;
|
||||
// Make sure the parent element of the provided HTML is of the same type as
|
||||
// `base`'s parent. This matters when the HTML contains fragments that are
|
||||
// only valid inside certain elements, eg <td>s, which must have a <tr>
|
||||
// parent.
|
||||
modified = document.createElement(base.tagName);
|
||||
modified.innerHTML = html;
|
||||
}
|
||||
|
||||
// Naively recurse into the children, if any, replacing or updating new
|
||||
// elements that are in the same position as old, deleting trailing elements
|
||||
// when the new list contains fewer children, or appending new elements if
|
||||
// it contains more children.
|
||||
//
|
||||
// For re-ordered children, the `id` attribute can be used to preserve identity.
|
||||
|
||||
// Loop through .childNodes, not just .children, so we compare text nodes (and
|
||||
// comment nodes, fwiw) too.
|
||||
|
||||
const nodesByKey = {old: this.nodesByKey(base, opts.key),
|
||||
new: this.nodesByKey(modified, opts.key)};
|
||||
|
||||
let idx;
|
||||
for (idx=0; modified.firstChild; idx++) {
|
||||
const newNode = modified.removeChild(modified.firstChild);
|
||||
if (idx >= base.childNodes.length) {
|
||||
// It's a new node. Append it.
|
||||
base.appendChild(newNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
let baseNode = base.childNodes[idx];
|
||||
|
||||
// If the children are indexed, then make sure to retain their identity in
|
||||
// the new order.
|
||||
const newKey = opts.key(newNode);
|
||||
if (opts.key(baseNode) || newKey) {
|
||||
// If the new node has a key, then either use its existing match, or insert it.
|
||||
// If not, but the old node has a key, then make sure to leave it untouched and insert the new one instead.
|
||||
// Else neither node has a key. Just overwrite old with new.
|
||||
const match = (newKey && newKey in nodesByKey.old)? nodesByKey.old[newKey]: newNode;
|
||||
if (match !== baseNode) {
|
||||
baseNode = base.insertBefore(match, baseNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (baseNode.nodeType !== newNode.nodeType || baseNode.tagName !== newNode.tagName) {
|
||||
// Completely different node types. Just update the whole subtree, like React does.
|
||||
base.replaceChild(newNode, baseNode);
|
||||
} else if ([Node.TEXT_NODE, Node.COMMENT_NODE].indexOf(baseNode.nodeType) >= 0) {
|
||||
// This is the terminating case of the merge() recursion.
|
||||
if (baseNode.textContent === newNode.textContent) continue; // Don't write if we don't need to.
|
||||
baseNode.textContent = newNode.textContent;
|
||||
} else if (baseNode !== newNode) { // Only need to update if we haven't just inserted the newNode in.
|
||||
// It's an existing node with the same tag name. Update only what's necessary.
|
||||
// First, make dicts of attributes, for fast lookup:
|
||||
const attrs = {base: this.attrs(baseNode), new: this.attrs(newNode)};
|
||||
for (const attr in attrs.base) {
|
||||
// Remove any missing attributes.
|
||||
if (attr in attrs.new) continue;
|
||||
baseNode.removeAttribute(attr);
|
||||
}
|
||||
for (const attr in attrs.new) {
|
||||
// Add and update any new or modified attributes.
|
||||
if (attr in attrs.base && attrs.base[attr] === attrs.new[attr]) continue;
|
||||
baseNode.setAttribute(attr, attrs.new[attr]);
|
||||
}
|
||||
// Now, recurse into the children. If the only children are text, this will
|
||||
// be the final recursion on this node.
|
||||
this.merge(baseNode, newNode);
|
||||
}
|
||||
}
|
||||
while (base.childNodes.length > idx) {
|
||||
// If base has more children than modified, delete the extras.
|
||||
base.removeChild(base.lastChild);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import h from "./tag.js";
|
||||
import app from "./app.js";
|
||||
|
||||
export {app};
|
||||
export {h};
|
18
src/tag.js
18
src/tag.js
|
@ -14,19 +14,15 @@ export default function h(tag, ...args) {
|
|||
// support all scalar values as TextNodes
|
||||
const isScalar = (value) => ["boolean", "string", "number"].includes(typeof value);
|
||||
|
||||
for(let i = 0; i < args.length; i++) {
|
||||
if (isScalar(args[i])) {
|
||||
el.appendChild(document.createTextNode(args[i]));
|
||||
} else if (Array.isArray(args[i])) {
|
||||
el.append(...args[i]);
|
||||
args.forEach((arg) => {
|
||||
if (isScalar(arg)) {
|
||||
el.appendChild(document.createTextNode(arg));
|
||||
} else if (Array.isArray(arg)) {
|
||||
el.append(...arg);
|
||||
} else {
|
||||
for(const [k,v] of Object.entries(args[i])) {
|
||||
// if not both ways, some attributes do not render
|
||||
el.setAttribute(k, v);
|
||||
el[k] = v;
|
||||
}
|
||||
}
|
||||
Object.assign(el, arg);
|
||||
}
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tagged UI Creation Lib</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module">
|
||||
import {app, h} from "../src/index.js";
|
||||
|
||||
const $ = document.querySelector.bind(document);
|
||||
|
||||
const todo = app({
|
||||
state: {todos: ["one", "two", "three"], value: ""},
|
||||
actions: {
|
||||
add: (state, event) => {
|
||||
return {...state, todos: [...state.todos, $("#todo").value]};
|
||||
}
|
||||
},
|
||||
view: (state, actions) => {
|
||||
return h("main", [
|
||||
h("h1", "Todo App"),
|
||||
h("hr"),
|
||||
h("div", [
|
||||
h("label", "Todo"),
|
||||
h("input", {id: "todo", value: state.value}),
|
||||
h("button", {onclick: actions.add}, "Add")
|
||||
]),
|
||||
h("hr"),
|
||||
h("ul", state.todos.map((i) => {
|
||||
return h("li", {}, i)
|
||||
})
|
||||
),
|
||||
h("hr"),
|
||||
h("strong", `Count: ${state.todos.length}`),
|
||||
h("hr"),
|
||||
]);
|
||||
},
|
||||
mount: "#app"
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue