rasti 3.0.0-alpha.0 → 3.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rasti.min.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";class e{on(t,e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),()=>this.off(t,e)}once(t,e){if("function"==typeof e){const s=this,i=e;e=function(...n){i(...n),s.off(t,e)}}return this.on(t,e)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((function(t){t(...e)}))}}var s=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);const i={el:!0,tag:!0,attributes:!0,events:!0,model:!0,template:!0,onDestroy:!0};class n extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.uid="uid"+ ++n.uid,this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],Object.keys(t).forEach((e=>{i[e]&&(this[e]=t[e])})),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureElement(){if(this.el)this.el=s(this.el,this);else{const t=s(this.tag,this),e=s(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.parentNode.removeChild(this.el),this}delegateEvents(t){if(t||(t=s(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let e={};return Object.keys(t).forEach((s=>{const i=s.split(" "),n=i.shift(),r=i.join(" ");let h=t[s];h=("string"==typeof h?this[h]:h).bind(this),e[n]||(e[n]=[]),e[n].push({selector:r,listener:h})})),Object.keys(e).forEach((t=>{const s=s=>{e[t].forEach((({selector:t,listener:e})=>{t&&!s.target.closest(t)||e(s,this)}))};this.delegatedEventListeners.push({type:t,listener:s}),this.el.addEventListener(t,s)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}}n.uid=0;const r=(t,e)=>s(t,e,e),h=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(E.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),l=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)"),i=new RegExp(`${s}`,"g"),n=[];let r,h=0;for(;null!==(r=i.exec(t));){const s=t.slice(h,r.index);n.push(s,e[r[1]]),h=r.index+r[0].length}return n.push(t.slice(h)),n},o=(t,e)=>{const s=t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t.all=Object.assign(t.all,i):"string"==typeof i&&(t.all[i]=!0);else{const n=e(s[1]);t.all[i]=n}return t}),{all:{},events:{},attributes:{}});return Object.keys(s.all).forEach((t=>{const e=t.match(/on(([A-Z]{1}[a-z]+)+)/);e&&e[1]?s.events[e[1].toLowerCase()]=s.all[t]:s.attributes[t]=s.all[t]})),s},c=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)");return t.replace(new RegExp(`<(${s})([^>]*)>([\\s\\S]*?)</(${s})>|<(${s})([^>]*)/>`,"g"),(function(){const{tag:t,attributes:s,inner:i,close:n,raw:h}=a(arguments,e);if(!(t.prototype instanceof E))return h;let d;if(n){if(t!==n)return h;const s=l(c(i,e),e);d=function(){return s.map((t=>r(t,this))).flat()}}return e.push((function(){const e=o(s,(t=>r(t,this))).all;return d&&(e.renderChildren=d.bind(this)),t.mount(e)})),E.PLACEHOLDER_EXPRESSION(e.length-1)}))},a=(t,e)=>{const s=E.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,l,o,c,a,d,u]=t,f={raw:i,attributes:[]};let p;n?(f.tag=void 0!==r?e[r]:n,f.inner=l,f.close=void 0!==c?e[c]:o,p=h):(f.tag=void 0!==d?e[d]:a,p=u);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(p));){const[,t,s,,i,n]=g,r=void 0!==s?e[s]:t,h=void 0!==i?e[i]:n;void 0!==h?f.attributes.push([r,h]):f.attributes.push([r])}return f},d={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0},u={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,source:!0,track:!0,wbr:!0};class E extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{d[e]&&(this[e]=t[e])})),this.options=t,this.onCreate.apply(this,arguments)}subscribe(t){if(!t.on)return;const e=this.onChange.bind(this),s=t.on("change",e);return this.destroyQueue.push("function"==typeof s?s:()=>t.off("change",e)),this}isContainer(){return!(this.tag||!this.template)}ensureElement(){this.el&&(this.el=s(this.el,this),this.delegateEvents())}findElement(t){return(t||document).querySelector(`[${E.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],n={[E.DATA_ATTRIBUTE_UID]:this.uid};return this.attributes&&Object.assign(n,s(this.attributes,this)),Object.keys(n).forEach((s=>{let r=n[s];!1===r?e[s]=!0:!0===r?(t[s]="",i.push(s)):(null==r&&(r=""),t[s]=r,i.push(`${s}="${r}"`))})),{add:t,remove:e,html:i.join(" ")}}hydrate(t){return this.model&&this.subscribe(this.model),this.state&&this.subscribe(this.state),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.el=this.findElement(t),this.delegateEvents(),this.children.forEach((t=>t.hydrate(this.el)))),this.onRender.call(this,E.RENDER_TYPE_HYDRATE),this}recycle(t){if(this.isContainer())return this.children[0].recycle(t);return this.findElement(t).replaceWith(this.el),this.onRender.call(this,E.RENDER_TYPE_RECYCLE),this}destroy(){return super.destroy.apply(this,arguments),this.destroyed=!0,this}onCreate(){}onChange(){this.render()}onRender(){}onDestroy(){}partial(t,...e){return l(c(h(t,e),e),e).map((t=>r(t,this))).flat()}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${E.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!u[t]?`<${t} ${e}></${t}>`:`<${t} ${e} />`}toString(){if(this.destroyChildren(),this.isContainer())return this.template.call(this,this.addChild.bind(this));const t=s(this.tag,this)||"div",e=this.getAttributes().html,i=this.template?this.template.call(this,this.addChild.bind(this)):"";return this.template||!u[t]?`<${t} ${e}>${i}</${t}>`:`<${t} ${e} />`}render(){if(this.destroyed)return this;if(!this.isContainer()){if(!this.el){const t=this.createElement("template");return t.innerHTML=this,this.hydrate(t.content),this}const t=this.getAttributes();Object.keys(t.remove).forEach((t=>{this.el.removeAttribute(t)})),Object.keys(t.add).forEach((e=>{this.el.setAttribute(e,t.add[e])}))}if(this.template){const t=document.activeElement,e=[],s=[],i=this.children;this.children=[];const n=this.template.call(this,(t=>{let n=t;const r=t.key&&i.find((e=>e.key===t.key));return r?(n=r.getRecyclePlaceholder(),s.push(r),t.destroy()):e.push(t),n}));if(this.isContainer())if(e[0]){const t=this.createElement("template");t.innerHTML=n,this.addChild(e[0]).hydrate(t.content);const s=t.content.children[0];this.el&&this.el.replaceWith(s),this.el=s}else{if(!s[0])throw new Error("Container component must have a child component");this.addChild(s[0])}else this.el.innerHTML=n,e.forEach((t=>{this.addChild(t).hydrate(this.el)})),s.forEach((t=>{this.addChild(t).recycle(this.el)}));i.forEach((t=>{s.indexOf(t)>-1||t.destroy()})),this.el.contains(t)&&t.focus()}return this.onRender.call(this,E.RENDER_TYPE_RENDER),this}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.appendChild(i.render().el)),i}static create(t,...e){const s=E.PLACEHOLDER_EXPRESSION("(\\d+)");let i,n,d,u;"function"==typeof t&&(e=[t],t=["",""]);const f=c(h(t,e),e);let p=f.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(p){const{tag:t,attributes:s,inner:h,close:c}=a(p,e);if(i=function(){return r(t,this)},n=function(){return o(s,(t=>r(t,this))).attributes},d=function(){const t=o(s,(t=>r(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=r(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},c){const t=h?l(h,e):[];u=function(e){return t.map((t=>r(t,this))).flat().map((t=>null!=t&&!1!==t&&!0!==t?t instanceof E?e(t):t:"")).join("")}}}else{if(p=f.match(new RegExp(`^\\s*${s}\\s*$`)),!p)throw new SyntaxError("Invalid component");u=function(t){return t(r(e[p[1]],this)).toString()}}return this.extend({tag:i,attributes:n,events:d,template:u})}}E.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,E.DATA_ATTRIBUTE_UID="data-rasti-uid",E.RENDER_TYPE_HYDRATE="hydrate",E.RENDER_TYPE_RECYCLE="recycle",E.RENDER_TYPE_RENDER="render",t.Component=E,t.Emitter=e,t.Model=class extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments);const e=s(this.defaults,this)||{};this.attributes=Object.assign({},e,t),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,t,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const l=Object.keys(h);if(l.length&&(this._pending=["change",this,h,...n]),l.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}toJSON(){return Object.assign({},this.attributes)}},t.View=n}));
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";class e{on(t,e){if("function"!=typeof e)throw new TypeError("Listener must be a function");return this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e),()=>this.off(t,e)}once(t,e){if("function"==typeof e){const s=this,i=e;e=function(...n){i(...n),s.off(t,e)}}return this.on(t,e)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((function(t){t(...e)}))}}var s=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);const i={el:!0,tag:!0,attributes:!0,events:!0,model:!0,template:!0,onDestroy:!0};class n extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.uid="uid"+ ++n.uid,this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],Object.keys(t).forEach((e=>{i[e]&&(this[e]=t[e])})),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureElement(){if(this.el)this.el=s(this.el,this);else{const t=s(this.tag,this),e=s(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.parentNode.removeChild(this.el),this}delegateEvents(t){if(t||(t=s(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let e={};return Object.keys(t).forEach((s=>{const i=s.split(" "),n=i.shift(),r=i.join(" ");let h=t[s];h=("string"==typeof h?this[h]:h).bind(this),e[n]||(e[n]=[]),e[n].push({selector:r,listener:h})})),Object.keys(e).forEach((t=>{const s=s=>{e[t].forEach((({selector:t,listener:e})=>{t&&!s.target.closest(t)||e(s,this)}))};this.delegatedEventListeners.push({type:t,listener:s}),this.el.addEventListener(t,s)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}static sanitize(t){return`${t}`.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[t])))}}n.uid=0;const r=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...r(e)):t.push(e),t)),[]);class h{constructor(t){this.value=t}toString(){return this.value}}const o=(t,e)=>s(t,e,e),l=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(f.PLACEHOLDER_EXPRESSION(i)),t)),[]).join(""),a=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),i=new RegExp(`${s}`,"g"),n=[];let r,o=0;for(;null!==(r=i.exec(t));){const s=t.slice(o,r.index);n.push(new h(s),e[r[1]]),o=r.index+r[0].length}return n.push(new h(t.slice(o))),n},c=(t,e)=>{const s=t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t.all=Object.assign(t.all,i):"string"==typeof i&&(t.all[i]=!0);else{const n=e(s[1]);t.all[i]=n}return t}),{all:{},events:{},attributes:{}});return Object.keys(s.all).forEach((t=>{const e=t.match(/on(([A-Z]{1}[a-z]+)+)/);e&&e[1]?s.events[e[1].toLowerCase()]=s.all[t]:s.attributes[t]=s.all[t]})),s},u=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)");return t.replace(new RegExp(`<(${s})([^>]*)>([\\s\\S]*?)</(${s})>|<(${s})([^>]*)/>`,"g"),(function(){const{tag:t,attributes:s,inner:i,close:n,raw:h}=d(arguments,e);if(!(t.prototype instanceof f))return h;let l;if(n){if(t!==n)return h;const s=a(u(i,e),e);l=function(){return r(s.map((t=>o(t,this))))}}return e.push((function(){const e=c(s,(t=>o(t,this))).all;return l&&(e.renderChildren=l.bind(this)),t.mount(e)})),f.PLACEHOLDER_EXPRESSION(e.length-1)}))},d=(t,e)=>{const s=f.PLACEHOLDER_EXPRESSION("(\\d+)"),[i,n,r,h,o,l,a,c,u,d]=t,E={raw:i,attributes:[]};let p;n?(E.tag=void 0!==r?e[r]:n,E.inner=o,E.close=void 0!==a?e[a]:l,p=h):(E.tag=void 0!==u?e[u]:c,p=d);const y=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let g;for(;null!==(g=y.exec(p));){const[,t,s,,i,n]=g,r=void 0!==s?e[s]:t,h=void 0!==i?e[i]:n;void 0!==h?E.attributes.push([r,h]):E.attributes.push([r])}return E},E={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,source:!0,track:!0,wbr:!0},p={key:!0,state:!0,onCreate:!0,onChange:!0,onRender:!0};class f extends n{constructor(t={}){super(...arguments),Object.keys(t).forEach((e=>{p[e]&&(this[e]=t[e])})),this.options=t,this.onCreate.apply(this,arguments)}subscribe(t){if(!t.on)return;const e=this.onChange.bind(this),s=t.on("change",e);return this.destroyQueue.push("function"==typeof s?s:()=>t.off("change",e)),this}isContainer(){return!(this.tag||!this.template)}ensureElement(){this.el&&(this.el=s(this.el,this),this.delegateEvents())}findElement(t){return(t||document).querySelector(`[${f.DATA_ATTRIBUTE_UID}="${this.uid}"]`)}getAttributes(){const t={},e={},i=[],r={[f.DATA_ATTRIBUTE_UID]:this.uid};this.attributes&&Object.assign(r,s(this.attributes,this));const h=this.previousAttributes||{};return this.previousAttributes=r,Object.keys(r).forEach((s=>{let h=r[s];!1===h?e[s]=!0:!0===h?(t[s]="",i.push(s)):(null==h&&(h=""),t[s]=h,i.push(`${n.sanitize(s)}="${n.sanitize(h)}"`))})),Object.keys(h).forEach((t=>{t in r||(e[t]=!0)})),{add:t,remove:e,html:i.join(" ")}}hydrate(t){return this.model&&this.subscribe(this.model),this.state&&this.subscribe(this.state),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.el=this.findElement(t),this.delegateEvents(),this.children.forEach((t=>t.hydrate(this.el)))),this.onRender.call(this,f.RENDER_TYPE_HYDRATE),this}recycle(t){if(this.isContainer())return this.children[0].recycle(t);return this.findElement(t).replaceWith(this.el),this.onRender.call(this,f.RENDER_TYPE_RECYCLE),this}destroy(){return super.destroy.apply(this,arguments),this.destroyed=!0,this}onCreate(){}onChange(){this.render()}onRender(){}onDestroy(){}partial(t,...e){return r(a(u(l(t,e),e),e).map((t=>o(t,this))))}getRecyclePlaceholder(){if(this.isContainer())return this.children[0].getRecyclePlaceholder();const t=s(this.tag,this)||"div",e=`${f.DATA_ATTRIBUTE_UID}="${this.uid}"`;return this.template||!E[t]?`<${t} ${e}></${t}>`:`<${t} ${e} />`}toString(){if(this.destroyChildren(),this.isContainer())return this.template.call(this,this.addChild.bind(this));const t=s(this.tag,this)||"div",e=this.getAttributes().html,i=this.template?this.template.call(this,this.addChild.bind(this)):"";return this.template||!E[t]?`<${t} ${e}>${i}</${t}>`:`<${t} ${e} />`}render(){if(this.destroyed)return this;if(!this.isContainer()){if(!this.el){const t=this.createElement("template");return t.innerHTML=this,this.hydrate(t.content),this}const t=this.getAttributes();Object.keys(t.remove).forEach((t=>{this.el.removeAttribute(t)})),Object.keys(t.add).forEach((e=>{this.el.setAttribute(e,t.add[e])}))}if(this.template){const t=document.activeElement,e=[],s=[],i=this.children;this.children=[];const n=this.template.call(this,(t=>{let n=t;const r=t.key&&i.find((e=>e.key===t.key));return r?(n=r.getRecyclePlaceholder(),s.push(r),t.destroy()):e.push(t),n}));if(this.isContainer())if(e[0]){const t=this.createElement("template");t.innerHTML=n,this.addChild(e[0]).hydrate(t.content);const s=t.content.children[0];this.el&&this.el.replaceWith(s),this.el=s}else{if(!s[0])throw new Error("Container component must have a child component");this.addChild(s[0])}else this.el.innerHTML=n,e.forEach((t=>{this.addChild(t).hydrate(this.el)})),s.forEach((t=>{this.addChild(t).recycle(this.el)}));i.forEach((t=>{s.indexOf(t)>-1||t.destroy()})),this.el.contains(t)&&t.focus()}return this.onRender.call(this,f.RENDER_TYPE_RENDER),this}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.appendChild(i.render().el)),i}static create(t,...e){const s=f.PLACEHOLDER_EXPRESSION("(\\d+)");let i,E,p,y;"function"==typeof t&&(e=[t],t=["",""]);const g=u(l(t,e),e);let b=g.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${s})([^>]*)>([\\s\\S]*?)</(\\1|${s})>\\s*$|^\\s*<([a-z]+[1-6]?|${s})([^>]*)/>\\s*$`));if(b){const{tag:t,attributes:s,inner:l,close:u}=d(b,e);if(i=function(){return n.sanitize(o(t,this))},E=function(){return c(s,(t=>o(t,this))).attributes},p=function(){const t=c(s,(t=>o(t,this))).events;return Object.keys(t).reduce(((e,s)=>{const i=o(t[s],this);return Object.keys(i).forEach((t=>{e[`${s}${"&"===t?"":` ${t}`}`]=i[t]})),e}),{})},u){const t=l?a(l,e):[];y=function(e){return r(t.map((t=>o(t,this)))).map((t=>null!=t&&!1!==t&&!0!==t?t instanceof h?t:t instanceof f?e(t):n.sanitize(`${t}`):"")).join("")}}}else{if(b=g.match(new RegExp(`^\\s*${s}\\s*$`)),!b)throw new SyntaxError("Invalid component");y=function(t){return t(o(e[b[1]],this)).toString()}}return this.extend({tag:i,attributes:E,events:p,template:y})}}f.PLACEHOLDER_EXPRESSION=t=>`__RASTI_{${t}}__`,f.DATA_ATTRIBUTE_UID="data-rasti-uid",f.RENDER_TYPE_HYDRATE="hydrate",f.RENDER_TYPE_RECYCLE="recycle",f.RENDER_TYPE_RENDER="render",t.Component=f,t.Emitter=e,t.Model=class extends e{constructor(t={}){super(),this.preinitialize.apply(this,arguments);const e=s(this.defaults,this)||{};this.attributes=Object.assign({},e,t),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,t,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const o=Object.keys(h);if(o.length&&(this._pending=["change",this,h,...n]),o.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}toJSON(){return Object.assign({},this.attributes)}},t.View=n}));
package/es/Component.js CHANGED
@@ -1,7 +1,21 @@
1
1
  import View from './View.js';
2
2
  import getResult from './utils/getResult.js';
3
+ import deepFlat from './utils/deepFlat.js';
3
4
  import './Emitter.js';
4
5
 
6
+ /*
7
+ * Wrapper class for HTML strings marked as safe.
8
+ */
9
+ class SafeHTML {
10
+ constructor(value) {
11
+ this.value = value;
12
+ }
13
+
14
+ toString() {
15
+ return this.value;
16
+ }
17
+ }
18
+
5
19
  /*
6
20
  * Same as getResult, but pass context as argument to the expression.
7
21
  * Used to evaluate expressions in the context of a component.
@@ -9,7 +23,7 @@ import './Emitter.js';
9
23
  * @param {any} context The context to call the expression with.
10
24
  * @return {any} The result of the evaluated expression.
11
25
  */
12
- const renderExpression = (expression, context) => getResult(expression, context, context);
26
+ const getExpressionResult = (expression, context) => getResult(expression, context, context);
13
27
 
14
28
  /*
15
29
  * Generate string with placeholders for interpolated expressions.
@@ -44,10 +58,10 @@ const splitPlaceholders = (main, expressions) => {
44
58
  // so all the components are added as children by the parent component.
45
59
  while ((match = regExp.exec(main)) !== null) {
46
60
  const before = main.slice(lastIndex, match.index);
47
- out.push(before, expressions[match[1]]);
61
+ out.push(new SafeHTML(before), expressions[match[1]]);
48
62
  lastIndex = match.index + match[0].length;
49
63
  }
50
- out.push(main.slice(lastIndex));
64
+ out.push(new SafeHTML(main.slice(lastIndex)));
51
65
 
52
66
  return out;
53
67
  };
@@ -55,15 +69,15 @@ const splitPlaceholders = (main, expressions) => {
55
69
  /*
56
70
  * Expand attributes.
57
71
  * @param attributes {array} Array of attributes as key, value pairs.
58
- * @param renderExpression {function} Function to render expressions.
72
+ * @param getExpressionResult {function} Function to render expressions.
59
73
  * @return {object}
60
74
  * @property {object} all All attributes.
61
75
  * @property {object} events Event listeners.
62
76
  * @property {object} attributes Attributes.
63
77
  */
64
- const expandAttributes = (attributes, renderExpression) => {
78
+ const expandAttributes = (attributes, getExpressionResult) => {
65
79
  const out = attributes.reduce((out, pair) => {
66
- const attribute = renderExpression(pair[0]);
80
+ const attribute = getExpressionResult(pair[0]);
67
81
  // Attribute without value.
68
82
  if (pair.length === 1) {
69
83
  if (typeof attribute === 'object') {
@@ -75,7 +89,7 @@ const expandAttributes = (attributes, renderExpression) => {
75
89
  }
76
90
  } else {
77
91
  // Attribute with value.
78
- const value = renderExpression(pair[1]);
92
+ const value = getExpressionResult(pair[1]);
79
93
  out.all[attribute] = value;
80
94
  }
81
95
 
@@ -126,12 +140,12 @@ const expandComponents = (main, expressions) => {
126
140
  const list = splitPlaceholders(expandComponents(inner, expressions), expressions);
127
141
  // Create renderChildren function.
128
142
  renderChildren = function() {
129
- return list.map(item => renderExpression(item, this)).flat();
143
+ return deepFlat(list.map(item => getExpressionResult(item, this)));
130
144
  };
131
145
  }
132
146
  // Create mount function.
133
147
  const mount = function() {
134
- const options = expandAttributes(attributes, value => renderExpression(value, this)).all;
148
+ const options = expandAttributes(attributes, value => getExpressionResult(value, this)).all;
135
149
  // Add renderChildren function to options.
136
150
  if (renderChildren) options.renderChildren = renderChildren.bind(this);
137
151
  // Mount component.
@@ -195,6 +209,14 @@ const parseMatch = (match, expressions) => {
195
209
  return data;
196
210
  };
197
211
 
212
+ /*
213
+ * HTML tags that are self closing.
214
+ */
215
+ const selfClosingTags = {
216
+ area : true, base : true, br : true, col : true, embed : true, hr : true,
217
+ img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
218
+ };
219
+
198
220
  /*
199
221
  * These option keys will be extended on the component instance.
200
222
  */
@@ -206,26 +228,19 @@ const componentOptions = {
206
228
  onRender : true
207
229
  };
208
230
 
209
- /*
210
- * HTML tags that are self closing.
211
- */
212
- const selfClosingTags = {
213
- area : true, base : true, br : true, col : true, embed : true, hr : true,
214
- img : true, input : true, link : true, meta : true, source : true, track : true, wbr : true
215
- };
216
-
217
231
  /**
218
232
  * Components are a special kind of `View` that is designed to be easily composable,
219
233
  * making it simple to add child views and build complex user interfaces.
220
234
  * Unlike views, which are render-agnostic, components have a specific set of rendering
221
235
  * guidelines that allow for a more declarative development style.
222
- * Components are defined with the `create` static method, which takes a tagged template.
236
+ * Components are defined with the {@link #module_component_create Component.create} static method, which takes a tagged template string or a function that returns another component.
223
237
  * @module
224
238
  * @extends Rasti.View
225
239
  * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
226
240
  * @property {string} key A unique key to identify the component. Used to recycle child components.
227
241
  * @property {object} model A `Rasti.Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method.
228
242
  * @property {object} state A `Rasti.Model` or any emitter object containing data and business logic, to be used as internal state. The component will listen to `change` events and call `onChange` lifecycle method.
243
+ * @see {@link #module_component_create Component.create}
229
244
  * @example
230
245
  * import { Component, Model } from 'rasti';
231
246
  * // Create Timer component.
@@ -324,9 +339,11 @@ class Component extends View {
324
339
  const attributes = { [Component.DATA_ATTRIBUTE_UID] : this.uid };
325
340
 
326
341
  if (this.attributes) Object.assign(attributes, getResult(this.attributes, this));
342
+ // Store previous attributes.
343
+ const previousAttributes = this.previousAttributes || {};
344
+ this.previousAttributes = attributes;
327
345
 
328
346
  Object.keys(attributes).forEach(key => {
329
- // Evaluate attribute value.
330
347
  let value = attributes[key];
331
348
  // Transform bool attribute values
332
349
  if (value === false) {
@@ -338,7 +355,13 @@ class Component extends View {
338
355
  if (value === null || typeof value === 'undefined') value = '';
339
356
 
340
357
  add[key] = value;
341
- html.push(`${key}="${value}"`);
358
+ html.push(`${View.sanitize(key)}="${View.sanitize(value)}"`);
359
+ }
360
+ });
361
+ // Remove attributes that were in previousAttributes but not in current attributes.
362
+ Object.keys(previousAttributes).forEach(key => {
363
+ if (!(key in attributes)) {
364
+ remove[key] = true;
342
365
  }
343
366
  });
344
367
 
@@ -465,13 +488,11 @@ class Component extends View {
465
488
  * });
466
489
  */
467
490
  partial(strings, ...expressions) {
468
- return splitPlaceholders(
469
- expandComponents(
470
- addPlaceholders(strings, expressions), expressions
471
- ), expressions
472
- ).map(item => {
473
- return renderExpression(item, this);
474
- }).flat();
491
+ return deepFlat(
492
+ splitPlaceholders(
493
+ expandComponents(addPlaceholders(strings, expressions), expressions), expressions
494
+ ).map(item => getExpressionResult(item, this))
495
+ );
475
496
  }
476
497
 
477
498
  getRecyclePlaceholder() {
@@ -631,7 +652,6 @@ class Component extends View {
631
652
  * It instantiate the Component view using options,
632
653
  * appends its element into the DOM (if `el` is provided).
633
654
  * And returns the view instance.
634
- * <br><br> &#9888; **Security Notice:** `Component` utilizes `innerHTML` on a document fragment for rendering, which may introduce Cross - Site Scripting (XSS) risks. Ensure that any user-generated content is properly sanitized before inserting it into the DOM. For best practices on secure data handling, refer to the [OWASP's XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).<br><br>
635
655
  * @static
636
656
  * @param {object} options The view options.
637
657
  * @param {node} el Dom element to append the view element.
@@ -657,38 +677,96 @@ class Component extends View {
657
677
  }
658
678
 
659
679
  /**
660
- * Takes a tagged template containing an HTML string,
661
- * and returns a new `Component` class.
680
+ * Takes a tagged template string or a function that returns another component, and returns a new `Component` class.
662
681
  * - The template outer tag and attributes will be used to create the view's root element.
663
- * - Boolean attributes should be passed in the form of `attribute="${() => true}"`.
664
- * - Event handlers should be passed, at the root element, in the form of `onEventName=${{'selector' : listener }}`. Where `selector` is a CSS selector. The event will be delegated to the view's root element.
665
682
  * - The template inner HTML will be used as the view's template.
666
- * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it.
667
- * - If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
668
- * - If the function returns a component instance, it will be added as a child component.
669
- * - If the function returns an array, each item will be evaluated as above.
683
+ * ```javascript
684
+ * const Button = Component.create`<button class="button">Click me</button>`;
685
+ * ```
686
+ * - Template interpolations that are functions will be evaluated during the render process, receiving the view instance as an argument and being bound to it. If the function returns `null`, `undefined`, `false`, or an empty string, the interpolation won't render any content.
687
+ * ```javascript
688
+ * const Button = Component.create`
689
+ * <button class="${({ options }) => options.className}">
690
+ * ${({ options }) => options.renderChildren()}
691
+ * </button>
692
+ * `;
693
+ * ```
694
+ * - Event handlers should be passed, at the root element as camelized attributes, in the format `onEventName=${{'selector' : listener }}`. They will be transformed to an event object and delegated to the root element. See {@link #module_view__delegateevents View.delegateEvents}.
695
+ * - Boolean attributes should be passed in the form of `attribute="${() => true}"`. `false` attributes won't be rendered. `true` attributes will be rendered without a value.
696
+ * ```javascript
697
+ * const Input = Component.create`
698
+ * <input type="text" disabled=${({ options }) => options.disabled} />
699
+ * `;
700
+ * ```
701
+ * - If the interpolated function returns a component instance, it will be added as a child component.
702
+ * - If the interpolated function returns an array, each item will be evaluated as above.
703
+ * ```javascript
704
+ * // Create a button component.
705
+ * const Button = Component.create`
706
+ * <button class="button">
707
+ * ${({ options }) => options.renderChildren()}
708
+ * </button>
709
+ * `;
710
+ * // Create a navigation component. Add buttons as children. Iterate over items.
711
+ * const Navigation = Component.create`
712
+ * <nav>
713
+ * ${({ options }) => options.items.map(
714
+ * item => Button.mount({ renderChildren: () => item.label })
715
+ * )}
716
+ * </nav>
717
+ * `;
718
+ * // Create a header component. Add navigation as a child.
719
+ * const Header = Component.create`
720
+ * <header>
721
+ * ${({ options }) => Navigation.mount({ items : options.items})}
722
+ * </header>
723
+ * `;
724
+ * ```
725
+ * - Child components can be added using a component tag.
726
+ * ```javascript
727
+ * // Create a button component.
728
+ * const Button = Component.create`
729
+ * <button class="button">
730
+ * ${({ options }) => options.renderChildren()}
731
+ * </button>
732
+ * `;
733
+ * // Create a navigation component. Add buttons as children. Iterate over items.
734
+ * const Navigation = Component.create`
735
+ * <nav>
736
+ * ${self => self.options.items.map(
737
+ * item => self.partial`<${Button}>${item.label}</${Button}>`
738
+ * )}
739
+ * </nav>
740
+ * `;
741
+ * // Create a header component. Add navigation as a child.
742
+ * const Header = Component.create`
743
+ * <header>
744
+ * <${Navigation} items="${({ options }) => options.items}" />
745
+ * </header>
746
+ * `;
747
+ * ```
670
748
  * - If the tagged template contains only one expression that mounts a component, or the tags are references to a component, the component will be considered a <b>container</b>. It will render a single component as a child. `this.el` will be a reference to that child component's element.
749
+ * ```javascript
750
+ * // Create a button component.
751
+ * const Button = Component.create`
752
+ * <button class="${({ options }) => options.className}">
753
+ * ${self => self.renderChildren()}
754
+ * </button>
755
+ * `;
756
+ * // Create a container using the button component
757
+ * const ButtonOk = Component.create`
758
+ * <${Button} className="ok">Ok</${Button}>
759
+ * `;
760
+ * // Create a button component using a function
761
+ * const ButtonCancel = Component.create(() => Button.mount({
762
+ * className: 'cancel',
763
+ * renderChildren: () => 'Cancel'
764
+ * }));
765
+ * ```
671
766
  * @static
672
767
  * @param {string|function} strings - HTML template for the component or a function that mounts a sub component.
673
768
  * @param {...*} expressions - The expressions to be interpolated within the template.
674
769
  * @return {Rasti.Component} The newly created component class.
675
- * @example
676
- * import { Component } from 'rasti';
677
- * // Create a button component.
678
- * const Button = Component.create`
679
- * <button class="${({ options }) => options.className}">
680
- * ${self => self.renderChildren()}
681
- * </button>
682
- * `;
683
- * // Create a container using the button component
684
- * const ButtonOk = Component.create`
685
- * <${Button} className="ok">Ok</${Button}>
686
- * `;
687
- * // Create a button component using a function
688
- * const ButtonCancel = Component.create(() => Button.mount({
689
- * className: 'cancel',
690
- * renderChildren: () => 'Cancel'
691
- * }));
692
770
  */
693
771
  static create(strings, ...expressions) {
694
772
  const PH = Component.PLACEHOLDER_EXPRESSION('(\\d+)');
@@ -711,18 +789,18 @@ class Component extends View {
711
789
  const { tag : tagExpression, attributes : attributesAndEvents, inner, close } = parseMatch(match, expressions);
712
790
  // Get tag, attributes.
713
791
  tag = function() {
714
- return renderExpression(tagExpression, this);
792
+ return View.sanitize(getExpressionResult(tagExpression, this));
715
793
  };
716
794
  // Get attributes.
717
795
  attributes = function() {
718
- return expandAttributes(attributesAndEvents, value => renderExpression(value, this)).attributes;
796
+ return expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).attributes;
719
797
  };
720
798
  // Get events.
721
799
  events = function() {
722
- const onlyEvents = expandAttributes(attributesAndEvents, value => renderExpression(value, this)).events;
800
+ const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
723
801
 
724
802
  return Object.keys(onlyEvents).reduce((out, key) => {
725
- const typeListeners = renderExpression(onlyEvents[key], this);
803
+ const typeListeners = getExpressionResult(onlyEvents[key], this);
726
804
 
727
805
  Object.keys(typeListeners).forEach(selector => {
728
806
  out[`${key}${selector === '&' ? '' : ` ${selector}`}`] = typeListeners[selector];
@@ -735,9 +813,11 @@ class Component extends View {
735
813
  if (close) {
736
814
  const list = inner ? splitPlaceholders(inner, expressions) : [];
737
815
  template = function(addChild) {
738
- return list.map(item => renderExpression(item, this)).flat().map(item => {
816
+ return deepFlat(list.map(item => getExpressionResult(item, this))).map(item => {
739
817
  if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
740
- return item instanceof Component ? addChild(item) : item;
818
+ if (item instanceof SafeHTML) return item;
819
+ if (item instanceof Component) return addChild(item);
820
+ return View.sanitize(`${item}`);
741
821
  }
742
822
  return '';
743
823
  }).join('');
@@ -751,7 +831,7 @@ class Component extends View {
751
831
  // If there is only one expression and no tag, is a container.
752
832
  template = function(addChild) {
753
833
  // Replace expressions.
754
- return addChild(renderExpression(expressions[match[1]], this)).toString();
834
+ return addChild(getExpressionResult(expressions[match[1]], this)).toString();
755
835
  };
756
836
  } else {
757
837
  throw new SyntaxError('Invalid component');
package/es/Emitter.js CHANGED
@@ -41,7 +41,8 @@ class Emitter {
41
41
  * @param {string} type Type of the event (e.g. `change`).
42
42
  * @param {function} listener Callback function to be called when the event is emitted.
43
43
  * @example
44
- * this.model.on('change', this.render.bind(this)); // Re render when model changes.
44
+ * // Re render when model changes.
45
+ * this.model.on('change', this.render.bind(this));
45
46
  */
46
47
  on(type, listener) {
47
48
  // Validate listener.
@@ -62,6 +63,7 @@ class Emitter {
62
63
  * @param {string} type Type of the event (e.g. `change`).
63
64
  * @param {function} listener Callback function to be called when the event is emitted.
64
65
  * @example
66
+ * // Log a message once when model changes.
65
67
  * this.model.once('change', () => console.log('This will happen once'));
66
68
  */
67
69
  once(type, listener) {
@@ -84,7 +86,8 @@ class Emitter {
84
86
  * @param {string} [type] Type of the event (e.g. `change`). If is not provided, it removes all listeners.
85
87
  * @param {function} [listener] Callback function to be called when the event is emitted. If listener is not provided, it removes all listeners for specified type.
86
88
  * @example
87
- * this.model.off('change'); // Stop listening to changes.
89
+ * // Stop listening to changes.
90
+ * this.model.off('change');
88
91
  */
89
92
  off(type, listener) {
90
93
  // No listeners.
@@ -113,7 +116,8 @@ class Emitter {
113
116
  * @param {string} type Type of the event (e.g. `change`).
114
117
  * @param {any} [...args] Arguments to be passed to listener.
115
118
  * @example
116
- * this.emit('invalid'); // Emit validation error event.
119
+ * // Emit validation error event.
120
+ * this.emit('invalid');
117
121
  */
118
122
  emit(type, ...args) {
119
123
  // No listeners.
package/es/Model.js CHANGED
@@ -10,13 +10,13 @@ import getResult from './utils/getResult.js';
10
10
  * that contain all the necessary functions for manipulating their specific data.
11
11
  * Models should be easily passed throughout your app and used anywhere the corresponding data is needed.
12
12
  * Rasti models store their attributes in `this.attributes`, which is extended from `this.defaults` and the
13
- * constructor `attrs` parameter. For every attribute, a getter is generated to retrieve the model property
13
+ * constructor `attributes` parameter. For every attribute, a getter is generated to retrieve the model property
14
14
  * from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
15
15
  * and `change:attribute` events.
16
16
  * @module
17
17
  * @extends Rasti.Emitter
18
- * @param {object} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.
19
- * @property {object} defaults Object containing default attributes for the model. It will extend `this.attributes`. If a function is passed, it will be called to get the defaults. It will be bound to the model instance.
18
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributes`, in order to emit `change` events.
19
+ * @property {object|function} defaults Object containing default attributes for the model. It will extend `this.attributes`. If a function is passed, it will be called to get the defaults. It will be bound to the model instance.
20
20
  * @property {object} previous Object containing previous attributes when a change occurs.
21
21
  * @example
22
22
  * import { Model } from 'rasti';
@@ -49,14 +49,14 @@ import getResult from './utils/getResult.js';
49
49
  * product.setDiscount(10); // Output: "New Price: 900"
50
50
  */
51
51
  class Model extends Emitter {
52
- constructor(attrs = {}) {
52
+ constructor(attributes = {}) {
53
53
  super();
54
54
  // Call preinitialize.
55
55
  this.preinitialize.apply(this, arguments);
56
56
  // Get defaults. If `this.defaults` is a function, call it.
57
57
  const defaults = getResult(this.defaults, this) || {};
58
58
  // Set attributes object with defaults and passed attributes.
59
- this.attributes = Object.assign({}, defaults, attrs);
59
+ this.attributes = Object.assign({}, defaults, attributes);
60
60
  // Object to store previous attributes when a change occurs.
61
61
  this.previous = {};
62
62
  // Generate getters/setters for every attribute.
@@ -65,7 +65,7 @@ class Model extends Emitter {
65
65
 
66
66
  /**
67
67
  * If you define a preinitialize method, it will be invoked when the Model is first created, before any instantiation logic is run for the Model.
68
- * @param {object} attrs Object containing model attributes to extend `this.attributes`.
68
+ * @param {object} attributes Object containing model attributes to extend `this.attributes`.
69
69
  */
70
70
  preinitialize() {}
71
71