flarp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
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,332 @@
1
+ # Flarp
2
+
3
+ **DOM-native XML State Management for Web Components**
4
+
5
+ ```
6
+ XML is state. Signals are reactive. The DOM is the runtime.
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Why Flarp?
12
+
13
+ State management shouldn't require learning proprietary patterns. Flarp uses what the web already has:
14
+
15
+ - **XML for state** — Nested structure you can see and understand
16
+ - **Signals for reactivity** — No virtual DOM, no diffing, just updates
17
+ - **Web Components for UI** — Standard, portable, composable
18
+ - **DOM as runtime** — MutationObserver does the scheduling
19
+
20
+ ## Quick Start
21
+
22
+ ```html
23
+ <script type="module">
24
+ import 'https://unpkg.com/flarp/src/index.js';
25
+ </script>
26
+
27
+ <f-state key="myapp" autosave="500">
28
+ <user>
29
+ <name>Alice</name>
30
+ <role>Developer</role>
31
+ </user>
32
+ </f-state>
33
+
34
+ <main>
35
+ <h1><f-text path="user.name"></f-text></h1>
36
+ <f-field path="user.role"></f-field>
37
+ </main>
38
+ ```
39
+
40
+ Components find `<f-state>` automatically — no nesting required!
41
+
42
+ > **Note:** The DOM lowercases all tag names. Use lowercase paths like `user.name`, not `User.Name`. Path matching is case-insensitive, so `User.Name` will still work, but your XML will appear lowercase in the DOM.
43
+
44
+ ## Key Innovation: Signal-Based Readiness
45
+
46
+ Events fire once. If you miss them, you miss them. Signals don't have this problem:
47
+
48
+ ```js
49
+ // This works even if 'ready' already happened!
50
+ store.state.when('ready', () => {
51
+ const name = store.at('User.Name');
52
+ name.subscribe(v => console.log('Name:', v));
53
+ });
54
+
55
+ // Or use async/await
56
+ await store.state.until('ready');
57
+ ```
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ npm install flarp
63
+ ```
64
+
65
+ Or use directly:
66
+
67
+ ```html
68
+ <script type="module">
69
+ import 'https://unpkg.com/flarp/src/index.js';
70
+ </script>
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Components
76
+
77
+ ### `<f-state>` — The Store
78
+
79
+ ```html
80
+ <f-state
81
+ key="myapp" <!-- Persistence key -->
82
+ autosave="500" <!-- Debounce ms -->
83
+ src="/api/data" <!-- Load from URL -->
84
+ >
85
+ <User>
86
+ <n>Alice</n>
87
+ </User>
88
+ </f-state>
89
+ ```
90
+
91
+ **API:**
92
+ - `at(path)` — Get reactive node
93
+ - `query(path)` — Get all matching nodes
94
+ - `set(path, value)` — Set value
95
+ - `add(path, xml)` — Add child
96
+ - `remove(path)` — Remove node
97
+ - `serialize()` — Get XML string
98
+ - `on(action, fn)` — Register action
99
+ - `emit(action, payload)` — Dispatch action
100
+ - `state.when(name, fn)` — React to state
101
+ - `state.until(name)` — Promise for state
102
+
103
+ ### `<f-text>` — Display
104
+
105
+ ```html
106
+ <f-text path="User.Name"></f-text>
107
+ <f-text path="Price" format="currency"></f-text>
108
+ ```
109
+
110
+ **Formats:** `uppercase`, `lowercase`, `number`, `currency`, `percent`
111
+
112
+ ### `<f-field>` — Two-Way Binding
113
+
114
+ ```html
115
+ <f-field path="User.Name"></f-field>
116
+ <f-field path="User.Age" type="number"></f-field>
117
+ <f-field path="User.Active" type="checkbox"></f-field>
118
+ ```
119
+
120
+ ### `<f-each>` — Lists
121
+
122
+ ```html
123
+ <f-each path="Users.User">
124
+ <template>
125
+ <div class="card">
126
+ <f-text path="Name"></f-text>
127
+ <f-field path="Email"></f-field>
128
+ </div>
129
+ </template>
130
+ </f-each>
131
+ ```
132
+
133
+ Paths inside templates are relative to each item.
134
+
135
+ ### `<f-when>` — Conditionals
136
+
137
+ ```html
138
+ <f-when test="User.LoggedIn == 'true'">
139
+ <span>Welcome!</span>
140
+ </f-when>
141
+
142
+ <f-when test="Cart.Total > 100">
143
+ <span>Free shipping!</span>
144
+ </f-when>
145
+ ```
146
+
147
+ ### `<f-match>` — Switch
148
+
149
+ ```html
150
+ <f-match test="User.Role">
151
+ <f-case value="admin">Admin Panel</f-case>
152
+ <f-case value="user">Dashboard</f-case>
153
+ <f-else>Please log in</f-else>
154
+ </f-match>
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Store Discovery
160
+
161
+ Components don't need to be nested inside `<f-state>`. Flarp searches siblings and ancestors:
162
+
163
+ ```html
164
+ <div class="app">
165
+ <f-state id="app">
166
+ <User><n>Alice</n></User>
167
+ </f-state>
168
+
169
+ <main>
170
+ <!-- Finds sibling f-state automatically -->
171
+ <f-text path="User.Name"></f-text>
172
+ </main>
173
+ </div>
174
+ ```
175
+
176
+ Or reference explicitly:
177
+
178
+ ```html
179
+ <f-text store="app" path="User.Name"></f-text>
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Event Sourcing
185
+
186
+ ```js
187
+ // Register actions
188
+ store.on('addUser', ({ name, role }) => {
189
+ store.add('Users', `<User><n>${name}</n><Role>${role}</Role></User>`);
190
+ });
191
+
192
+ // Dispatch
193
+ store.emit('addUser', { name: 'Bob', role: 'Developer' });
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Per-Node Reactivity
199
+
200
+ Every XML node is independently:
201
+ - **Reactive** — Has its own Signal
202
+ - **Identifiable** — UUID attribute
203
+ - **Versioned** — `rev` for conflict resolution
204
+ - **Serializable** — `node.serialize()`
205
+
206
+ ```js
207
+ const node = store.at('User.Name');
208
+
209
+ node.uuid; // "a1b2c3..."
210
+ node.rev; // 5
211
+ node.serialize(); // "<n uuid="..." rev="5">Alice</n>"
212
+
213
+ // Subscribe to just this node
214
+ node.subscribe(value => console.log(value));
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Persistence
220
+
221
+ State persists to localStorage automatically:
222
+
223
+ ```html
224
+ <f-state key="myapp" autosave="500">
225
+ ```
226
+
227
+ Features:
228
+ - **Crash recovery** — Reload resumes state
229
+ - **Cross-tab sync** — BroadcastChannel
230
+ - **Conflict resolution** — Higher `rev` wins, `uuid` tiebreaker
231
+
232
+ ---
233
+
234
+ ## Modular Architecture
235
+
236
+ Flarp is built from composable pieces:
237
+
238
+ ```js
239
+ // Just the signal primitive
240
+ import { Signal } from 'flarp/core';
241
+
242
+ // Just XML utilities
243
+ import { Node, Path } from 'flarp/xml';
244
+
245
+ // Just persistence
246
+ import { Persist } from 'flarp/sync';
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Custom Components
252
+
253
+ ```js
254
+ import { findStore } from 'flarp';
255
+
256
+ class UserCard extends HTMLElement {
257
+ connectedCallback() {
258
+ const store = findStore(this);
259
+
260
+ store.state.when('ready', () => {
261
+ const name = store.at('User.Name');
262
+
263
+ name.subscribe(v => {
264
+ this.innerHTML = `<h2>${v}</h2>`;
265
+ });
266
+ });
267
+ }
268
+ }
269
+
270
+ customElements.define('user-card', UserCard);
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Philosophy
276
+
277
+ 1. **XML is data** — Human-readable, self-describing, AI-friendly
278
+ 2. **Signals are sync** — No scheduler, no batching, just updates
279
+ 3. **DOM is truth** — MutationObserver, not virtual DOM
280
+ 4. **State ≠ UI** — Keep them separate, connect with paths
281
+ 5. **Persist by default** — State survives refresh
282
+
283
+ ---
284
+
285
+ ## Tag Naming
286
+
287
+ **Important:** The browser's HTML parser lowercases all tag names. When you write:
288
+
289
+ ```html
290
+ <f-state>
291
+ <User><Name>Alice</Name></User>
292
+ </f-state>
293
+ ```
294
+
295
+ The DOM actually contains:
296
+
297
+ ```html
298
+ <f-state>
299
+ <user><name>Alice</name></user>
300
+ </f-state>
301
+ ```
302
+
303
+ **Flarp handles this automatically** — path matching is case-insensitive. Both `user.name` and `User.Name` will work. However, we recommend using lowercase tags for clarity:
304
+
305
+ ```html
306
+ <f-state>
307
+ <user>
308
+ <name>Alice</name>
309
+ <email>alice@example.com</email>
310
+ </user>
311
+ </f-state>
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Browser Support
317
+
318
+ Modern browsers with ES modules:
319
+ - Chrome 61+
320
+ - Firefox 60+
321
+ - Safari 11+
322
+ - Edge 79+
323
+
324
+ ---
325
+
326
+ ## License
327
+
328
+ MIT
329
+
330
+ ---
331
+
332
+ *Built for developers who believe state management can be simple.*
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "flarp",
3
+ "version": "1.0.0",
4
+ "description": "DOM-native XML state management for Web Components",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./core": "./src/core/index.js",
11
+ "./xml": "./src/xml/index.js",
12
+ "./sync": "./src/sync/index.js",
13
+ "./dom": "./src/dom/index.js",
14
+ "./components": "./src/components/index.js"
15
+ },
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "dev": "http-server -c-1 -o",
21
+ "start": "http-server -c-1 -o"
22
+ },
23
+ "keywords": [
24
+ "xml",
25
+ "state",
26
+ "signals",
27
+ "reactive",
28
+ "web-components",
29
+ "dom"
30
+ ],
31
+
32
+
33
+ "author": "catpea (https://github.com/catpea)",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/catpea/flarp.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/catpea/flarp/issues"
41
+ },
42
+ "homepage": "https://catpea.github.io/flarp",
43
+ "funding": {
44
+ "type": "github",
45
+ "url": "https://github.com/sponsors/catpea"
46
+ }
47
+
48
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * FBind - One-way binding to external DOM elements
3
+ *
4
+ * @element f-bind
5
+ *
6
+ * @attr {string} path - XML path to bind from
7
+ * @attr {string} target - CSS selector for target element(s)
8
+ * @attr {string} attr - Attribute to set (optional)
9
+ * @attr {string} prop - Property to set (optional)
10
+ * @attr {string} store - Optional store ID reference
11
+ *
12
+ * @example
13
+ * <f-bind path="User.Name" target="#header h1"></f-bind>
14
+ * <f-bind path="App.Theme" target="body" attr="data-theme"></f-bind>
15
+ * <f-bind path="User.Avatar" target="#avatar" prop="src"></f-bind>
16
+ */
17
+
18
+ import { findStore } from '../dom/find.js';
19
+
20
+ export default class FBind extends HTMLElement {
21
+ #unsubscribe = null;
22
+
23
+ connectedCallback() {
24
+ this.style.display = 'none';
25
+ this.#connect();
26
+ }
27
+
28
+ disconnectedCallback() {
29
+ this.#unsubscribe?.();
30
+ }
31
+
32
+ #connect() {
33
+ const path = this.getAttribute('path');
34
+ const target = this.getAttribute('target');
35
+ const attr = this.getAttribute('attr');
36
+ const prop = this.getAttribute('prop');
37
+
38
+ if (!path || !target) {
39
+ console.warn('<f-bind> requires path and target attributes');
40
+ return;
41
+ }
42
+
43
+ const store = this._store || findStore(this);
44
+ if (!store) {
45
+ console.warn('<f-bind> could not find store');
46
+ return;
47
+ }
48
+
49
+ store.state.when('ready', () => {
50
+ const node = store.at(path);
51
+ if (!node) return;
52
+
53
+ this.#unsubscribe = node.subscribe(value => {
54
+ const targets = document.querySelectorAll(target);
55
+
56
+ for (const el of targets) {
57
+ if (attr) {
58
+ if (value != null) {
59
+ el.setAttribute(attr, value);
60
+ } else {
61
+ el.removeAttribute(attr);
62
+ }
63
+ } else if (prop) {
64
+ el[prop] = value ?? '';
65
+ } else if ('value' in el && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
66
+ el.value = value ?? '';
67
+ } else {
68
+ el.textContent = value ?? '';
69
+ }
70
+ }
71
+ });
72
+ });
73
+ }
74
+ }
75
+
76
+ customElements.define('f-bind', FBind);
@@ -0,0 +1,194 @@
1
+ /**
2
+ * FEach - Reactive list iteration
3
+ *
4
+ * @element f-each
5
+ *
6
+ * @attr {string} path - XML path to iterate over
7
+ * @attr {string} store - Optional store ID reference
8
+ *
9
+ * Inside the template, paths are relative to each item.
10
+ * Use "." for the current node's text content.
11
+ *
12
+ * @example
13
+ * <f-each path="Users.User">
14
+ * <template>
15
+ * <div class="user">
16
+ * <f-text path="Name"></f-text>
17
+ * <f-field path="Email"></f-field>
18
+ * </div>
19
+ * </template>
20
+ * </f-each>
21
+ */
22
+
23
+ import { findStore, scopedStore } from '../dom/find.js';
24
+ import Node from '../xml/Node.js';
25
+ import * as Path from '../xml/Path.js';
26
+
27
+ export default class FEach extends HTMLElement {
28
+ #template = null;
29
+ #container = null;
30
+ #instances = new WeakMap();
31
+ #observer = null;
32
+
33
+ connectedCallback() {
34
+ this.#setup();
35
+ }
36
+
37
+ disconnectedCallback() {
38
+ this.#observer?.disconnect();
39
+ }
40
+
41
+ #setup() {
42
+ const path = this.getAttribute('path');
43
+ this.#template = this.querySelector('template');
44
+
45
+ if (!this.#template) {
46
+ console.warn('<f-each> requires a <template> child');
47
+ return;
48
+ }
49
+
50
+ const store = this._store || findStore(this);
51
+ if (!store) {
52
+ console.warn('<f-each> could not find store');
53
+ return;
54
+ }
55
+
56
+ // Create container for rendered items
57
+ this.#container = document.createElement('div');
58
+ this.#container.style.display = 'contents';
59
+ this.appendChild(this.#container);
60
+
61
+ store.state.when('ready', () => {
62
+ // Find parent element to observe
63
+ const parentPath = Path.parent(path);
64
+ const parentResolved = parentPath
65
+ ? Path.resolve(store, parentPath)
66
+ : { element: store };
67
+
68
+ if (!parentResolved) {
69
+ console.warn(`<f-each> parent path not found: ${parentPath}`);
70
+ return;
71
+ }
72
+
73
+ const parent = parentResolved.element;
74
+
75
+ // Initial render
76
+ this.#render(store, path);
77
+
78
+ // Observe for structural changes
79
+ this.#observer = new MutationObserver(() => {
80
+ this.#render(store, path);
81
+ });
82
+
83
+ this.#observer.observe(parent, { childList: true });
84
+ });
85
+ }
86
+
87
+ #render(store, path) {
88
+ const elements = Path.resolveAll(store, path);
89
+ const seen = new Set(elements);
90
+
91
+ // Remove stale items
92
+ for (const child of Array.from(this.#container.children)) {
93
+ if (!seen.has(child.__element)) {
94
+ child.remove();
95
+ }
96
+ }
97
+
98
+ // Add/update items
99
+ elements.forEach((element, index) => {
100
+ if (this.#instances.has(element)) {
101
+ // Ensure correct order
102
+ const existing = this.#instances.get(element);
103
+ if (this.#container.children[index] !== existing) {
104
+ this.#container.insertBefore(existing, this.#container.children[index]);
105
+ }
106
+ return;
107
+ }
108
+
109
+ // Create new instance
110
+ const fragment = this.#template.content.cloneNode(true);
111
+ const wrapper = document.createElement('div');
112
+ wrapper.style.display = 'contents';
113
+ wrapper.appendChild(fragment);
114
+ wrapper.__element = element;
115
+
116
+ // Create scoped store for this item
117
+ const scoped = this.#createScopedStore(store, element);
118
+
119
+ // Set scoped store on all f-* children
120
+ this.#initializeScoped(wrapper, scoped);
121
+
122
+ this.#instances.set(element, wrapper);
123
+
124
+ // Insert at correct position
125
+ if (this.#container.children[index]) {
126
+ this.#container.insertBefore(wrapper, this.#container.children[index]);
127
+ } else {
128
+ this.#container.appendChild(wrapper);
129
+ }
130
+ });
131
+ }
132
+
133
+ #createScopedStore(parentStore, element) {
134
+ return {
135
+ at(path) {
136
+ if (path === '.' || path === '') {
137
+ return Node.wrap(element);
138
+ }
139
+ const resolved = Path.resolve(element, path);
140
+ if (!resolved) return null;
141
+
142
+ if (resolved.attr) {
143
+ const { AttrNode } = parentStore.constructor;
144
+ return AttrNode.wrap(resolved.element, resolved.attr);
145
+ }
146
+ return Node.wrap(resolved.element);
147
+ },
148
+
149
+ query(path) {
150
+ return Path.resolveAll(element, path).map(el => Node.wrap(el));
151
+ },
152
+
153
+ add: parentStore.add?.bind(parentStore),
154
+ remove: parentStore.remove?.bind(parentStore),
155
+ emit: parentStore.emit?.bind(parentStore),
156
+ on: parentStore.on?.bind(parentStore),
157
+ state: parentStore.state,
158
+
159
+ // For nested f-each
160
+ constructor: parentStore.constructor
161
+ };
162
+ }
163
+
164
+ #initializeScoped(root, scoped) {
165
+ const scopedTags = ['f-text', 'f-field', 'f-each', 'f-when', 'f-bind'];
166
+
167
+ for (const tag of scopedTags) {
168
+ for (const el of root.querySelectorAll(tag)) {
169
+ el._store = scoped;
170
+ }
171
+ }
172
+
173
+ // Handle elements added later
174
+ const observer = new MutationObserver(mutations => {
175
+ for (const m of mutations) {
176
+ for (const node of m.addedNodes) {
177
+ if (node.nodeType !== 1) continue;
178
+ if (scopedTags.includes(node.tagName.toLowerCase())) {
179
+ node._store = scoped;
180
+ }
181
+ for (const tag of scopedTags) {
182
+ for (const el of node.querySelectorAll?.(tag) || []) {
183
+ el._store = scoped;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ });
189
+
190
+ observer.observe(root, { childList: true, subtree: true });
191
+ }
192
+ }
193
+
194
+ customElements.define('f-each', FEach);