wrec 0.7.0 → 0.8.0
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 +4 -232
- package/dist/wrec.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,23 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Wrec is a small, zero dependency library that
|
|
6
6
|
greatly simplifies building web components.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Wrec was inspired by [Lit](https://lit.dev).
|
|
11
|
-
It has the following advantages over Lit:
|
|
12
|
-
|
|
13
|
-
- Wrec is simpler ... just a single class to extend (Wrec).
|
|
14
|
-
- Wrec is slightly smaller ... 4K versus 5.8K minified.
|
|
15
|
-
- Wrec has a cleaner syntax ... no need to
|
|
16
|
-
surround JS expressions with `${...}`.
|
|
17
|
-
- Wrec provides automatic 2-way data binding ...
|
|
18
|
-
no need to dispatch custom events and listen for them.
|
|
19
|
-
- Wrec doesn't require a special syntax for Boolean attributes.
|
|
20
|
-
- Wrec enables specifying the content of a `textarea` element
|
|
21
|
-
with a JavaScript expressions in its text content.
|
|
22
|
-
|
|
23
|
-
Wrec components have many of the features provided by Alpine.js.
|
|
7
|
+
It is described in detail, with several working examples,
|
|
8
|
+
in [my blog](https://mvolkmann.github.io/blog/wrec/).
|
|
24
9
|
|
|
25
10
|
## Getting Started
|
|
26
11
|
|
|
@@ -47,11 +32,11 @@ Here are the steps:
|
|
|
47
32
|
"Prettier" to add syntax highlighting and format the CSS and HTML strings.
|
|
48
33
|
|
|
49
34
|
```js
|
|
50
|
-
import Wrec, {
|
|
35
|
+
import Wrec, {css, html} from 'wrec';
|
|
51
36
|
|
|
52
37
|
class MyCounter extends Wrec {
|
|
53
38
|
static properties = {
|
|
54
|
-
count: {
|
|
39
|
+
count: {type: Number}
|
|
55
40
|
};
|
|
56
41
|
|
|
57
42
|
static css = css`
|
|
@@ -106,144 +91,8 @@ Here are the steps:
|
|
|
106
91
|
|
|
107
92
|
1. Click the "-" and "+" buttons to verify that the component is working.
|
|
108
93
|
|
|
109
|
-
## Reactivity
|
|
110
|
-
|
|
111
|
-
Wrec supports reactivity.
|
|
112
|
-
Attribute values and the text content of elements
|
|
113
|
-
can refer to web component properties with the syntax `this.somePropertyName`.
|
|
114
|
-
In this context, `this` always refers to the parent web component.
|
|
115
|
-
The DOM of the web component is surgically updated.
|
|
116
|
-
Only attribute values and text content
|
|
117
|
-
that refer to modified web component properties are updated.
|
|
118
|
-
Attribute values and text content that contain references to properties
|
|
119
|
-
must be valid JavaScript expressions that are NOT surrounded by `${...}`.
|
|
120
|
-
For an example of this kind of web component, see `examples/hello-world.js`.
|
|
121
|
-
|
|
122
|
-
Wrec evaluates JavaScript expressions in the context of a web component instance
|
|
123
|
-
which can be referred to with the `this` keyword in the expressions.
|
|
124
|
-
|
|
125
|
-
To render "this." followed by a valid JavaScript identifier without
|
|
126
|
-
replacing it with the value of the expression, escaping with a double period.
|
|
127
|
-
For example, `this.count` will be reactively replaced with its value,
|
|
128
|
-
but `this..count` will render the text with a single period.
|
|
129
|
-
|
|
130
|
-
In insert the value of an expression
|
|
131
|
-
that does not use properties of the web component,
|
|
132
|
-
into an HTML template string,
|
|
133
|
-
surround the expression with the syntax `${...}`.
|
|
134
|
-
For example, assuming `DAYS` is a variable
|
|
135
|
-
whose value is an array of month names:
|
|
136
|
-
|
|
137
|
-
```html
|
|
138
|
-
<p>The month is ${DAYS[new Date().getDay()]}.</p>
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## Reactive CSS
|
|
142
|
-
|
|
143
|
-
CSS property values cannot refer to web component properties
|
|
144
|
-
with the syntax `this.somePropertyName`.
|
|
145
|
-
This is because the CSS parser in browsers removes invalid property values.
|
|
146
|
-
A workaround is to place such references in a CSS variable
|
|
147
|
-
and refer to that variable from a CSS property.
|
|
148
|
-
For an example of this, see `examples/data-binding.js`
|
|
149
|
-
which contains the following CSS:
|
|
150
|
-
|
|
151
|
-
```css
|
|
152
|
-
p {
|
|
153
|
-
--color: this.color;
|
|
154
|
-
--size: this.size;
|
|
155
|
-
color: var(--color);
|
|
156
|
-
font-size: calc(var(--size) * 1px);
|
|
157
|
-
margin: 6px 0;
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
CSS variable values can be any valid JavaScript expression.
|
|
162
|
-
The example above can be changed to double the size as follows:
|
|
163
|
-
|
|
164
|
-
```css
|
|
165
|
-
--size: this.size * 2;
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## Required Attributes
|
|
169
|
-
|
|
170
|
-
The configuration object for required attributes in a Wrec component
|
|
171
|
-
should contain `required: true`.
|
|
172
|
-
For an example of this, see the `name` property in `examples/radio-group.js`.
|
|
173
|
-
|
|
174
|
-
## Boolean Attributes
|
|
175
|
-
|
|
176
|
-
When the value of an attribute is a Boolean,
|
|
177
|
-
wrec adds the attribute to the element with no value
|
|
178
|
-
or removes the attribute from the element.
|
|
179
|
-
This is commonly used for attributes like `disabled`.
|
|
180
|
-
|
|
181
|
-
## Event Listeners
|
|
182
|
-
|
|
183
|
-
To wire event listeners,
|
|
184
|
-
Wrec looks for attributes whose name begins with "on".
|
|
185
|
-
It assumes the remainder of the attribute name is an event name.
|
|
186
|
-
It also assumes that the value of the attribute is either
|
|
187
|
-
a method name that should be called or code that should be executed
|
|
188
|
-
when that event is dispatched.
|
|
189
|
-
For example, with the attribute `onclick="increment"`,
|
|
190
|
-
if `increment` is a method in the component, wrec will
|
|
191
|
-
add an event listener to the element containing the attribute
|
|
192
|
-
for "click" events and call `this.increment(event)`.
|
|
193
|
-
Alternatively, the attribute `onclick="this.count++"`
|
|
194
|
-
adds an event listener that increments `this.count`
|
|
195
|
-
when the element is clicked.
|
|
196
|
-
|
|
197
|
-
The case of the event name within the attribute name
|
|
198
|
-
does not matter because Wrec lowercases the name.
|
|
199
|
-
So the attributes in the previous examples
|
|
200
|
-
can be replaced by `onClick="increment"`.
|
|
201
|
-
|
|
202
|
-
## Property Change Events
|
|
203
|
-
|
|
204
|
-
Wrec components will dispatch change events whenever
|
|
205
|
-
a property configured with `dispatch: true` changes.
|
|
206
|
-
For an example of this,
|
|
207
|
-
see the `checked` property in `examples/toggle-switch.js`.
|
|
208
|
-
The component defined in `examples/binding-demo.js`
|
|
209
|
-
listens for that event, as does the `script` in `examples/index.html`.
|
|
210
|
-
|
|
211
|
-
## Conditional and Iterative HTML Generation
|
|
212
|
-
|
|
213
|
-
Wrec supports conditional and iterative generation of HTML.
|
|
214
|
-
See `examples/temperature-eval.js` for an example of a web component
|
|
215
|
-
that conditionally decides what to render based on an attribute value.
|
|
216
|
-
See `examples/radio-group.js` for an example of a web component
|
|
217
|
-
that iterates over values in a comma-delimited attribute value
|
|
218
|
-
to determine what to render.
|
|
219
|
-
|
|
220
94
|
## Data Binding
|
|
221
95
|
|
|
222
|
-
Wrec supports two-way data binding.
|
|
223
|
-
See the example component binding-demo
|
|
224
|
-
and the components it renders.
|
|
225
|
-
|
|
226
|
-
Wrec two-way data binding can be used with HTML form elements.
|
|
227
|
-
|
|
228
|
-
- `input` and `select` elements can have a `value` attribute
|
|
229
|
-
whose value is "this.somePropertyName".
|
|
230
|
-
- `textarea` elements can have text content
|
|
231
|
-
that is "this.somePropertyName"
|
|
232
|
-
|
|
233
|
-
In all these cases, if the user changes the value of the form element,
|
|
234
|
-
the specified property is updated.
|
|
235
|
-
When the property is updated,
|
|
236
|
-
the displayed value of the form element is updated.
|
|
237
|
-
For examples, see `examples/data-bind.js`.
|
|
238
|
-
|
|
239
|
-
Data binding in Lit is not two-way like in wrec.
|
|
240
|
-
A Lit component cannot simply pass one of its properties to
|
|
241
|
-
a child Lit component and have the child can update the property.
|
|
242
|
-
The child must dispatch custom events that
|
|
243
|
-
the parent listens for so it can update its own state.
|
|
244
|
-
For an example of this, see
|
|
245
|
-
[wrec-compare](https://github.com/mvolkmann/lit-examples/blob/main/wrec-compare/binding-demo.ts).
|
|
246
|
-
|
|
247
96
|
## Form Submissions
|
|
248
97
|
|
|
249
98
|
Web components that extend `Wrec` can contribute values to
|
|
@@ -253,80 +102,3 @@ Wrec looks for this automatically does the rest of the work.
|
|
|
253
102
|
```js
|
|
254
103
|
static formAssociated = true;
|
|
255
104
|
```
|
|
256
|
-
|
|
257
|
-
## Error Checking
|
|
258
|
-
|
|
259
|
-
Wrec checks for many kinds of errors and throws an `Error` when they are found.
|
|
260
|
-
Look for messages in the DevTools console.
|
|
261
|
-
The kinds of errors that are detected include:
|
|
262
|
-
|
|
263
|
-
- attribute names in web component instances
|
|
264
|
-
with no matching property declaration
|
|
265
|
-
- attribute values with a type that differs from the declared property type
|
|
266
|
-
- event handling function names that
|
|
267
|
-
don't match any method name in the web component
|
|
268
|
-
- expressions in attribute values or element text content
|
|
269
|
-
that reference undeclared web component properties
|
|
270
|
-
- expressions in element text content
|
|
271
|
-
that do not evaluate to a string or number
|
|
272
|
-
|
|
273
|
-
## Security
|
|
274
|
-
|
|
275
|
-
Wrec uses the JavaScript `eval` function to evaluate JavaScript expressions
|
|
276
|
-
that are placed in attribute values and the text content of elements.
|
|
277
|
-
This has security implications if those expressions
|
|
278
|
-
can come from untrusted sources, so it is best avoid
|
|
279
|
-
creating web components that use untrusted content in those ways.
|
|
280
|
-
|
|
281
|
-
Perhaps the most dangerous thing the use of `eval` allows
|
|
282
|
-
is sending HTTP requests to other servers.
|
|
283
|
-
Such requests could contain data scraped from your web app
|
|
284
|
-
in order to share it with unscrupulous sites.
|
|
285
|
-
|
|
286
|
-
The easiest way to prevent this is to add a
|
|
287
|
-
Content Security Policy (CSP) to your web app.
|
|
288
|
-
Simply adding the following element as a child of the
|
|
289
|
-
`head` element in each page blocks sending HTTP requests
|
|
290
|
-
to any domain except that of your web app:
|
|
291
|
-
|
|
292
|
-
```html
|
|
293
|
-
<meta http-equiv="Content-Security-Policy" content="connect-src 'self'" />
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
## More Examples
|
|
297
|
-
|
|
298
|
-
Check out the web app in the `examples` directory.
|
|
299
|
-
To run it, cd to that directory, enter `npm install`,
|
|
300
|
-
enter `npm run dev`, and browse localhost:5173.
|
|
301
|
-
|
|
302
|
-
This app begins by rendering "counter" components.
|
|
303
|
-
The first is implemented as a vanilla web component.
|
|
304
|
-
The next two uses the Wrec library.
|
|
305
|
-
Compare the files `counter-vanilla.js` and `counter-wrec.js`
|
|
306
|
-
to see how much using Wrec simplifies the code.
|
|
307
|
-
|
|
308
|
-
The `examples` app renders several other
|
|
309
|
-
web components that are built with wrec.
|
|
310
|
-
Examine their code for more examples of wrec usage.
|
|
311
|
-
|
|
312
|
-
## Tests
|
|
313
|
-
|
|
314
|
-
wrec has an extensive set of Playwright tests.
|
|
315
|
-
To run them:
|
|
316
|
-
|
|
317
|
-
1. Clone the wrec repository.
|
|
318
|
-
1. cd to the `examples` directory.
|
|
319
|
-
1. Enter `npm install`.
|
|
320
|
-
1. Enter `npm run testui`.
|
|
321
|
-
1. Click the right pointing triangle.
|
|
322
|
-
|
|
323
|
-
If there is no "Action" tab which displays a browser view of the running tests,
|
|
324
|
-
reset the Playwright UI settings by entering one of these commands:
|
|
325
|
-
|
|
326
|
-
```bash
|
|
327
|
-
# macOS
|
|
328
|
-
rm -rf ~/Library/Caches/ms-playwright/.settings
|
|
329
|
-
|
|
330
|
-
# Windows
|
|
331
|
-
del %LOCALAPPDATA%\ms-playwright\.settings /s /q
|
|
332
|
-
```
|
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,defaultForType=t=>t===Number?0:t!==Boolean&&"";function updateAttribute(t,e,
|
|
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,defaultForType=t=>t===Number?0:t!==Boolean&&"";function updateAttribute(t,e,r){const s=t.getAttribute(e);"boolean"==typeof r?r?s!==e&&t.setAttribute(e,e):t.removeAttribute(e):s!==r&&t.setAttribute(e,r)}function updateValue(t,e,r){t instanceof CSSRule?t.style.setProperty(e,r):updateAttribute(t,e,r)}class Wrec extends HTMLElement{static#t=new Map;static#e=new Map;#r=new Map;#s;#o;#i=new Map;propertyToParentPropertyMap=new Map;constructor(){super(),this.attachShadow({mode:"open"}),this.constructor.properties||(this.constructor.properties={}),this.constructor["#propertyToExpressionsMap"]||(this.constructor["#propertyToExpressionsMap"]=new Map),this.constructor.formAssociated&&(this.#o=this.attachInternals(),this.#s=new FormData,this.#o.setFormValue(this.#s))}attributeChangedCallback(t,e,r){const s=Wrec.getPropertyName(t),o=this.#n(s,r);this[s]=o,this.#a(s,o)}#c(t,e,r){t.addEventListener("input",t=>{this[e]=t.target.value});let s=this.#i.get(e);s||(s=[],this.#i.set(e,s)),s.push(r?{element:t,attrName:r}:t)}#p(){const t=this.constructor;let{_template:e}=t;if(!e){e=t.template=document.createElement("template");let r=t.css?`<style>${t.css}</style>`:"";r+=t.html,e.innerHTML=r}this.shadowRoot.replaceChildren(e.content.cloneNode(!0))}connectedCallback(){this.#u(),this.#l(),this.#p(),requestAnimationFrame(()=>{this.#h(this.shadowRoot),this.#m(this.shadowRoot),this.constructor.processed=!0,this.#f()})}#f(){const{properties:t}=this.constructor;for(const[e,{computed:r}]of Object.entries(t))r&&(this[e]=this.#d(r))}#l(){const{observedAttributes:t,properties:e}=this.constructor;for(const[r,s]of Object.entries(e))this.#b(r,s,t)}#b(t,e,r){const s=Wrec.getAttributeName(t);e.required&&!this.hasAttribute(s)&&this.#E(this,t,"is a required attribute");const o=r.includes(t)&&this.hasAttribute(s)?this.#g(t):e.value||defaultForType(e.type),i="#"+t;this[i]=o,e.computed&&this.#y(t,e),Object.defineProperty(this,t,{enumerable:!0,get(){return this[i]},set(r){if(r===this[i])return;this[i]=r;let o=this.constructor["#propertyToComputedMap"];if(o){const e=o.get(t)||[];for(const[t,r]of e)this[t]=this.#d(r)}if(this.hasAttribute(s)){r!==this.#g(t)&&updateAttribute(this,t,r)}this.#R(t);const n=this.propertyToParentPropertyMap.get(t);if(n){this.getRootNode().host.setAttribute(n,r)}this.#a(t,r),e.dispatch&&this.dispatchEvent(new CustomEvent("change",{bubbles:!0,composed:!0,detail:{propertyName:t}}))}})}static elementName(){return this.name.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}#v(t){const e=t.localName.includes("-");for(const r of t.getAttributeNames()){const s=t.getAttribute(r),o=this.#N(t,s);if(o){const s=this[o];void 0===s&&this.#A(t,r,o),t[o]=s,"value"===r&&this.#c(t,o,r),e&&t.propertyToParentPropertyMap.set(Wrec.getPropertyName(r),o)}this.#T(s,t,r)}}#d(expression){return(()=>eval(expression)).call(this)}#C(t){const{localName:e}=t;if("style"===e){for(const e of t.sheet.cssRules)if(e.type===CSSRule.STYLE_RULE)for(const t of e.style)if(t.startsWith("--")){const r=e.style.getPropertyValue(t);this.#T(r,e,t)}}else{const r=t.textContent.trim(),s=this.#N(t,r);"textarea"===e&&s?(this.#c(t,s),t.textContent=this[s]):this.#T(r,t)}}static getAttributeName(t){let e=Wrec.#t.get(t);return e||(e=t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Wrec.#t.set(t,e)),e}static getPropertyName(t){let e=Wrec.#e.get(t);return e||(e=t.replace(/-([a-z])/g,(t,e)=>e.toUpperCase()),Wrec.#e.set(t,e)),e}#m(t){const e=t.querySelectorAll("*");for(const t of e)this.#v(t),t.firstElementChild||this.#C(t)}static get observedAttributes(){return Object.keys(this.properties||{}).map(Wrec.getAttributeName)}#N(t,e){if(!REFERENCE_RE.test(e))return;const r=e.substring(SKIP);return void 0===this[r]&&this.#A(t,null,r),r}#R(t){const e=this.constructor["#propertyToExpressionsMap"].get(t)||[];for(const t of e){const e=this.#d(t),r=this.#r.get(t)||[];for(const t of r)t instanceof Element?this.#w(t,e):updateValue(t.element,t.attrName,e)}requestAnimationFrame(()=>{this.#M(t)})}static register(){const t=this.elementName();customElements.get(t)||customElements.define(t,this)}#y(t,e){const{computed:r,uses:s}=e;let o=this.constructor["#propertyToComputedMap"];function i(e,r){let s=o.get(e);s||(s=[],o.set(e,s)),s.push([t,r])}o||(o=this.constructor["#propertyToComputedMap"]=new Map);const n=r.match(REFERENCES_RE)||[];for(const e of n){const s=e.substring(SKIP);void 0===this[s]&&this.#A(null,t,s),"function"!=typeof this[s]&&i(s,r)}if(s)for(const t of s.split(","))i(t,r)}#T(t,e,r){const s=this.#x(e,r,t);if(!s){const s=t.replaceAll("this..","this.");return void(r?updateValue(e,r,s):e.textContent=s)}this.constructor.processed||s.forEach(e=>{const r=e.substring(SKIP),s=this.constructor["#propertyToExpressionsMap"];let o=s.get(r);o||(o=[],s.set(r,o)),o.push(t)});let o=this.#r.get(t);o||(o=[],this.#r.set(t,o)),o.push(r?{element:e,attrName:r}:e);const i=this.#d(t);r?updateValue(e,r,i):this.#w(e,i)}#a(t,e){this.#s&&(this.#s.set(t,e),this.#o.setFormValue(this.#s))}#E(t,e,r){throw new Error(`component ${this.constructor.elementName()}`+(t?`, element "${t.localName}"`:"")+(e?`, attribute "${e}"`:"")+` ${r}`)}#A(t,e,r){this.#E(t,e,`refers to missing property "${r}"`)}#g(t){return this.#n(t,this.getAttribute(t))}#n(t,e){if(e?.match(REFERENCES_RE))return e;const{type:r}=this.constructor.properties[t];if(r===String)return e;if(r===Number){const r=Number(e);if(!isNaN(r))return r;this.#E(null,t,`must be a number, but was "${e}"`)}if(r===Boolean)return"true"===e||"false"!==e&&(e&&e!==t&&this.#E(null,t,"is a Boolean attribute, so its value must match attribute name or be missing"),e===t);this.#E(null,t,"does not specify its type")}#M(t){const e=this[t],r=this.#i.get(t)||[];for(const t of r)if(t instanceof Element)"textarea"===t.localName?t.value=e:t.textContent=e;else{const{element:r,attrName:s}=t;updateAttribute(r,s,e),r[s]=e}}#w(t,e){const{localName:r}=t,s=typeof e;"string"!==s&&"number"!==s&&this.#E(t,null," computed content is not a string or number"),"textarea"===r?t.value=e:"string"===s&&e.trim().startsWith("<")?(t.innerHTML=e,this.#h(t),this.#m(t)):t.textContent=e}#u(){const t=new Set(Object.keys(this.constructor.properties));for(const e of this.getAttributeNames())"id"!==e&&(e.startsWith("on")||t.has(Wrec.getPropertyName(e))||this.#E(null,e,"is not a supported attribute"))}#x(t,e,r){const s=r.match(REFERENCES_RE);if(s)return s.forEach(r=>{const s=r.substring(SKIP);void 0===this[s]&&this.#A(t,e,s)}),s}#h(root){for(const element of root.querySelectorAll("*")){const attributesToRemove=[];for(const attr of element.attributes){const attrName=attr.name;if(attrName.startsWith("on")){const eventName=attrName.slice(2).toLowerCase(),attrValue=attr.value;let fn;this.#x(element,attrName,attrValue),"function"==typeof this[attrValue]?fn=t=>this[attrValue](t):(this.#x(element,attrName,attrValue),fn=()=>eval(attrValue)),element.addEventListener(eventName,fn),attributesToRemove.push(attrName)}}for(const t of attributesToRemove)element.removeAttribute(t)}}}export default Wrec;export const css=String.raw;export const html=String.raw;
|