wrec 0.5.0 → 0.5.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 CHANGED
@@ -104,13 +104,15 @@ Here are the steps:
104
104
 
105
105
  1. Click the "-" and "+" buttons to verify that the component is working.
106
106
 
107
- ## More Detail
107
+ ## Boolean Attributes
108
108
 
109
109
  When the value of an attribute is a Boolean,
110
110
  wrec adds the attribute to the element with no value
111
111
  or removes the attribute from the element.
112
112
  This is commonly used for attributes like `disabled`.
113
113
 
114
+ ## Event Listeners
115
+
114
116
  To wire event listeners,
115
117
  Wrec looks for attributes whose name begins with "on".
116
118
  It assumes the remainder of the attribute name is an event name.
@@ -130,6 +132,8 @@ does not matter because Wrec lowercases the name.
130
132
  So the attributes in the previous examples
131
133
  can be replaced by `onClick="increment"`.
132
134
 
135
+ ## Reactivity
136
+
133
137
  Wrec supports reactivity.
134
138
  Attribute values and the text content of elements
135
139
  can refer to web component properties with the syntax `this.somePropertyName`.
@@ -155,6 +159,8 @@ whose value is an array of month names:
155
159
  <p>The month is ${DAYS[new Date().getDay()]}.</p>
156
160
  ```
157
161
 
162
+ ## Conditional and Iterative HTML Generation
163
+
158
164
  Wrec supports conditional and iterative generation of HTML.
159
165
  See `examples/temperature-eval.js` for an example of a web component
160
166
  that conditionally decides what to render based on an attribute value.
@@ -162,13 +168,7 @@ See `examples/radio-group.js` for an example of a web component
162
168
  that iterates over values in a comma-delimited attribute value
163
169
  to determine what to render.
164
170
 
165
- Data binding in Lit is not two-way like in wrec.
166
- A Lit component cannot simply pass one of its properties to
167
- a child Lit component and have the child can update the property.
168
- The child must dispatch custom events that
169
- the parent listens for so it can update its own state.
170
- For an example of this, see
171
- [wrec-compare](https://github.com/mvolkmann/lit-examples/blob/main/wrec-compare/binding-demo.ts).
171
+ ## Data Binding
172
172
 
173
173
  Wrec supports two-way data binding.
174
174
  See the example component binding-demo
@@ -187,6 +187,16 @@ When the property is updated,
187
187
  the displayed value of the form element is updated.
188
188
  For examples, see `examples/data-bind.js`.
189
189
 
190
+ Data binding in Lit is not two-way like in wrec.
191
+ A Lit component cannot simply pass one of its properties to
192
+ a child Lit component and have the child can update the property.
193
+ The child must dispatch custom events that
194
+ the parent listens for so it can update its own state.
195
+ For an example of this, see
196
+ [wrec-compare](https://github.com/mvolkmann/lit-examples/blob/main/wrec-compare/binding-demo.ts).
197
+
198
+ ## Form Submissions
199
+
190
200
  Web components that extend `Wrec` can contribute values to
191
201
  form submissions by adding the following line to their class definition.
192
202
  Wrec looks for this automatically does the rest of the work.
@@ -195,6 +205,22 @@ Wrec looks for this automatically does the rest of the work.
195
205
  static formAssociated = true;
196
206
  ```
197
207
 
208
+ ## Error Checking
209
+
210
+ Wrec checks for many kinds of errors and throws an `Error` when they are found.
211
+ Look for messages in the DevTools console.
212
+ The kinds of errors that are detected include:
213
+
214
+ - attribute names in web component instances
215
+ with no matching property declaration
216
+ - attribute values with a type that differs from the declared property type
217
+ - event handling function names that
218
+ don't match any method name in the web component
219
+ - expressions in attribute values or element text content
220
+ that reference undeclared web component properties
221
+ - expressions in element text content
222
+ that do not evaluate to a string or number
223
+
198
224
  ## Security
199
225
 
200
226
  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.2",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",