all repos — h3 @ 176ab9207a785a6186e7975298282b72786320ea

A tiny, extremely minimalist JavaScript microframework.

h3.min.js

 1
 2
 3
 4
 5
 6
 7
 8
/**
 * H3 v0.9.0 "Impeccable Iconian"
 * Copyright 2020 Fabio Cevasco <h3rald@h3rald.com>
 * 
 * @license MIT
 * For the full license, see: https://github.com/h3rald/h3/blob/master/LICENSE
 */
const checkProperties=(e,t)=>{for(const r in e){if(!(r in t))return!1;if(!equal(e[r],t[r]))return!1}return!0},equal=(e,t)=>{if(null===e&&null===t||void 0===e&&void 0===t)return!0;if(void 0===e&&void 0!==t||void 0!==e&&void 0===t||null===e&&null!==t||null!==e&&null===t)return!1;if(e.constructor!==t.constructor)return!1;if("function"==typeof e&&e.toString()!==t.toString())return!1;if([String,Number,Boolean].includes(e.constructor))return e===t;if(e.constructor===Array){if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(!equal(e[r],t[r]))return!1;return!0}return checkProperties(e,t)},selectorRegex=/^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i;let $onrenderCallbacks=[];class VNode{constructor(...e){if(this.type=void 0,this.attributes={},this.data={},this.id=void 0,this.$html=void 0,this.$onrender=void 0,this.style=void 0,this.value=void 0,this.children=[],this.classList=[],this.eventListeners={},0===e.length)throw new Error("[VNode] No arguments passed to VNode constructor.");if(1===e.length){let t=e[0];if("string"==typeof t)this.processSelector(t);else{if("function"!=typeof t&&("object"!=typeof t||null===t))throw new Error("[VNode] Invalid first argument passed to VNode constructor.");"#text"===t.type?(this.type="#text",this.value=t.value):this.from(this.processVNodeObject(t))}}else if(2===e.length){let[t,r]=e;if("string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),"string"==typeof r)return void(this.children=[new VNode({type:"#text",value:r})]);if("function"!=typeof r&&("object"!=typeof r||null===r))throw new Error("[VNode] The second argument of a VNode constructor must be an object, an array or a string.");Array.isArray(r)||r instanceof Function||r instanceof VNode?this.processChildren(r):this.processProperties(r)}else{if(3!==e.length)throw new Error("[VNode] Too many arguments passed to VNode constructor.");{let[t,r,s]=e;if("string"!=typeof t)throw new Error("[VNode] Invalid first argument passed to VNode constructor.");if(this.processSelector(t),"object"!=typeof r||null===r)throw new Error("[VNode] Invalid second argument passed to VNode constructor.");this.processProperties(r),this.processChildren(s)}}}from(e){this.value=e.value,this.type=e.type,this.id=e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.data=e.data,this.value=e.value,this.eventListeners=e.eventListeners,this.children=e.children,this.attributes=e.attributes,this.classList=e.classList}equal(e,t){return equal(e,void 0===t?this:t)}processProperties(e){this.id=this.id||e.id,this.$html=e.$html,this.$onrender=e.$onrender,this.style=e.style,this.value=e.value,this.data=e.data||{},this.classList=e.classList&&e.classList.length>0?e.classList:this.classList,this.attributes=e,Object.keys(e).filter(t=>t.startsWith("on")&&e[t]).forEach(t=>{if("function"!=typeof e[t])throw new Error(`[VNode] Event handler specified for ${t} event is not a function.`);this.eventListeners[t.slice(2)]=e[t],delete this.attributes[t]}),delete this.attributes.value,delete this.attributes.$html,delete this.attributes.$onrender,delete this.attributes.id,delete this.attributes.data,delete this.attributes.style,delete this.attributes.classList}processSelector(e){if(!e.match(selectorRegex))throw new Error("[VNode] Invalid selector: "+e);const[,t,r,s]=e.match(selectorRegex);this.type=t,r&&(this.id=r.slice(1)),this.classList=s&&s.split(".").slice(1)||[]}processVNodeObject(e){if(e instanceof VNode)return e;if(e instanceof Function){let t=e();if("string"==typeof t&&(t=new VNode({type:"#text",value:t})),!(t instanceof VNode))throw new Error("[VNode] Function argument does not return a VNode");return t}throw new Error("[VNode] Invalid first argument provided to VNode constructor.")}processChildren(e){const t=Array.isArray(e)?e:[e];this.children=t.map(e=>{if("string"==typeof e)return new VNode({type:"#text",value:e});if("function"==typeof e||"object"==typeof e&&null!==e)return this.processVNodeObject(e);if(e)throw new Error("[VNode] Specified child is not a VNode: "+e)}).filter(e=>e)}render(){if("#text"===this.type)return document.createTextNode(this.value);const e=document.createElement(this.type);return this.id&&(e.id=this.id),Object.keys(this.attributes).forEach(t=>{if(this.attributes[t]&&"string"==typeof this.attributes[t]){const r=document.createAttribute(t);r.value=this.attributes[t],e.setAttributeNode(r)}"string"==typeof this.attributes[t]&&e[t]||(e[t]=this.attributes[t])}),Object.keys(this.eventListeners).forEach(t=>{e.addEventListener(t,this.eventListeners[t])}),this.value&&(e.value=this.value),this.style&&(e.style.cssText=this.style),this.classList.forEach(t=>{e.classList.add(t)}),Object.keys(this.data).forEach(t=>{e.dataset[t]=this.data[t]}),this.children.forEach(t=>{const r=t.render();e.appendChild(r),t.$onrender&&$onrenderCallbacks.push(()=>t.$onrender(r))}),this.$html&&(e.innerHTML=this.$html),e}redraw(e){let{node:t,vnode:r}=e;const s=r,i=this;if(i.constructor!==s.constructor||i.type!==s.type||i.type===s.type&&"#text"===i.type&&i!==s){const e=s.render();return t.parentNode.replaceChild(e,t),s.$onrender&&s.$onrender(e),void i.from(s)}function n(e,t){let r=[],s=0,i=0;for(let n=0;n<e.children.length;n++){let o=-1;for(let a=0;a<t.children.length;a++)if(equal(e.children[n],t.children[a])&&!r.includes(a)){o=a,i++,s++;break}r.push(o)}if(i===s&&t.children.length===e.children.length)return r;if(i===t.children.length)for(let e=0;e<r.length;e++)-1===r[e]&&(r[e]=-3);if(s===e.children.length)for(let e=0;e<t.children.length;e++)r.includes(e)||r.splice(e,0,-2);if(t.children.length<e.children.length)for(let e=0;e<r.length;e++)-1!==r[e]||t.children[e]||(r[e]=-3);return r}i.id!==s.id&&(t.id=s.id||"",i.id=s.id),i.value!==s.value&&(t.value=s.value||"",i.value=s.value),equal(i.classList,s.classList)||(i.classList.forEach(e=>{s.classList.includes(e)||t.classList.remove(e)}),s.classList.forEach(e=>{i.classList.includes(e)||t.classList.add(e)}),i.classList=s.classList),i.style!==s.style&&(t.style.cssText=s.style||"",i.style=s.style),equal(i.data,s.data)||(Object.keys(i.data).forEach(e=>{s.data[e]?s.data[e]!==i.data[e]&&(t.dataset[e]=s.data[e]):delete t.dataset[e]}),Object.keys(s.data).forEach(e=>{i.data[e]||(t.dataset[e]=s.data[e])}),i.data=s.data),equal(i.attributes,s.attributes)||(Object.keys(i.attributes).forEach(e=>{!1===s.attributes[e]&&(t[e]=!1),s.attributes[e]?s.attributes[e]&&s.attributes[e]!==i.attributes[e]&&t.setAttribute(e,s.attributes[e]):t.removeAttribute(e)}),Object.keys(s.attributes).forEach(e=>{!i.attributes[e]&&s.attributes[e]&&t.setAttribute(e,s.attributes[e])}),i.attributes=s.attributes),equal(i.eventListeners,s.eventListeners)||(Object.keys(i.eventListeners).forEach(e=>{s.eventListeners[e]?equal(s.eventListeners[e],i.eventListeners[e])||(t.removeEventListener(e,i.eventListeners[e]),t.addEventListener(e,s.eventListeners[e])):t.removeEventListener(e,i.eventListeners[e])}),Object.keys(s.eventListeners).forEach(e=>{i.eventListeners[e]||t.addEventListener(e,s.eventListeners[e])}),i.eventListeners=s.eventListeners);let o=n(i,s),a=[...Array(o.filter(e=>-3!==e).length).keys()];for(;!equal(o,a);){let e=-1;for(let r of o){e++;let n=!1;if(r!==e){switch(r){case-1:i.children[e].redraw({node:t.childNodes[e],vnode:s.children[e]});break;case-2:i.children.splice(e,0,s.children[e]);const o=s.children[e].render();t.insertBefore(o,t.childNodes[e]),s.children[e].$onrender&&s.children[e].$onrender(o),n=!0;break;case-3:i.children.splice(e,1),t.removeChild(t.childNodes[e]),n=!0;break;default:const a=i.children.splice(r,1)[0];i.children.splice(e,0,a),t.insertBefore(t.childNodes[r],t.childNodes[e]),n=!0}if(n)break}}o=n(i,s),a=[...Array(o.length).keys()]}equal(i.$onrender,s.$onrender)||(i.$onrender=s.$onrender),i.$html!==s.$html&&(t.innerHTML=s.$html,i.$html=s.$html,i.$onrender&&i.$onrender(t))}}class Store{constructor(){this.events={},this.state={}}dispatch(e,t){if("$log"!==e&&this.dispatch("$log",{event:e,data:t}),this.events[e]){this.events[e].forEach(e=>{this.state={...this.state,...e(this.state,t)}})}}on(e,t){return(this.events[e]||(this.events[e]=[])).push(t),()=>{this.events[e]=this.events[e].filter(e=>e!==t)}}}class Route{constructor({path:e,def:t,query:r,parts:s}){if(this.path=e,this.def=t,this.query=r,this.parts=s,this.params={},this.query){this.query.split("&").forEach(e=>{const[t,r]=e.split("=");this.params[decodeURIComponent(t)]=decodeURIComponent(r)})}}}class Router{constructor({element:e,routes:t,store:r,location:s}){if(this.element=e,this.redraw=null,this.store=r,this.location=s||window.location,!t||0===Object.keys(t).length)throw new Error("[Router] No routes defined.");Object.keys(t);this.routes=t}setRedraw(e,t){this.redraw=()=>{e.redraw({node:this.element.childNodes[0],vnode:this.routes[this.route.def](t)}),this.store.dispatch("$redraw")}}async start(){const e=async e=>{const t=this.route,r=e&&e.newURL&&e.newURL.match(/(#.+)$/)&&e.newURL.match(/(#.+)$/)[1]||this.location.hash,s=r.replace(/\?.+$/,"").slice(1),i=r.match(/\?(.+)$/),n=i&&i[1]?i[1]:"",o=s.split("/").slice(1);let a={};for(let e of Object.keys(this.routes)){let t=e.split("/").slice(1),r=!0,i=0;for(a={};r&&t[i];){const e=t[i],s=o[i];e.startsWith(":")&&s?a[e.slice(1)]=s:r=e===s,i++}if(r){this.route=new Route({query:n,path:s,def:e,parts:a});break}}if(!this.route)throw new Error(`[Router] No route matches '${r}'`);if(t){const e=this.routes[t.def];e.state=e.teardown&&await e.teardown(e.state)}const l=this.routes[this.route.def];for(l.state={},l.setup&&await l.setup(l.state),redrawing=!0,this.store.dispatch("$navigation",this.route);this.element.firstChild;)this.element.removeChild(this.element.firstChild);const h=l(l.state),d=h.render();this.element.appendChild(d),h.$onrender&&h.$onrender(d),$onrenderCallbacks.forEach(e=>e()),$onrenderCallbacks=[],this.setRedraw(h,l.state),window.scrollTo(0,0),this.store.dispatch("$redraw"),redrawing=!1};window.addEventListener("hashchange",e),await e()}navigateTo(e,t){let r=Object.keys(t||{}).map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(t[e])}`).join("&");r=r?"?"+r:"",this.location.hash=`#${e}${r}`}}const h3=(...e)=>new VNode(...e);let store=null,router=null,redrawing=!1;h3.init=e=>{let{element:t,routes:r,modules:s,preStart:i,postStart:n,location:o}=e;if(!r){if("function"!=typeof e)throw new Error("[h3.init] The specified argument is not a valid configuration object or component function");r={"/":e}}if(t=t||document.body,!(t&&t instanceof Element))throw new Error("[h3.init] Invalid element specified.");return store=new Store,(s||[]).forEach(e=>{e(store)}),store.dispatch("$init"),router=new Router({element:t,routes:r,store:store,location:o}),Promise.resolve(i&&i()).then(()=>router.start()).then(()=>n&&n())},h3.navigateTo=(e,t)=>{if(!router)throw new Error("[h3.navigateTo] No application initialized, unable to navigate.");return router.navigateTo(e,t)},Object.defineProperty(h3,"route",{get:()=>{if(!router)throw new Error("[h3.route] No application initialized, unable to retrieve current route.");return router.route}}),Object.defineProperty(h3,"state",{get:()=>{if(!store)throw new Error("[h3.state] No application initialized, unable to retrieve current state.");return store.state}}),h3.on=(e,t)=>{if(!store)throw new Error("[h3.on] No application initialized, unable to listen to events.");return store.on(e,t)},h3.dispatch=(e,t)=>{if(!store)throw new Error("[h3.dispatch] No application initialized, unable to dispatch events.");return store.dispatch(e,t)},h3.redraw=e=>{if(!router||!router.redraw)throw new Error("[h3.redraw] No application initialized, unable to redraw.");redrawing||(redrawing=!0,router.redraw(),redrawing=e||!1)};export default h3;