@plentico/pattr 0.0.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Plentico
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,414 @@
1
+ # Pattr
2
+
3
+ <img src="https://github.com/plentico/pattr/blob/master/pattr.svg" />
4
+
5
+ A terse, attribute-driven JS library that provides reactive DOM updates within scoped components
6
+
7
+ ## Purpose
8
+
9
+ This is a companion project to [Pico](https://github.com/plentico/pico). It's a simple JS script that interacts with "p" attributes on HTML markup to provide reactive updates.
10
+
11
+ It's a similar concept to [AlpineJS](https://alpinejs.dev/), but the main differences are that AlpineJS does way more, and Pattr has an intentionally compact syntax and scopes components so that:
12
+ - Parent changes update Child and Grandchild components
13
+ - Child changes update Grandchildren, but do nothing to Parents
14
+ - Grandchildren do not impact Child or Parents
15
+
16
+ Goals:
17
+ 1. Modify the markup as little as possible. That means, simple, readable syntax and making inferences from standard html (e.g. an input with type=number automatically converts string value to number).
18
+ 2. Minimal influence on dev structure. That means avoiding requirements like wrapping loop items or conditional output in singular root elements.
19
+
20
+ ## Table of Contents
21
+
22
+ - [Data Sources](#data-sources)
23
+ - [Directives](#directives)
24
+ - [Event Handling](#event-handling)
25
+ - [Scoped Components](#scoped-components)
26
+ - [Loops](#loops)
27
+ - [Modifiers](#modifiers)
28
+
29
+ ## Data Sources
30
+
31
+ Pattr reads data from JSON script tags in your HTML:
32
+
33
+ ### Root Data (Props from CMS)
34
+ ```html
35
+ <script id="p-root-data" type="application/json">
36
+ {
37
+ "title": "Welcome",
38
+ "count": 5
39
+ }
40
+ </script>
41
+ ```
42
+
43
+ Both data sources are merged and available throughout your app.
44
+
45
+ ## Directives
46
+
47
+ ### `p-text` - Display Text Content
48
+
49
+ ```html
50
+ <div p-text="name"></div>
51
+ <div p-text="`Hello ${name}!`"></div>
52
+ ```
53
+
54
+ ### `p-html` - Display HTML Content
55
+
56
+ ```html
57
+ <div p-html="htmlContent"></div>
58
+ ```
59
+
60
+ **Security Note:** Use with caution. Consider using the `allow` modifier to whitelist safe tags.
61
+
62
+ ### `p-show` - Conditional Display
63
+
64
+ ```html
65
+ <span p-show="count > 5">Count is greater than 5</span>
66
+ <span p-show="!show_menu">Menu is hidden</span>
67
+ ```
68
+
69
+ ### `p-style` - Set Styles
70
+
71
+ **String syntax:**
72
+ ```html
73
+ <div p-style="show_menu ? 'max-height: 300px' : 'max-height: 0'"></div>
74
+ ```
75
+
76
+ **Object syntax:**
77
+ ```html
78
+ <div p-style="{ color: 'red', fontSize: '20px' }"></div>
79
+ ```
80
+
81
+ ### `p-class` - Set Classes
82
+
83
+ **String syntax:**
84
+ ```html
85
+ <div p-class="active"></div>
86
+ ```
87
+
88
+ **String syntax with template literals**
89
+ ```html
90
+ <div p-class="`base-class ${isActive ? 'active' : ''} ${hasError ? 'error' : ''}`">
91
+ ```
92
+
93
+ **Array syntax:**
94
+ ```html
95
+ <div p-class="['btn', 'btn-primary']"></div>
96
+ ```
97
+
98
+ **Object syntax (conditional classes):**
99
+ ```html
100
+ <div p-class="{ active: isActive, disabled: !isEnabled }"></div>
101
+ ```
102
+
103
+ ### `p-attr` - Set Attributes
104
+
105
+ **Single attributes**
106
+ ```html
107
+ <div
108
+ p-attr:data-id="userId"
109
+ p-attr:data-total="price * quantity"
110
+ p-attr:data-name="firstName + ' ' + lastName"
111
+ p-attr:data-age="`${name} is ${age} years old`"
112
+ p-attr:data-status="isActive ? 'active' : 'inactive'"
113
+ p-attr:data-upper="userName.toUpperCase()"
114
+ p-attr:aria-label="ariaText"
115
+ p-attr:href="linkUrl"
116
+ >
117
+ ```
118
+
119
+ **Object syntax for multiple attributes**
120
+ ```html
121
+ <div p-attr="{
122
+ 'data-id': userId,
123
+ 'data-total': price * quantity,
124
+ 'data-name': firstName + ' ' + lastName,
125
+ 'data-age': `${name} is ${age} years old`,
126
+ 'data-status': isActive ? 'active' : 'inactive',
127
+ 'data-upper': userName.toUpperCase(),
128
+ 'aria-label': ariaText,
129
+ 'href': linkUrl
130
+ }">
131
+ ```
132
+
133
+ ### `p-model` - Two-Way Data Binding
134
+
135
+ ```html
136
+ <input type="text" p-model="name" />
137
+ <textarea p-model="description"></textarea>
138
+ ```
139
+
140
+ ## Event Handling
141
+
142
+ Use `p-on:eventname` to handle events:
143
+
144
+ ```html
145
+ <!-- Click events -->
146
+ <button p-on:click="count++">Increment</button>
147
+ <button p-on:click="count--">Decrement</button>
148
+ <button p-on:click="items = [...items, 'New Item']">Add Item</button>
149
+
150
+ <!-- Input events -->
151
+ <input p-on:focus="isEditing = true" p-on:blur="isEditing = false" />
152
+
153
+ <!-- Multiple statements -->
154
+ <button p-on:click="count++; show_menu = false">Update & Close</button>
155
+ ```
156
+
157
+ Available events: `click`, `focus`, `blur`, `input`, `change`, `submit`, `keydown`, `keyup`, `mouseenter`, `mouseleave`, etc.
158
+
159
+ ## Scoped Components
160
+
161
+ Create nested components with isolated reactive state using `p-scope` and `p-id`:
162
+
163
+ ```html
164
+ <div>
165
+ Parent count: <span p-text="count"></span>
166
+ <button p-on:click="count++">+</button>
167
+ </div>
168
+
169
+ <section p-id="child1" p-scope="count = count * 2; name = name + 'o';">
170
+ <div>Child count (×2): <span p-text="count"></span></div>
171
+ <button p-on:click="count++">+</button>
172
+
173
+ <section p-id="grandchild1" p-scope="count = count + 1;">
174
+ <div>Grandchild count (+1): <span p-text="count"></span></div>
175
+ <button p-on:click="count++">+</button>
176
+ </section>
177
+ </section>
178
+ ```
179
+
180
+ ### Scoping Example
181
+
182
+ Components down the chain can diverge from the reactivity provided by their Parent components. However, if a Parent component is updated, it will resync all descendant components.
183
+
184
+ ```
185
+ Parent = 2
186
+ Child (Parent * 2) = 4
187
+ Grandchild (Child + 1) = 5
188
+ ```
189
+
190
+ If we increment Parent by 1:
191
+
192
+ ```
193
+ Parent = 3
194
+ Child (Parent * 2) = 6
195
+ Grandchild (Child + 1) = 7
196
+ ```
197
+
198
+ If we now increment Child by 1 (diverges from Parent):
199
+
200
+ ```
201
+ Parent = 3
202
+ Child (Parent * 2) = 7
203
+ Grandchild (Child + 1) = 8
204
+ ```
205
+
206
+ But if we then increment Parent by 1 (resyncs with Parent):
207
+
208
+ ```
209
+ Parent = 4
210
+ Child (Parent * 2) = 8
211
+ Grandchild (Child + 1) = 9
212
+ ```
213
+
214
+ ## Loops
215
+
216
+ Use `p-for` with `<template>` to iterate over data:
217
+
218
+ ### Simple Iteration
219
+
220
+ ```html
221
+ <template p-for="item of items">
222
+ <div p-text="item"></div>
223
+ </template>
224
+ ```
225
+
226
+ ### With Index (Array Destructuring)
227
+
228
+ ```html
229
+ <template p-for="[i, item] of items.entries()">
230
+ <div>
231
+ <span p-text="i"></span>: <span p-text="item"></span>
232
+ </div>
233
+ </template>
234
+ ```
235
+
236
+ ### Nested Loops (BROKEN, STILL WIP)
237
+
238
+ ```html
239
+ <template p-for="[i, cat] of cats.entries()">
240
+ <div>
241
+ <h3 p-text="cat"></h3>
242
+ <template p-for="letter of cat">
243
+ <button p-on:click="cats[i] = cat + letter" p-text="letter"></button>
244
+ </template>
245
+ </div>
246
+ </template>
247
+ ```
248
+
249
+ ### Loop Actions
250
+
251
+ ```html
252
+ <template p-for="[i, item] of items.entries()">
253
+ <div>
254
+ <span p-text="item"></span>
255
+
256
+ <!-- Remove item -->
257
+ <button p-on:click="items = items.filter((_, idx) => idx !== i)">Remove</button>
258
+
259
+ <!-- Update item -->
260
+ <button p-on:click="items[i] = items[i] + '!'">Add !</button>
261
+ </div>
262
+ </template>
263
+
264
+ <!-- Add new item -->
265
+ <input p-model="new_item" />
266
+ <button p-on:click="items = [...items, new_item]; new_item = ''">Add</button>
267
+ ```
268
+
269
+ ### SSR Support
270
+
271
+ Pattr supports hydrating server-side rendered loops. Add `p-for-key` attributes to SSR elements:
272
+
273
+ ```html
274
+ <template p-for="item of items">
275
+ <div>...</div>
276
+ </template>
277
+
278
+ <!-- SSR rendered elements -->
279
+ <div p-for-key="0">Apple</div>
280
+ <div p-for-key="1">Banana</div>
281
+ <div p-for-key="2">Orange</div>
282
+ ```
283
+
284
+ ## Modifiers
285
+
286
+ Modifiers extend directive functionality using colon syntax:
287
+
288
+ ### Trim Text
289
+
290
+ Limit text length and add ellipsis:
291
+
292
+ ```html
293
+ <div p-text:trim.50="longText"></div>
294
+ <!-- Result: "This is a very long text that will be tr..." -->
295
+ ```
296
+
297
+ ### Trim HTML
298
+
299
+ Trim HTML while preserving tags:
300
+
301
+ ```html
302
+ <div p-html:trim.100="htmlContent"></div>
303
+ ```
304
+
305
+ ### Allow HTML Tags
306
+
307
+ Whitelist specific HTML tags (removes all others):
308
+
309
+ ```html
310
+ <div p-html:allow.strong.em.p="unsafeHtml"></div>
311
+ ```
312
+
313
+ ### Combine Modifiers
314
+
315
+ Chain multiple modifiers together:
316
+
317
+ ```html
318
+ <div p-html:allow.strong.em:trim.50="htmlContent"></div>
319
+ ```
320
+
321
+ ## Installation
322
+
323
+ ### CDN
324
+
325
+ ```html
326
+ <!-- Via unpkg -->
327
+ <script src="https://unpkg.com/@plentico/pattr" defer></script>
328
+
329
+ <!-- Via jsdelivr -->
330
+ <script src="https://cdn.jsdelivr.net/npm/@plentico/pattr" defer></script>
331
+
332
+ <!-- Minified -->
333
+ <script src="https://unpkg.com/@plentico/pattr/min" defer></script>
334
+ ```
335
+
336
+ ### npm
337
+
338
+ ```bash
339
+ npm install @plentico/pattr
340
+ ```
341
+
342
+ ```javascript
343
+ import Pattr from '@plentico/pattr';
344
+ ```
345
+
346
+ ### Local
347
+
348
+ ```html
349
+ <script src="/pattr.js" defer></script>
350
+ ```
351
+
352
+ ## Complete Example
353
+
354
+ ```html
355
+ <!DOCTYPE html>
356
+ <html>
357
+ <head>
358
+ <script src="https://unpkg.com/@plentico/pattr" defer></script>
359
+ <script id="p-root-data" type="application/json">
360
+ {
361
+ "title": "Todo App",
362
+ "username": "John"
363
+ }
364
+ </script>
365
+ <script id="p-local-data" type="application/json">
366
+ {
367
+ "todos": ["Buy groceries", "Walk dog"],
368
+ "new_todo": "",
369
+ "show_completed": false
370
+ }
371
+ </script>
372
+ </head>
373
+ <body>
374
+ <h1 p-text="title"></h1>
375
+ <p>Welcome, <span p-text="username"></span>!</p>
376
+
377
+ <!-- Add todo -->
378
+ <input p-model="new_todo" placeholder="New todo..." />
379
+ <button p-on:click="todos = [...todos, new_todo]; new_todo = ''">
380
+ Add Todo
381
+ </button>
382
+
383
+ <!-- Todo list -->
384
+ <template p-for="[i, todo] of todos.entries()">
385
+ <div>
386
+ <span p-text="todo"></span>
387
+ <button p-on:click="todos = todos.filter((_, idx) => idx !== i)">
388
+ Delete
389
+ </button>
390
+ </div>
391
+ </template>
392
+
393
+ <!-- Scoped component -->
394
+ <section p-id="stats" p-scope="count = todos.length;">
395
+ <p>Total todos: <span p-text="count"></span></p>
396
+ </section>
397
+ </body>
398
+ </html>
399
+ ```
400
+
401
+ ## Browser Support
402
+
403
+ Pattr uses modern JavaScript features:
404
+ - Proxy
405
+ - Template literals
406
+ - Arrow functions
407
+ - Destructuring
408
+ - Spread operator
409
+
410
+ Supports all modern browsers (Chrome, Firefox, Safari, Edge).
411
+
412
+ ## License
413
+
414
+ MIT
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@plentico/pattr",
3
+ "version": "0.0.4",
4
+ "description": "A lightweight reactive framework",
5
+ "type": "module",
6
+ "main": "pattr.js",
7
+ "browser": "pattr.js",
8
+ "unpkg": "pattr.min.js",
9
+ "jsdelivr": "pattr.min.js",
10
+ "exports": {
11
+ ".": {
12
+ "browser": "./pattr.js",
13
+ "default": "./pattr.js"
14
+ },
15
+ "./min": {
16
+ "browser": "./pattr.min.js",
17
+ "default": "./pattr.min.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "pattr.js",
22
+ "pattr.min.js"
23
+ ],
24
+ "scripts": {
25
+ "dev": "echo '\\n Server running at:\\n\\n ➜ http://localhost:8080\\n' && npx http-server -p 8080 -c-1",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:e2e": "playwright test",
29
+ "test:all": "npm run test && npm run test:e2e",
30
+ "build": "terser pattr.js -o pattr.min.js -c -m --comments '/^!/'",
31
+ "prepublishOnly": "npm run test:all && npm run build"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/plentico/pattr.git"
36
+ },
37
+ "keywords": [
38
+ "reactive",
39
+ "framework",
40
+ "lightweight",
41
+ "dom",
42
+ "data-binding",
43
+ "alpinejs-alternative"
44
+ ],
45
+ "author": "Plentico",
46
+ "license": "MIT",
47
+ "bugs": {
48
+ "url": "https://github.com/plentico/pattr/issues"
49
+ },
50
+ "homepage": "https://github.com/plentico/pattr#readme",
51
+ "devDependencies": {
52
+ "@playwright/test": "^1.40.0",
53
+ "http-server": "^14.1.1",
54
+ "jsdom": "^23.0.0",
55
+ "terser": "^5.27.0",
56
+ "vitest": "^1.0.0"
57
+ }
58
+ }