mirror of https://github.com/n2geoff/uhm
release 0.7.0
This commit is contained in:
parent
6da60e2c00
commit
6068ea8220
14
README.md
14
README.md
|
@ -9,9 +9,12 @@ Uhm, because you should think about, ah, NOT using it.
|
|||
## Features
|
||||
- Real DOM
|
||||
- [Non-destructive (re)Rendering](https://github.com/bryhoyt/emerj)
|
||||
- Supports tagged html syntax via [xhtm](https://github.com/dy/xhtm)
|
||||
- No Build System
|
||||
- No Over Engineering
|
||||
- ~2kb minified
|
||||
- ~6kb minified / ~3kb gzip
|
||||
|
||||
> NOTE: experimenting with different builds, sizes may vary
|
||||
|
||||
## Install
|
||||
|
||||
|
@ -23,7 +26,7 @@ import {app,h} from "https://cdn.jsdelivr.net/gh/n2geoff/uhm/dist/uhm.min.js";
|
|||
|
||||
## Overview
|
||||
|
||||
**Uhm** only has 2 exported functions, `app()` and `h()`, and the later is optional.
|
||||
**Uhm** only has 3 exported functions, `app()`, `h()` and `html`, and the later is optional.
|
||||
|
||||
### app({opts})
|
||||
|
||||
|
@ -49,16 +52,16 @@ Interface with internal state for utility expansion
|
|||
|
||||
> !IMPORTANT: long running operations require manual `update()` called
|
||||
|
||||
### h(tag, [...args])
|
||||
### h(tag, attrs, [...children])
|
||||
|
||||
The `h()` is an **optional** hypertext build utility that weighs in around **~250b** and is provided as *a* way to build out your `view` DOM, but you can build your `view` using any method you like as long as it returns valid DOM.
|
||||
The `h()` is a hypertext build utility that provides *a* way to build out your `view` DOM, but you can build your `view` using `html` or even `jsx`, really any method you like as long as it returns valid DOM.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import {app, h} from "./um.min.js";
|
||||
import {app, h} from "./uhm.min.js";
|
||||
|
||||
const myapp = app({
|
||||
state: {name: "[Your Name Here]", job: "Developer"},
|
||||
|
@ -83,5 +86,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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n2geoff/uhm",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"exports": "./index.js",
|
||||
"tasks": {
|
||||
"dev": "deno run --watch index.js",
|
||||
|
|
|
@ -121,7 +121,7 @@ var diff = {
|
|||
},
|
||||
};
|
||||
|
||||
/*! Uhm v0.6.0 | MIT LICENSE | https://github.com/n2geoff/uhm */
|
||||
/*! Uhm v0.7.0 | MIT LICENSE | https://github.com/n2geoff/uhm */
|
||||
|
||||
/**
|
||||
* App Builder
|
||||
|
@ -139,21 +139,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";
|
||||
|
||||
/**
|
||||
* simple type validation check
|
||||
*
|
||||
* @param {*} value
|
||||
* @param {String} type
|
||||
* @returns {*}
|
||||
*/
|
||||
function check(value, type) {
|
||||
return typeof value === typeof type ? value : type;
|
||||
}
|
||||
const state = opts.state || {};
|
||||
const view = opts.view || (() => null);
|
||||
const actions = opts.actions || {};
|
||||
const mount = opts.mount || 'body';
|
||||
|
||||
/**
|
||||
* Assigns Dispatch-able Actions into App
|
||||
|
@ -163,9 +152,9 @@ function app(opts) {
|
|||
*/
|
||||
function dispatch(data, actions) {
|
||||
Object.entries(actions).forEach(([name, action]) => {
|
||||
if (typeof action === "function") {
|
||||
if (typeof action === 'function') {
|
||||
actions[name] = (...args) => {
|
||||
// update date from action return
|
||||
// update date from action
|
||||
Object.assign(state, action(data, ...args));
|
||||
|
||||
// delay update
|
||||
|
@ -179,7 +168,20 @@ function app(opts) {
|
|||
|
||||
/** update dom */
|
||||
const update = () => {
|
||||
diff.merge(document.querySelector(mount), view(state, actions));
|
||||
const parentNode = document.querySelector(mount);
|
||||
let result = view(state, actions);
|
||||
|
||||
// handle multiple nodes
|
||||
if (Array.isArray(result)) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(...result.filter(node => node != null));
|
||||
result = fragment;
|
||||
} else if (typeof result === 'string') {
|
||||
const temp = document.createElement(parentNode.tagName);
|
||||
temp.innerHTML = result;
|
||||
result = temp;
|
||||
}
|
||||
diff.merge(parentNode, result);
|
||||
};
|
||||
|
||||
// mount view
|
||||
|
@ -187,7 +189,7 @@ function app(opts) {
|
|||
dispatch(state, actions);
|
||||
}
|
||||
|
||||
return {state,update}
|
||||
return { state, update };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,31 +198,188 @@ function app(opts) {
|
|||
* 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
|
||||
* @param {Object} props - tag attributes
|
||||
* @param {Object|String|Array} args - text or array of child elements
|
||||
*
|
||||
* @returns {HTMLElement} The created DOM element(s)
|
||||
*/
|
||||
function h(tag, ...args) {
|
||||
const el = document.createElement(tag);
|
||||
function h(tagName, props, ...children) {
|
||||
const el = tagName === DocumentFragment ? document.createDocumentFragment() : document.createElement(tagName);
|
||||
const isScalar = (value) => typeof value === 'string' || typeof value === 'number';
|
||||
const booleanAttrs = ['disabled', 'checked', 'selected', 'hidden', 'readonly', 'required', 'open', 'autoplay', 'loop', 'muted'];
|
||||
|
||||
// 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;
|
||||
// Handle props (object or null)
|
||||
if (props != null && typeof props === 'object' && !Array.isArray(props)) {
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (value == null) continue;
|
||||
if (booleanAttrs.includes(key)) {
|
||||
if (value === true) {
|
||||
el.setAttribute(key, '');
|
||||
el[key] = true;
|
||||
} else if (value === false) {
|
||||
el.removeAttribute(key);
|
||||
el[key] = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key.startsWith('on') && typeof value === 'function') {
|
||||
el.addEventListener(key.slice(2).toLowerCase(), value);
|
||||
continue;
|
||||
}
|
||||
if (key === 'class') {
|
||||
el.className = value;
|
||||
continue;
|
||||
}
|
||||
if (key === 'style' && typeof value === 'object') {
|
||||
Object.assign(el.style, value);
|
||||
continue;
|
||||
}
|
||||
el.setAttribute(key, value);
|
||||
if (key in el) {
|
||||
el[key] = value;
|
||||
}
|
||||
}
|
||||
} else if (props != null) {
|
||||
// If props is not an object, treat it as a child
|
||||
children.unshift(props);
|
||||
}
|
||||
|
||||
// Handle children
|
||||
for (const child of children) {
|
||||
if (child == null) continue;
|
||||
if (isScalar(child)) {
|
||||
el.appendChild(document.createTextNode(child));
|
||||
} else if (Array.isArray(child)) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(...child.filter(c => c != null));
|
||||
el.appendChild(fragment);
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
} else if (typeof child === 'boolean') {
|
||||
console.warn(`Boolean child ${child} passed to h() for tag "${tagName}". Booleans are not rendered.`);
|
||||
} else {
|
||||
console.error(`Unsupported child type: ${typeof child} for tag "${tagName}" in h() function`);
|
||||
}
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
export { app, h };
|
||||
// source: https://github.com/dy/xhtm | MIT
|
||||
|
||||
const FIELD = '\ue000', QUOTES = '\ue001';
|
||||
|
||||
function htm (statics) {
|
||||
let h = this, prev = 0, current = [null], field = 0, args, name, value, quotes = [], quote = 0, last, level = 0, pre = false;
|
||||
|
||||
const evaluate = (str, parts = [], raw) => {
|
||||
let i = 0;
|
||||
str = (!raw && str === QUOTES ?
|
||||
quotes[quote++].slice(1,-1) :
|
||||
str.replace(/\ue001/g, m => quotes[quote++]));
|
||||
|
||||
if (!str) return str
|
||||
str.replace(/\ue000/g, (match, idx) => {
|
||||
if (idx) parts.push(str.slice(i, idx));
|
||||
i = idx + 1;
|
||||
return parts.push(arguments[++field])
|
||||
});
|
||||
if (i < str.length) parts.push(str.slice(i));
|
||||
return parts.length > 1 ? parts : parts[0]
|
||||
};
|
||||
|
||||
// close level
|
||||
const up = () => {
|
||||
// console.log('-level', current);
|
||||
[current, last, ...args] = current;
|
||||
current.push(h(last, ...args));
|
||||
if (pre === level--) pre = false; // reset <pre>
|
||||
};
|
||||
|
||||
let str = statics
|
||||
.join(FIELD)
|
||||
.replace(/<!--[^]*?-->/g, '')
|
||||
.replace(/<!\[CDATA\[[^]*\]\]>/g, '')
|
||||
.replace(/('|")[^\1]*?\1/g, match => (quotes.push(match), QUOTES));
|
||||
|
||||
// ...>text<... sequence
|
||||
str.replace(/(?:^|>)((?:[^<]|<[^\w\ue000\/?!>])*)(?:$|<)/g, (match, text, idx, str) => {
|
||||
let tag, close;
|
||||
|
||||
if (idx) {
|
||||
str.slice(prev, idx)
|
||||
// <abc/> → <abc />
|
||||
.replace(/(\S)\/$/, '$1 /')
|
||||
.split(/\s+/)
|
||||
.map((part, i) => {
|
||||
// </tag>, </> .../>
|
||||
if (part[0] === '/') {
|
||||
part = part.slice(1);
|
||||
// ignore duplicate empty closers </input>
|
||||
if (EMPTY[part]) return
|
||||
// ignore pairing self-closing tags
|
||||
close = tag || part || 1;
|
||||
// skip </input>
|
||||
}
|
||||
// <tag
|
||||
else if (!i) {
|
||||
tag = evaluate(part);
|
||||
// <p>abc<p>def, <tr><td>x<tr>
|
||||
if (typeof tag === 'string') { tag = tag.toLowerCase(); while (CLOSE[current[1]+tag]) up(); }
|
||||
current = [current, tag, null];
|
||||
level++;
|
||||
if (!pre && PRE[tag]) pre = level;
|
||||
// console.log('+level', tag)
|
||||
if (EMPTY[tag]) close = tag;
|
||||
}
|
||||
// attr=...
|
||||
else if (part) {
|
||||
let props = current[2] || (current[2] = {});
|
||||
if (part.slice(0, 3) === '...') {
|
||||
Object.assign(props, arguments[++field]);
|
||||
}
|
||||
else {
|
||||
[name, value] = part.split('=');
|
||||
Array.isArray(value = props[evaluate(name)] = value ? evaluate(value) : true) &&
|
||||
// if prop value is array - make sure it serializes as string without csv
|
||||
(value.toString = value.join.bind(value, ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (close) {
|
||||
if (!current[0]) err(`Wrong close tag \`${close}\``);
|
||||
up();
|
||||
// if last child is optionally closable - close it too
|
||||
while (last !== close && CLOSE[last]) up();
|
||||
}
|
||||
prev = idx + match.length;
|
||||
|
||||
// fix text indentation
|
||||
if (!pre) text = text.replace(/\s*\n\s*/g,'').replace(/\s+/g, ' ');
|
||||
|
||||
if (text) evaluate((last = 0, text), current, true);
|
||||
});
|
||||
|
||||
if (current[0] && CLOSE[current[1]]) up();
|
||||
|
||||
if (level) err(`Unclosed \`${current[1]}\`.`);
|
||||
|
||||
return current.length < 3 ? current[1] : (current.shift(), current)
|
||||
}
|
||||
|
||||
const err = (msg) => { throw SyntaxError(msg) };
|
||||
|
||||
// self-closing elements
|
||||
const EMPTY = htm.empty = {};
|
||||
|
||||
// optional closing elements
|
||||
const CLOSE = htm.close = {};
|
||||
|
||||
// preformatted text elements
|
||||
const PRE = htm.pre = {};
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
export { app, h, html };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*! 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)}};
|
||||
/*! Uhm v0.6.0 | MIT LICENSE | https://github.com/n2geoff/uhm */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)),setTimeout((()=>update()),20)})})),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};
|
||||
/*! Uhm v0.7.0 | MIT LICENSE | https://github.com/n2geoff/uhm */function app(opts){const state=opts.state||{},view=opts.view||(()=>null),actions=opts.actions||{},mount=opts.mount||"body";const update=()=>{const parentNode=document.querySelector(mount);let result=view(state,actions);if(Array.isArray(result)){const fragment=document.createDocumentFragment();fragment.append(...result.filter((node=>null!=node))),result=fragment}else if("string"==typeof result){const temp=document.createElement(parentNode.tagName);temp.innerHTML=result,result=temp}diff.merge(parentNode,result)};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)),setTimeout((()=>update()),20)})})),update()}(state,actions),{state:state,update:update}}function h(tagName,props,...children){const el=tagName===DocumentFragment?document.createDocumentFragment():document.createElement(tagName),booleanAttrs=["disabled","checked","selected","hidden","readonly","required","open","autoplay","loop","muted"];if(null==props||"object"!=typeof props||Array.isArray(props))null!=props&&children.unshift(props);else for(const[key,value]of Object.entries(props))null!=value&&(booleanAttrs.includes(key)?!0===value?(el.setAttribute(key,""),el[key]=!0):!1===value&&(el.removeAttribute(key),el[key]=!1):key.startsWith("on")&&"function"==typeof value?el.addEventListener(key.slice(2).toLowerCase(),value):"class"!==key?"style"!==key||"object"!=typeof value?(el.setAttribute(key,value),key in el&&(el[key]=value)):Object.assign(el.style,value):el.className=value);for(const child of children)if(null!=child)if("string"==typeof(value=child)||"number"==typeof value)el.appendChild(document.createTextNode(child));else if(Array.isArray(child)){const fragment=document.createDocumentFragment();fragment.append(...child.filter((c=>null!=c))),el.appendChild(fragment)}else child instanceof Node?el.appendChild(child):"boolean"==typeof child?console.warn(`Boolean child ${child} passed to h() for tag "${tagName}". Booleans are not rendered.`):console.error(`Unsupported child type: ${typeof child} for tag "${tagName}" in h() function`);var value;return el}const FIELD="",QUOTES="";function htm(statics){let args,name,value,last,h=this,prev=0,current=[null],field=0,quotes=[],quote=0,level=0,pre=!1;const evaluate=(str,parts=[],raw)=>{let i=0;return(str=raw||""!==str?str.replace(/\ue001/g,(m=>quotes[quote++])):quotes[quote++].slice(1,-1))?(str.replace(/\ue000/g,((match,idx)=>(idx&&parts.push(str.slice(i,idx)),i=idx+1,parts.push(arguments[++field])))),i<str.length&&parts.push(str.slice(i)),parts.length>1?parts:parts[0]):str},up=()=>{[current,last,...args]=current,current.push(h(last,...args)),pre===level--&&(pre=!1)};return statics.join("").replace(/<!--[^]*?-->/g,"").replace(/<!\[CDATA\[[^]*\]\]>/g,"").replace(/('|")[^\1]*?\1/g,(match=>(quotes.push(match),""))).replace(/(?:^|>)((?:[^<]|<[^\w\ue000\/?!>])*)(?:$|<)/g,((match,text,idx,str)=>{let tag,close;if(idx&&str.slice(prev,idx).replace(/(\S)\/$/,"$1 /").split(/\s+/).map(((part,i)=>{if("/"===part[0]){if(part=part.slice(1),EMPTY[part])return;close=tag||part||1}else if(i){if(part){let props=current[2]||(current[2]={});"..."===part.slice(0,3)?Object.assign(props,arguments[++field]):([name,value]=part.split("="),Array.isArray(value=props[evaluate(name)]=!value||evaluate(value))&&(value.toString=value.join.bind(value,"")))}}else{if(tag=evaluate(part),"string"==typeof tag)for(tag=tag.toLowerCase();CLOSE[current[1]+tag];)up();current=[current,tag,null],level++,!pre&&PRE[tag]&&(pre=level),EMPTY[tag]&&(close=tag)}})),close)for(current[0]||err(`Wrong close tag \`${close}\``),up();last!==close&&CLOSE[last];)up();prev=idx+match.length,pre||(text=text.replace(/\s*\n\s*/g,"").replace(/\s+/g," ")),text&&evaluate((last=0,text),current,!0)})),current[0]&&CLOSE[current[1]]&&up(),level&&err(`Unclosed \`${current[1]}\`.`),current.length<3?current[1]:(current.shift(),current)}const err=msg=>{throw SyntaxError(msg)},EMPTY=htm.empty={},CLOSE=htm.close={},PRE=htm.pre={},html=htm.bind(h);export{app,h,html};
|
||||
//# sourceMappingURL=true
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue