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 +21 -0
- package/README.md +332 -0
- package/package.json +48 -0
- package/src/components/FBind.js +76 -0
- package/src/components/FEach.js +194 -0
- package/src/components/FField.js +153 -0
- package/src/components/FStore.js +332 -0
- package/src/components/FText.js +73 -0
- package/src/components/FWhen.js +253 -0
- package/src/components/index.js +10 -0
- package/src/core/Signal.js +220 -0
- package/src/core/State.js +168 -0
- package/src/core/index.js +6 -0
- package/src/dom/find.js +128 -0
- package/src/dom/index.js +5 -0
- package/src/index.js +63 -0
- package/src/sync/Channel.js +113 -0
- package/src/sync/Persist.js +216 -0
- package/src/sync/index.js +6 -0
- package/src/xml/Node.js +396 -0
- package/src/xml/Path.js +176 -0
- package/src/xml/Tree.js +279 -0
- package/src/xml/index.js +7 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FField - Two-way form binding
|
|
3
|
+
*
|
|
4
|
+
* @element f-field
|
|
5
|
+
*
|
|
6
|
+
* @attr {string} path - XML path to bind
|
|
7
|
+
* @attr {string} store - Optional store ID reference
|
|
8
|
+
* @attr {string} type - Input type (text, number, checkbox, etc.)
|
|
9
|
+
* @attr {string} target - CSS selector for external input
|
|
10
|
+
* @attr {string} event - Event to listen for (default: input)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <f-field path="User.Name"></f-field>
|
|
14
|
+
* <f-field path="User.Age" type="number"></f-field>
|
|
15
|
+
* <f-field path="User.Active" type="checkbox"></f-field>
|
|
16
|
+
* <f-field path="User.Email" target="#emailInput"></f-field>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { findStore } from '../dom/find.js';
|
|
20
|
+
|
|
21
|
+
export default class FField extends HTMLElement {
|
|
22
|
+
#input = null;
|
|
23
|
+
#unsubscribe = null;
|
|
24
|
+
#updating = false;
|
|
25
|
+
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
this.#connect();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
disconnectedCallback() {
|
|
31
|
+
this.#unsubscribe?.();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#connect() {
|
|
35
|
+
const path = this.getAttribute('path');
|
|
36
|
+
const type = this.getAttribute('type') || 'text';
|
|
37
|
+
const target = this.getAttribute('target');
|
|
38
|
+
const eventType = this.getAttribute('event') || 'input';
|
|
39
|
+
|
|
40
|
+
if (!path) {
|
|
41
|
+
console.warn('<f-field> requires path attribute');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const store = this._store || findStore(this);
|
|
46
|
+
if (!store) {
|
|
47
|
+
console.warn('<f-field> could not find store');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
store.state.when('ready', () => {
|
|
52
|
+
const node = store.at(path);
|
|
53
|
+
if (!node) {
|
|
54
|
+
console.warn(`<f-field> path not found: ${path}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get or create input
|
|
59
|
+
this.#input = target
|
|
60
|
+
? document.querySelector(target)
|
|
61
|
+
: this.#createInput(type);
|
|
62
|
+
|
|
63
|
+
if (!this.#input) return;
|
|
64
|
+
|
|
65
|
+
// Node → Input
|
|
66
|
+
this.#unsubscribe = node.subscribe(value => {
|
|
67
|
+
if (this.#updating) return;
|
|
68
|
+
this.#setInputValue(value, type);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Input → Node
|
|
72
|
+
const handler = () => {
|
|
73
|
+
this.#updating = true;
|
|
74
|
+
node.value = this.#getInputValue(type);
|
|
75
|
+
this.#updating = false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.#input.addEventListener(eventType, handler);
|
|
79
|
+
if (eventType !== 'change') {
|
|
80
|
+
this.#input.addEventListener('change', handler);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#createInput(type) {
|
|
86
|
+
let input;
|
|
87
|
+
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'textarea':
|
|
90
|
+
input = document.createElement('textarea');
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case 'select':
|
|
94
|
+
input = document.createElement('select');
|
|
95
|
+
// Copy option children
|
|
96
|
+
for (const opt of this.querySelectorAll('option')) {
|
|
97
|
+
input.appendChild(opt.cloneNode(true));
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
default:
|
|
102
|
+
input = document.createElement('input');
|
|
103
|
+
input.type = type;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Copy attributes
|
|
107
|
+
for (const attr of ['placeholder', 'class', 'id', 'name', 'min', 'max', 'step', 'pattern', 'required', 'disabled', 'readonly']) {
|
|
108
|
+
const val = this.getAttribute(attr);
|
|
109
|
+
if (val !== null) input.setAttribute(attr, val);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.replaceWith(input);
|
|
113
|
+
return input;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#setInputValue(value, type) {
|
|
117
|
+
if (!this.#input) return;
|
|
118
|
+
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'checkbox':
|
|
121
|
+
this.#input.checked = value === 'true' || value === '1';
|
|
122
|
+
break;
|
|
123
|
+
case 'radio':
|
|
124
|
+
this.#input.checked = this.#input.value === value;
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
this.#input.value = value ?? '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#getInputValue(type) {
|
|
132
|
+
if (!this.#input) return '';
|
|
133
|
+
|
|
134
|
+
switch (type) {
|
|
135
|
+
case 'checkbox':
|
|
136
|
+
return this.#input.checked ? 'true' : 'false';
|
|
137
|
+
case 'radio':
|
|
138
|
+
return this.#input.checked ? this.#input.value : '';
|
|
139
|
+
default:
|
|
140
|
+
return this.#input.value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get input() {
|
|
145
|
+
return this.#input;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
focus() {
|
|
149
|
+
this.#input?.focus();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
customElements.define('f-field', FField);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FStore - Main XML state container
|
|
3
|
+
*
|
|
4
|
+
* @element f-state
|
|
5
|
+
*
|
|
6
|
+
* @attr {string} key - Storage key for persistence
|
|
7
|
+
* @attr {string} src - URL to load XML from
|
|
8
|
+
* @attr {number} autosave - Autosave debounce in ms
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <f-state key="myapp" autosave="500">
|
|
12
|
+
* <User>
|
|
13
|
+
* <n>Alice</n>
|
|
14
|
+
* <Role>Developer</Role>
|
|
15
|
+
* </User>
|
|
16
|
+
* </f-state>
|
|
17
|
+
*
|
|
18
|
+
* <script>
|
|
19
|
+
* const store = document.querySelector('f-state');
|
|
20
|
+
*
|
|
21
|
+
* store.state.when('ready', () => {
|
|
22
|
+
* const name = store.at('User.Name');
|
|
23
|
+
* name.subscribe(v => console.log('Name:', v));
|
|
24
|
+
* });
|
|
25
|
+
* </script>
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import Signal from '../core/Signal.js';
|
|
29
|
+
import State from '../core/State.js';
|
|
30
|
+
import Tree from '../xml/Tree.js';
|
|
31
|
+
import Node, { AttrNode } from '../xml/Node.js';
|
|
32
|
+
import * as Path from '../xml/Path.js';
|
|
33
|
+
import * as Persist from '../sync/Persist.js';
|
|
34
|
+
import Channel, { Protocol } from '../sync/Channel.js';
|
|
35
|
+
|
|
36
|
+
export default class FStore extends HTMLElement {
|
|
37
|
+
#tree;
|
|
38
|
+
#state = new State();
|
|
39
|
+
#actions = new Map();
|
|
40
|
+
#storage = null;
|
|
41
|
+
#channel = null;
|
|
42
|
+
#saveDebounced = null;
|
|
43
|
+
#parser = new DOMParser();
|
|
44
|
+
|
|
45
|
+
// Expose for scoped stores
|
|
46
|
+
static Node = Node;
|
|
47
|
+
static AttrNode = AttrNode;
|
|
48
|
+
static Path = Path;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super();
|
|
52
|
+
this.#tree = new Tree(this);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
connectedCallback() {
|
|
56
|
+
this.#initialize();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
this.#channel?.close();
|
|
61
|
+
this.#tree.dispose();
|
|
62
|
+
this.#state.dispose();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #initialize() {
|
|
66
|
+
const key = this.getAttribute('key');
|
|
67
|
+
const src = this.getAttribute('src');
|
|
68
|
+
const autosave = parseInt(this.getAttribute('autosave') || '0', 10);
|
|
69
|
+
|
|
70
|
+
// Setup persistence
|
|
71
|
+
if (key) {
|
|
72
|
+
this.#storage = Persist.auto();
|
|
73
|
+
|
|
74
|
+
// Try to restore from storage
|
|
75
|
+
const stored = await this.#storage.get(key);
|
|
76
|
+
if (stored) {
|
|
77
|
+
this.#applyXML(stored, false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Setup cross-tab sync
|
|
81
|
+
this.#channel = new Channel(key);
|
|
82
|
+
this.#channel.onMessage(msg => {
|
|
83
|
+
if (msg.type === Protocol.UPDATE) {
|
|
84
|
+
this.#applyXML(msg.xml, false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Load from URL if no stored state
|
|
90
|
+
if (src && this.#tree.empty) {
|
|
91
|
+
await this.load(src);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Parse inline XML if present
|
|
95
|
+
if (this.#tree.empty && this.innerHTML.trim()) {
|
|
96
|
+
this.#parseInline();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Setup autosave
|
|
100
|
+
if (autosave > 0 && key) {
|
|
101
|
+
this.#saveDebounced = Persist.debounce(() => this.#save(), autosave);
|
|
102
|
+
this.#tree.onChange(() => this.#saveDebounced());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Mark ready
|
|
106
|
+
this.#state.set('ready');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#parseInline() {
|
|
110
|
+
const xml = this.innerHTML;
|
|
111
|
+
const doc = this.#parser.parseFromString(
|
|
112
|
+
`<root>${xml}</root>`,
|
|
113
|
+
'application/xml'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const error = doc.querySelector('parsererror');
|
|
117
|
+
if (error) {
|
|
118
|
+
console.error('Flarp parse error:', error.textContent);
|
|
119
|
+
this.#state.set('error');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.innerHTML = '';
|
|
124
|
+
for (const child of Array.from(doc.documentElement.children)) {
|
|
125
|
+
this.appendChild(document.importNode(child, true));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#applyXML(xml, broadcast = true) {
|
|
130
|
+
const doc = this.#parser.parseFromString(
|
|
131
|
+
`<root>${xml}</root>`,
|
|
132
|
+
'application/xml'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const error = doc.querySelector('parsererror');
|
|
136
|
+
if (error) {
|
|
137
|
+
this.#state.set('error');
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.innerHTML = '';
|
|
142
|
+
for (const child of Array.from(doc.documentElement.children)) {
|
|
143
|
+
this.appendChild(document.importNode(child, true));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (broadcast && this.#channel) {
|
|
147
|
+
this.#channel.send(Protocol.update(xml));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async #save() {
|
|
154
|
+
const key = this.getAttribute('key');
|
|
155
|
+
if (!key || !this.#storage) return;
|
|
156
|
+
|
|
157
|
+
const xml = this.serialize();
|
|
158
|
+
await this.#storage.set(key, xml);
|
|
159
|
+
|
|
160
|
+
if (this.#channel) {
|
|
161
|
+
this.#channel.send(Protocol.update(xml));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.#state.set('saved');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ========== Public API ==========
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get state machine
|
|
171
|
+
*/
|
|
172
|
+
get state() {
|
|
173
|
+
return this.#state;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get reactive node at path
|
|
178
|
+
* @param {string} path
|
|
179
|
+
* @returns {Node|AttrNode|null}
|
|
180
|
+
*/
|
|
181
|
+
at(path) {
|
|
182
|
+
return this.#tree.at(path);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get all nodes matching path
|
|
187
|
+
* @param {string} path
|
|
188
|
+
* @returns {Node[]}
|
|
189
|
+
*/
|
|
190
|
+
query(path) {
|
|
191
|
+
return this.#tree.query(path);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set value at path
|
|
196
|
+
* @param {string} path
|
|
197
|
+
* @param {string} value
|
|
198
|
+
*/
|
|
199
|
+
set(path, value) {
|
|
200
|
+
this.#tree.set(path, value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Add XML fragment
|
|
205
|
+
* @param {string} parentPath
|
|
206
|
+
* @param {string} xml
|
|
207
|
+
* @returns {Node|Node[]}
|
|
208
|
+
*/
|
|
209
|
+
add(parentPath, xml) {
|
|
210
|
+
return this.#tree.add(parentPath, xml);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Remove node at path
|
|
215
|
+
* @param {string} path
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
remove(path) {
|
|
219
|
+
return this.#tree.remove(path);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Serialize entire state
|
|
224
|
+
* @returns {string}
|
|
225
|
+
*/
|
|
226
|
+
serialize() {
|
|
227
|
+
return this.innerHTML;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get/set raw XML
|
|
232
|
+
*/
|
|
233
|
+
get xml() {
|
|
234
|
+
return this.serialize();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
set xml(value) {
|
|
238
|
+
this.#applyXML(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Load from URL
|
|
243
|
+
* @param {string} url
|
|
244
|
+
*/
|
|
245
|
+
async load(url) {
|
|
246
|
+
this.#state.set('loading');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(url || this.getAttribute('src'));
|
|
250
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
251
|
+
|
|
252
|
+
const text = await response.text();
|
|
253
|
+
this.#applyXML(text);
|
|
254
|
+
this.#state.clear('loading');
|
|
255
|
+
this.#state.set('loaded');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Flarp load error:', error);
|
|
258
|
+
this.#state.clear('loading');
|
|
259
|
+
this.#state.set('error');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Force save to storage
|
|
265
|
+
*/
|
|
266
|
+
async save() {
|
|
267
|
+
await this.#save();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clear stored state
|
|
272
|
+
*/
|
|
273
|
+
async clear() {
|
|
274
|
+
const key = this.getAttribute('key');
|
|
275
|
+
if (key && this.#storage) {
|
|
276
|
+
await this.#storage.remove(key);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ========== Event Sourcing ==========
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Register action handler
|
|
284
|
+
* @param {string} action
|
|
285
|
+
* @param {Function} handler
|
|
286
|
+
* @returns {Function} Unsubscribe
|
|
287
|
+
*/
|
|
288
|
+
on(action, handler) {
|
|
289
|
+
if (!this.#actions.has(action)) {
|
|
290
|
+
this.#actions.set(action, new Set());
|
|
291
|
+
}
|
|
292
|
+
this.#actions.get(action).add(handler);
|
|
293
|
+
return () => this.#actions.get(action)?.delete(handler);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Emit action
|
|
298
|
+
* @param {string} action
|
|
299
|
+
* @param {*} payload
|
|
300
|
+
*/
|
|
301
|
+
emit(action, payload) {
|
|
302
|
+
const handlers = this.#actions.get(action);
|
|
303
|
+
if (handlers) {
|
|
304
|
+
for (const handler of handlers) {
|
|
305
|
+
try {
|
|
306
|
+
handler.call(this, payload, this);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.error(`Action error (${action}):`, e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Also dispatch as DOM event
|
|
314
|
+
this.dispatchEvent(new CustomEvent(`action:${action}`, {
|
|
315
|
+
detail: payload,
|
|
316
|
+
bubbles: true
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ========== Tree subscription ==========
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Subscribe to any tree change
|
|
324
|
+
* @param {Function} fn
|
|
325
|
+
* @returns {Function} Unsubscribe
|
|
326
|
+
*/
|
|
327
|
+
onChange(fn) {
|
|
328
|
+
return this.#tree.onChange(fn);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
customElements.define('f-state', FStore);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FText - Reactive text display
|
|
3
|
+
*
|
|
4
|
+
* @element f-text
|
|
5
|
+
*
|
|
6
|
+
* @attr {string} path - XML path to display
|
|
7
|
+
* @attr {string} store - Optional store ID reference
|
|
8
|
+
* @attr {string} format - Optional format (uppercase, lowercase, number, etc.)
|
|
9
|
+
* @attr {string} default - Default value if path not found
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <f-text path="User.Name"></f-text>
|
|
13
|
+
* <f-text path="Product.Price" format="currency"></f-text>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { findStore } from '../dom/find.js';
|
|
17
|
+
|
|
18
|
+
const FORMATS = {
|
|
19
|
+
uppercase: v => String(v).toUpperCase(),
|
|
20
|
+
lowercase: v => String(v).toLowerCase(),
|
|
21
|
+
capitalize: v => String(v).charAt(0).toUpperCase() + String(v).slice(1),
|
|
22
|
+
number: v => Number(v).toLocaleString(),
|
|
23
|
+
currency: v => Number(v).toLocaleString(undefined, { style: 'currency', currency: 'USD' }),
|
|
24
|
+
percent: v => Number(v).toLocaleString(undefined, { style: 'percent' }),
|
|
25
|
+
trim: v => String(v).trim(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default class FText extends HTMLElement {
|
|
29
|
+
#unsubscribe = null;
|
|
30
|
+
|
|
31
|
+
connectedCallback() {
|
|
32
|
+
this.#connect();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
disconnectedCallback() {
|
|
36
|
+
this.#unsubscribe?.();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#connect() {
|
|
40
|
+
const path = this.getAttribute('path');
|
|
41
|
+
const format = this.getAttribute('format');
|
|
42
|
+
const defaultValue = this.getAttribute('default') || '';
|
|
43
|
+
|
|
44
|
+
const store = this._store || findStore(this);
|
|
45
|
+
if (!store) {
|
|
46
|
+
console.warn('<f-text> could not find store');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wait for store to be ready
|
|
51
|
+
store.state.when('ready', () => {
|
|
52
|
+
const node = store.at(path);
|
|
53
|
+
|
|
54
|
+
if (!node) {
|
|
55
|
+
this.textContent = defaultValue;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.#unsubscribe = node.subscribe(value => {
|
|
60
|
+
const formatted = this.#format(value ?? defaultValue, format);
|
|
61
|
+
this.textContent = formatted;
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#format(value, format) {
|
|
67
|
+
if (!format) return value;
|
|
68
|
+
const formatter = FORMATS[format];
|
|
69
|
+
return formatter ? formatter(value) : value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
customElements.define('f-text', FText);
|