wrec 0.5.0 → 0.5.1

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 CHANGED
@@ -195,6 +195,22 @@ Wrec looks for this automatically does the rest of the work.
195
195
  static formAssociated = true;
196
196
  ```
197
197
 
198
+ ## Error Checking
199
+
200
+ Wrec checks for many kinds of errors and throws an `Error` when they are found.
201
+ Look for messages in the DevTools console.
202
+ The kinds of errors that are detected include:
203
+
204
+ - attribute names in web component instances
205
+ with no matching property declaration
206
+ - attribute values with a type that differs from the declared property type
207
+ - event handling function names that
208
+ don't match any method name in the web component
209
+ - expressions in attribute values or element text content
210
+ that reference undeclared web component properties
211
+ - expressions in element text content
212
+ that do not evaluate to a string or number
213
+
198
214
  ## Security
199
215
 
200
216
  Wrec uses the JavaScript `eval` function to evaluate JavaScript expressions
package/dist/wrec.min.js CHANGED
@@ -1 +1 @@
1
- const FIRST_CHAR="a-zA-Z_$",OTHER_CHAR=FIRST_CHAR+"0-9",IDENTIFIER=`[${FIRST_CHAR}][${OTHER_CHAR}]*`,REFERENCE_RE=new RegExp(`^this.${IDENTIFIER}$`),REFERENCES_RE=new RegExp(`this.${IDENTIFIER}`,"g"),SKIP=5;class Wrec extends HTMLElement{static#t=document.createElement("template");#e=new Map;#s;#r;#n=new Map;constructor(){super(),this.attachShadow({mode:"open"});this.constructor["#propertyToExpressionsMap"]||(this.constructor["#propertyToExpressionsMap"]=new Map),this.constructor.formAssociated&&(this.#r=this.attachInternals(),this.#s=new FormData,this.#r.setFormValue(this.#s))}attributeChangedCallback(t,e,s){const r=this.#o(t,s);this[t]=r,this.#i(t,r)}#a(t,e,s){t.addEventListener("input",t=>{this[e]=t.target.value});let r=this.#n.get(e);r||(r=[],this.#n.set(e,r)),r.push(s?{element:t,attrName:s}:t)}buildDOM(){const t=this.constructor;let e=t.css?`<style>${t.css}</style>`:"";e+=t.html,Wrec.#t.innerHTML=e,this.shadowRoot.replaceChildren(Wrec.#t.content.cloneNode(!0))}componentName(){return this.constructor.name}connectedCallback(){this.#c(),this.buildDOM(),requestAnimationFrame(()=>{this.#l(this.shadowRoot),this.#h(this.shadowRoot),this.constructor.processed=!0})}#c(){const t=this.constructor.properties,{observedAttributes:e}=this.constructor;for(const[s,r]of Object.entries(t))this.#u(s,r,e)}#u(t,e,s){const r=s.includes(t)&&this.hasAttribute(t)?this.#p(t):e.value,n="#"+t;this[n]=r,Object.defineProperty(this,t,{enumerable:!0,get(){return this[n]},set(e){if(e===this[n])return;if(this[n]=e,this.hasAttribute(t)){e!==this.#p(t)&&this.#m(this,t,e)}this.#f(t);const s=this.propertyToParentPropertyMap,r=s?s.get(t):null;if(r){this.getRootNode().host.setAttribute(r,e)}this.#i(t,e)}})}#d(t){const e=t.localName.includes("-");for(const s of t.getAttributeNames()){const r=t.getAttribute(s),n=this.#E(t,r);if(n){const r=this[n];if(r||this.#b(t,s,n),t[n]=r,"value"===s&&this.#a(t,n,s),e){let e=t.propertyToParentPropertyMap;e||(e=new Map,t.propertyToParentPropertyMap=e),e.set(s,n)}}this.#R(r,t,s)}}#v(expression){return(()=>eval(expression)).call(this)}#g(t){const{localName:e}=t;if("style"===e)return;const s=t.textContent.trim(),r=this.#E(t,s);"textarea"===e&&r?(this.#a(t,r),t.textContent=this[r]):this.#R(s,t)}#h(t){const e=t.querySelectorAll("*");for(const t of e)this.#d(t),t.firstElementChild||this.#g(t)}static get observedAttributes(){return Object.keys(this.properties||{})}#E(t,e){if(!REFERENCE_RE.test(e))return;const s=e.substring(SKIP);return this[s]||this.#b(t,null,s),s}#f(t){const e=this.constructor["#propertyToExpressionsMap"].get(t)||[];for(const t of e){const e=this.#v(t),s=this.#e.get(t)||[];for(const t of s)if(t instanceof Element)this.#y(t,e);else{const{element:s,attrName:r}=t;this.#m(s,r,e)}}requestAnimationFrame(()=>{this.#A(t)})}static register(){const t=Wrec.#x(this.name);customElements.get(t)||customElements.define(t,this)}#R(t,e,s){const r=this.#N(e,s,t);if(!r)return;this.constructor.processed||r.forEach(e=>{const s=e.substring(SKIP),r=this.constructor["#propertyToExpressionsMap"];let n=r.get(s);n||(n=[],r.set(s,n)),n.push(t)});let n=this.#e.get(t);n||(n=[],this.#e.set(t,n)),n.push(s?{element:e,attrName:s}:e);const o=this.#v(t);s?this.#m(e,s,o):this.#y(e,o)}static#x=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();#i(t,e){this.#s&&(this.#s.set(t,e),this.#r.setFormValue(this.#s))}#b(t,e,s){throw new Error(`component ${this.componentName()}, element "${t.localName}"`+(e?`, attribute "${e}"`:"")+` refers to missing property "${s}"`)}#p(t){return this.#o(t,this.getAttribute(t))}#o(t,e){if(e?.match(REFERENCES_RE))return e;const s=this.constructor.properties[t].type;if(s===Number){const s=Number(e);if(!isNaN(s))return s;throw new Error(`attribute/property "${t}" must be a number, but was "${e}"`)}if(s===Boolean&&e&&e!==t)throw new Error("Boolean attribute value must match attribute name or be missing");return e}#m(t,e,s){const r=t.getAttribute(e);"boolean"==typeof s?s?r!==e&&t.setAttribute(e,e):t.removeAttribute(e):r!==s&&t.setAttribute(e,s)}#A(t){const e=this[t],s=this.#n.get(t)||[];for(const t of s)if(t instanceof Element)"textarea"===t.localName?t.value=e:t.textContent=e;else{const{element:s,attrName:r}=t;this.#m(s,r,e),s[r]=e}}#y(t,e){const{localName:s}=t;"textarea"===s?t.value=e:"string"==typeof e&&e.trim().startsWith("<")?(t.innerHTML=e,this.#l(t),this.#h(t)):t.textContent=e}#N(t,e,s){const r=s.match(REFERENCES_RE);if(r)return r.forEach(s=>{const r=s.substring(SKIP);this[r]||this.#b(t,e,r)}),r}#l(root){const elements=root.querySelectorAll("*");for(const element of elements)for(const attr of element.attributes){const{name:name}=attr;if(name.startsWith("on")){const eventName=name.slice(2).toLowerCase(),{value:value}=attr;let fn;if(this.#N(element,name,value),"function"==typeof this[value])fn=t=>this[value](t);else{const matches=this.#N(element,name,value);matches?fn=()=>eval(value):this.#b(element,name,value)}element.addEventListener(eventName,fn),element.removeAttribute(name)}}}}export default Wrec;export const css=String.raw;export const html=String.raw;
1
+ const FIRST_CHAR="a-zA-Z_$",OTHER_CHAR=FIRST_CHAR+"0-9",IDENTIFIER=`[${FIRST_CHAR}][${OTHER_CHAR}]*`,REFERENCE_RE=new RegExp(`^this.${IDENTIFIER}$`),REFERENCES_RE=new RegExp(`this.${IDENTIFIER}`,"g"),SKIP=5;class Wrec extends HTMLElement{static#t=document.createElement("template");#e=new Map;#s;#r;#n=new Map;constructor(){super(),this.attachShadow({mode:"open"});this.constructor["#propertyToExpressionsMap"]||(this.constructor["#propertyToExpressionsMap"]=new Map),this.constructor.formAssociated&&(this.#r=this.attachInternals(),this.#s=new FormData,this.#r.setFormValue(this.#s))}attributeChangedCallback(t,e,s){const r=this.#i(t,s);this[t]=r,this.#o(t,r)}#a(t,e,s){t.addEventListener("input",t=>{this[e]=t.target.value});let r=this.#n.get(e);r||(r=[],this.#n.set(e,r)),r.push(s?{element:t,attrName:s}:t)}buildDOM(){const t=this.constructor;let e=t.css?`<style>${t.css}</style>`:"";e+=t.html,Wrec.#t.innerHTML=e,this.shadowRoot.replaceChildren(Wrec.#t.content.cloneNode(!0))}connectedCallback(){this.#c(),this.#l(),this.buildDOM(),requestAnimationFrame(()=>{this.#h(this.shadowRoot),this.#u(this.shadowRoot),this.constructor.processed=!0})}#l(){const t=this.constructor.properties,{observedAttributes:e}=this.constructor;for(const[s,r]of Object.entries(t))this.#p(s,r,e)}#p(t,e,s){const r=s.includes(t)&&this.hasAttribute(t)?this.#m(t):e.value,n="#"+t;this[n]=r,Object.defineProperty(this,t,{enumerable:!0,get(){return this[n]},set(e){if(e===this[n])return;if(this[n]=e,this.hasAttribute(t)){e!==this.#m(t)&&this.#d(this,t,e)}this.#f(t);const s=this.propertyToParentPropertyMap,r=s?s.get(t):null;if(r){this.getRootNode().host.setAttribute(r,e)}this.#o(t,e)}})}static elementName(){return this.name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}#E(t){const e=t.localName.includes("-");for(const s of t.getAttributeNames()){const r=t.getAttribute(s),n=this.#b(t,r);if(n){const r=this[n];if(r||this.#v(t,s,n),t[n]=r,"value"===s&&this.#a(t,n,s),e){let e=t.propertyToParentPropertyMap;e||(e=new Map,t.propertyToParentPropertyMap=e),e.set(s,n)}}this.#R(r,t,s)}}#g(expression){return(()=>eval(expression)).call(this)}#y(t){const{localName:e}=t;if("style"===e)return;const s=t.textContent.trim(),r=this.#b(t,s);"textarea"===e&&r?(this.#a(t,r),t.textContent=this[r]):this.#R(s,t)}#u(t){const e=t.querySelectorAll("*");for(const t of e)this.#E(t),t.firstElementChild||this.#y(t)}static get observedAttributes(){return Object.keys(this.properties||{})}#b(t,e){if(!REFERENCE_RE.test(e))return;const s=e.substring(SKIP);return this[s]||this.#v(t,null,s),s}#f(t){const e=this.constructor["#propertyToExpressionsMap"].get(t)||[];for(const t of e){const e=this.#g(t),s=this.#e.get(t)||[];for(const t of s)if(t instanceof Element)this.#A(t,e);else{const{element:s,attrName:r}=t;this.#d(s,r,e)}}requestAnimationFrame(()=>{this.#w(t)})}static register(){const t=this.elementName();customElements.get(t)||customElements.define(t,this)}#R(t,e,s){const r=this.#N(e,s,t);if(!r)return;this.constructor.processed||r.forEach(e=>{const s=e.substring(SKIP),r=this.constructor["#propertyToExpressionsMap"];let n=r.get(s);n||(n=[],r.set(s,n)),n.push(t)});let n=this.#e.get(t);n||(n=[],this.#e.set(t,n)),n.push(s?{element:e,attrName:s}:e);const i=this.#g(t);s?this.#d(e,s,i):this.#A(e,i)}#o(t,e){this.#s&&(this.#s.set(t,e),this.#r.setFormValue(this.#s))}#x(t,e,s){throw new Error(`component ${this.constructor.elementName()}`+(t?`, element "${t.localName}"`:"")+(e?`, attribute "${e}"`:"")+` ${s}`)}#v(t,e,s){this.#x(t,e,`refers to missing property "${s}"`)}#m(t){return this.#i(t,this.getAttribute(t))}#i(t,e){if(e?.match(REFERENCES_RE))return e;const s=this.constructor.properties[t].type;if(s===Number){const s=Number(e);if(!isNaN(s))return s;this.#x(null,t,`must be a number, but was "${e}"`)}return s===Boolean&&e&&e!==t&&this.#x(null,t,"is a Boolean attribute, so its value must match attribute name or be missing"),e}#d(t,e,s){const r=t.getAttribute(e);"boolean"==typeof s?s?r!==e&&t.setAttribute(e,e):t.removeAttribute(e):r!==s&&t.setAttribute(e,s)}#w(t){const e=this[t],s=this.#n.get(t)||[];for(const t of s)if(t instanceof Element)"textarea"===t.localName?t.value=e:t.textContent=e;else{const{element:s,attrName:r}=t;this.#d(s,r,e),s[r]=e}}#A(t,e){const{localName:s}=t,r=typeof e;"string"!==r&&"number"!==r&&this.#x(t,null," computed content is not a string or number"),"textarea"===s?t.value=e:"string"===r&&e.trim().startsWith("<")?(t.innerHTML=e,this.#h(t),this.#u(t)):t.textContent=e}#c(){const t=new Set(Object.keys(this.constructor.properties));for(const e of this.getAttributeNames())t.has(e)||this.#x(null,e,"is not a supported attribute")}#N(t,e,s){const r=s.match(REFERENCES_RE);if(r)return r.forEach(s=>{const r=s.substring(SKIP);void 0===this[r]&&this.#v(t,e,r)}),r}#h(root){const elements=root.querySelectorAll("*");for(const element of elements)for(const attr of element.attributes){const{name:name}=attr;if(name.startsWith("on")){const eventName=name.slice(2).toLowerCase(),{value:value}=attr;let fn;if(this.#N(element,name,value),"function"==typeof this[value])fn=t=>this[value](t);else{const matches=this.#N(element,name,value);matches?fn=()=>eval(value):this.#v(element,name,value)}element.addEventListener(eventName,fn),element.removeAttribute(name)}}}}export default Wrec;export const css=String.raw;export const html=String.raw;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a small library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.5.0",
5
+ "version": "0.5.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",