diff --git a/deno.json b/deno.json
index 90fca06..75c8180 100644
--- a/deno.json
+++ b/deno.json
@@ -1,6 +1,6 @@
{
"name": "@n2geoff/um",
- "version": "0.4.1",
+ "version": "0.5.0",
"exports": "./index.js",
"tasks": {
"dev": "deno run --watch index.js",
diff --git a/dist/um.js b/dist/um.js
index fe24b87..55b101a 100644
--- a/dist/um.js
+++ b/dist/um.js
@@ -1,32 +1,128 @@
-/**
- * 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);
-
- args.forEach((arg) => {
- if (isScalar(arg)) {
- el.appendChild(document.createTextNode(arg));
- } else if (Array.isArray(arg)) {
- el.append(...arg);
- } else {
- Object.assign(el, arg);
+/*! 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.
+ *
+ * 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 , an open
+ * 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);
- return el;
-}
+ 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 s, which must have a
+ // 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);
+ }
+ },
+};
+
+/*! Um v0.5.0 | MIT LICENSE | https://github.com/n2geoff/um */
+
/**
* App Builder
*
@@ -83,7 +179,7 @@ function app(opts) {
/** update dom */
const update = () => {
- document.querySelector(mount).replaceChildren(view(state, actions));
+ diff.merge(document.querySelector(mount), view(state, actions));
};
// mount view
@@ -94,4 +190,37 @@ 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 };
diff --git a/dist/um.min.js b/dist/um.min.js
index f3ad342..3ef44b6 100644
--- a/dist/um.min.js
+++ b/dist/um.min.js
@@ -1,3 +1,4 @@
-/*! Um v:0.4.1 | 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){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=()=>{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};
+/*! Emerj v1.0.0 | MIT LICENSE | https://github.com/bryhoyt/emerj */
+var diff={attrs(elem){const attrs={};for(let i=0;inode.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