wrec 0.4.1 → 0.4.3

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
@@ -37,37 +37,38 @@ Here are the steps:
37
37
  ```
38
38
 
39
39
  1. Create the file `my-counter.js` containing the following.
40
- The comments `/*css*/` and `/*html*/` trigger the VS Code extension
41
- "es6-string-html" to add syntax highlighting to the CSS and HTML strings.
40
+ The tagged template literals with the tags `css` and `html` trigger the VS Code extension
41
+ "Prettier" to add syntax highlighting and format the CSS and HTML strings.
42
42
 
43
43
  ```js
44
- import Wrec from "wrec";
44
+ import Wrec, { css, html } from "wrec";
45
45
 
46
46
  class MyCounter extends Wrec {
47
47
  static properties = {
48
48
  count: { type: Number },
49
49
  };
50
50
 
51
- static css = /*css*/ `
51
+ static css = css`
52
52
  .counter {
53
53
  display: flex;
54
54
  align-items: center;
55
55
  gap: 0.5rem;
56
56
  }
57
-
57
+
58
58
  button {
59
59
  background-color: lightgreen;
60
60
  }
61
-
61
+
62
62
  button:disabled {
63
63
  background-color: gray;
64
64
  }
65
65
  `;
66
66
 
67
- static html = /*html*/ `
67
+ static html = html`
68
68
  <div>
69
- <button onClick="decrement" type="button"
70
- disabled="this.count === 0">-</button>
69
+ <button onClick="decrement" type="button" disabled="this.count === 0">
70
+ -
71
+ </button>
71
72
  <span>this.count</span>
72
73
  <button onClick="this.count++" type="button">+</button>
73
74
  <span>(this.count < 10 ? "single" : "double") + " digit"</span>
@@ -103,15 +104,21 @@ Here are the steps:
103
104
 
104
105
  ## More Detail
105
106
 
107
+ When the value of an attribute is a Boolean,
108
+ wrec adds the attribute to the element with no value
109
+ or removes the attribute from the element.
110
+ This is commonly used for attributes like `disabled`.
111
+
106
112
  To wire event listeners,
107
113
  Wrec looks for attributes whose name begins with "on".
108
114
  It assumes the remainder of the attribute name is an event name.
109
115
  It also assumes that the value of the attribute is either
110
116
  a method name that should be called or code that should be executed
111
117
  when that event is dispatched.
112
- For example, the attribute `onclick="increment"` causes Wrec to
118
+ For example, with the attribute `onclick="increment"`,
119
+ if `increment` is a method in the component, wrec will
113
120
  add an event listener to the element containing the attribute
114
- for "click" events and calls `this.increment(event)`.
121
+ for "click" events and call `this.increment(event)`.
115
122
  Alternatively, the attribute `onclick="this.count++"`
116
123
  adds an event listener that increments `this.count`
117
124
  when the element is clicked.
@@ -129,16 +136,31 @@ Only attribute values and text content
129
136
  that refer to modified web component properties are updated.
130
137
  Attribute values and text content that contain references to properties
131
138
  must be valid JavaScript expressions that are NOT surrounded by `${...}`.
132
- For an example of this kind of web component, see `demo/hello-world.js`.
139
+ For an example of this kind of web component, see `examples/hello-world.js`.
140
+
141
+ Wrec evaluates JavaScript expressions in the context of a web component instance
142
+ which can be referred to with the `this` keyword in the expressions.
133
143
 
134
144
  Wrec supports conditional and iterative generation of HTML.
135
- See `demo/temperature-eval.js` for an example of a web component
145
+ See `examples/temperature-eval.js` for an example of a web component
136
146
  that conditionally decides what to render based on an attribute value.
137
- See `demo/radio-group.js` for an example of a web component
147
+ See `examples/radio-group.js` for an example of a web component
138
148
  that iterates over values in a comma-delimited attribute value
139
149
  to determine what to render.
140
150
 
141
- Wrec supports two-way data binding for HTML form elements.
151
+ Data binding in Lit is not two-way like in wrec.
152
+ A Lit component cannot simply pass one of its properties to
153
+ a child Lit component and have the child can update the property.
154
+ The child must dispatch custom events that
155
+ the parent listens for so it can update its own state.
156
+ For an example of this, see
157
+ [wrec-compare](https://github.com/mvolkmann/lit-examples/blob/main/wrec-compare/binding-demo.ts).
158
+
159
+ Wrec supports two-way data binding.
160
+ See the example component binding-demo
161
+ and the components it renders.
162
+
163
+ Wrec two-way data binding can be used with HTML form elements.
142
164
 
143
165
  - `input` and `select` elements can have a `value` attribute
144
166
  whose value is "this.somePropertyName".
@@ -149,7 +171,7 @@ In all these cases, if the user changes the value of the form element,
149
171
  the specified property is updated.
150
172
  When the property is updated,
151
173
  the displayed value of the form element is updated.
152
- For examples, see `demo/data-bind.js`.
174
+ For examples, see `examples/data-bind.js`.
153
175
 
154
176
  Web components that extend `Wrec` can contribute values to
155
177
  form submissions by adding the following line to their class definition.
@@ -184,7 +206,7 @@ to any domain except that of your web app:
184
206
 
185
207
  ## More Examples
186
208
 
187
- Check out the web app in the `demo` directory.
209
+ Check out the web app in the `examples` directory.
188
210
  To run it, cd to that directory, enter `npm install`,
189
211
  enter `npm run dev`, and browse localhost:5173.
190
212
 
@@ -194,5 +216,17 @@ The next two uses the Wrec library.
194
216
  Compare the files `counter-vanilla.js` and `counter-wrec.js`
195
217
  to see how much using Wrec simplifies the code.
196
218
 
197
- The `demo` app renders several other web components that are built with wrec.
219
+ The `examples` app renders several other
220
+ web components that are built with wrec.
198
221
  Examine their code for more examples of wrec usage.
222
+
223
+ ## Tests
224
+
225
+ wrec has an extensive set of Playwright tests.
226
+ To run them:
227
+
228
+ 1. Clone the wrec repository.
229
+ 1. cd to the `examples` directory.
230
+ 1. Enter `npm install`.
231
+ 1. Enter `npm run testui`.
232
+ 1. Click the right pointing triangle.
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;#o=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.#n(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.#o.get(e);r||(r=[],this.#o.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.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;this["_"+t]=r,Object.defineProperty(this,t,{enumerable:!0,get(){return this["_"+t]},set(e){if(e===this["_"+t])return;if(this["_"+t]=e,this.hasAttribute(t)){e!==this.#p(t)&&this.#m(this,t,e)}this.#d(t);const s=this.propertyToParentPropertyMap,r=s?s.get(t):null;if(r){this.getRootNode().host.setAttribute(r,e)}this.#i(t,e)}})}#f(t){const e=t.localName.includes("-");for(const s of t.getAttributeNames()){const r=t.getAttribute(s);if(REFERENCE_RE.test(r)){const o=r.substring(SKIP);if(t[o]=this[o],"value"===s&&this.#a(t,o,s),e){let e=t.propertyToParentPropertyMap;e||(e=new Map,t.propertyToParentPropertyMap=e),e.set(s,o)}}this.#E(r,t,s)}}#b(expression){return(()=>eval(expression)).call(this)}#g(t){const{localName:e}=t;if("style"===e)return;const s=t.textContent.trim();if("textarea"===e&&REFERENCE_RE.test(s)){const e=s.substring(SKIP);this.#a(t,e),t.textContent=this[e]}else this.#E(s,t)}#p(t){return this.#n(t,this.getAttribute(t))}#n(t,e){const s=this.constructor.properties[t].type;return s===Number?Number(e):s===Boolean?Boolean(e):e}#h(t){const e=t.querySelectorAll("*");for(const t of e)this.#f(t),t.firstElementChild||this.#g(t)}static get observedAttributes(){return Object.keys(this.properties||{})}#d(t){const e=this.constructor["#propertyToExpressionsMap"].get(t)||[];for(const t of e){const e=this.#b(t),s=this.#e.get(t)||[];for(const t of s)if(t instanceof Element)this.#R(t,e);else{const{element:s,attrName:r}=t;this.#m(s,r,e)}}requestAnimationFrame(()=>{this.#A(t)})}static register(){const t=Wrec.#v(this.name);customElements.get(t)||customElements.define(t,this)}#E(t,e,s){const r=t.match(REFERENCES_RE);if(!r)return;this.constructor.processed||r.forEach(e=>{const s=e.substring(SKIP),r=this.constructor["#propertyToExpressionsMap"];let o=r.get(s);o||(o=[],r.set(s,o)),o.push(t)});let o=this.#e.get(t);o||(o=[],this.#e.set(t,o)),o.push(s?{element:e,attrName:s}:e);const n=this.#b(t);s?this.#m(e,s,n):this.#R(e,n)}static#v=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))}#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.#o.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}}#R(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}#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,fn="function"==typeof this[value]?t=>this[value](t):()=>eval(value);element.addEventListener(eventName,fn),element.removeAttribute(name)}}}}export default Wrec;
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;#o=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.#n(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.#o.get(e);r||(r=[],this.#o.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.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.#p(s,r,e)}#p(t,e,s){const r=s.includes(t)&&this.hasAttribute(t)?this.#u(t):e.value,o="#"+t;this[o]=r,Object.defineProperty(this,t,{enumerable:!0,get(){return this[o]},set(e){if(e===this[o])return;if(this[o]=e,this.hasAttribute(t)){e!==this.#u(t)&&this.#m(this,t,e)}this.#d(t);const s=this.propertyToParentPropertyMap,r=s?s.get(t):null;if(r){this.getRootNode().host.setAttribute(r,e)}this.#i(t,e)}})}#f(t){const e=t.localName.includes("-");for(const s of t.getAttributeNames()){const r=t.getAttribute(s);if(REFERENCE_RE.test(r)){const o=r.substring(SKIP),n=this[o];if(!n){const t=this.constructor.name;throw new Error(`component ${t} missing property "${o}"`)}if(t[o]=n,"value"===s&&this.#a(t,o,s),e){let e=t.propertyToParentPropertyMap;e||(e=new Map,t.propertyToParentPropertyMap=e),e.set(s,o)}}this.#E(r,t,s)}}#b(expression){return(()=>eval(expression)).call(this)}#g(t){const{localName:e}=t;if("style"===e)return;const s=t.textContent.trim();if("textarea"===e&&REFERENCE_RE.test(s)){const e=s.substring(SKIP);this.#a(t,e),t.textContent=this[e]}else this.#E(s,t)}#u(t){return this.#n(t,this.getAttribute(t))}#n(t,e){const s=this.constructor.properties[t].type;return s===Number?Number(e):s===Boolean?Boolean(e):e}#h(t){const e=t.querySelectorAll("*");for(const t of e)this.#f(t),t.firstElementChild||this.#g(t)}static get observedAttributes(){return Object.keys(this.properties||{})}#d(t){const e=this.constructor["#propertyToExpressionsMap"].get(t)||[];for(const t of e){const e=this.#b(t),s=this.#e.get(t)||[];for(const t of s)if(t instanceof Element)this.#R(t,e);else{const{element:s,attrName:r}=t;this.#m(s,r,e)}}requestAnimationFrame(()=>{this.#A(t)})}static register(){const t=Wrec.#y(this.name);customElements.get(t)||customElements.define(t,this)}#E(t,e,s){const r=t.match(REFERENCES_RE);if(!r)return;this.constructor.processed||r.forEach(e=>{const s=e.substring(SKIP),r=this.constructor["#propertyToExpressionsMap"];let o=r.get(s);o||(o=[],r.set(s,o)),o.push(t)});let o=this.#e.get(t);o||(o=[],this.#e.set(t,o)),o.push(s?{element:e,attrName:s}:e);const n=this.#b(t);s?this.#m(e,s,n):this.#R(e,n)}static#y=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))}#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.#o.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}}#R(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}#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,fn="function"==typeof this[value]?t=>this[value](t):()=>eval(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.4.1",
5
+ "version": "0.4.3",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",