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/README.md +40 -65
- package/dist/rasti.js +237 -107
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +140 -60
- package/es/Emitter.js +7 -3
- package/es/Model.js +6 -6
- package/es/View.js +74 -38
- package/es/index.js +1 -0
- package/es/utils/deepFlat.js +12 -0
- package/lib/Component.cjs +140 -60
- package/lib/Emitter.cjs +7 -3
- package/lib/Model.cjs +6 -6
- package/lib/View.cjs +74 -38
- package/lib/index.cjs +1 -0
- package/lib/utils/deepFlat.cjs +14 -0
- package/package.json +2 -2
- package/src/Component.js +141 -61
- package/src/Emitter.js +7 -3
- package/src/Model.js +6 -6
- package/src/View.js +74 -38
- package/src/utils/deepFlat.js +12 -0
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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[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
|
|
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
|
|
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,
|
|
78
|
+
const expandAttributes = (attributes, getExpressionResult) => {
|
|
65
79
|
const out = attributes.reduce((out, pair) => {
|
|
66
|
-
const attribute =
|
|
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 =
|
|
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 =>
|
|
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 =>
|
|
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
|
|
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
|
|
469
|
-
|
|
470
|
-
addPlaceholders(strings, expressions), expressions
|
|
471
|
-
),
|
|
472
|
-
)
|
|
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> ⚠ **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
|
|
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
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* - If the function returns an
|
|
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
|
|
792
|
+
return View.sanitize(getExpressionResult(tagExpression, this));
|
|
715
793
|
};
|
|
716
794
|
// Get attributes.
|
|
717
795
|
attributes = function() {
|
|
718
|
-
return expandAttributes(attributesAndEvents, value =>
|
|
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 =>
|
|
800
|
+
const onlyEvents = expandAttributes(attributesAndEvents, value => getExpressionResult(value, this)).events;
|
|
723
801
|
|
|
724
802
|
return Object.keys(onlyEvents).reduce((out, key) => {
|
|
725
|
-
const typeListeners =
|
|
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 =>
|
|
816
|
+
return deepFlat(list.map(item => getExpressionResult(item, this))).map(item => {
|
|
739
817
|
if (typeof item !== 'undefined' && item !== null && item !== false && item !== true) {
|
|
740
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 `
|
|
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}
|
|
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(
|
|
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,
|
|
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}
|
|
68
|
+
* @param {object} attributes Object containing model attributes to extend `this.attributes`.
|
|
69
69
|
*/
|
|
70
70
|
preinitialize() {}
|
|
71
71
|
|