@devera_se/bedrockjs 0.1.1

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Reactive state management system
3
+ */
4
+
5
+ // Current watcher being tracked
6
+ let currentWatcher = null;
7
+
8
+ // Set of all active watchers
9
+ const watchers = new Set();
10
+
11
+ // Map of reactive objects to their dependency sets
12
+ const dependencyMap = new WeakMap();
13
+
14
+ /**
15
+ * Create a reactive proxy that triggers updates on change
16
+ * @param {Object} target - Object to make reactive
17
+ * @returns {Proxy} - Reactive proxy
18
+ */
19
+ export function reactive(target) {
20
+ if (typeof target !== 'object' || target === null) {
21
+ return target;
22
+ }
23
+
24
+ // Already a proxy
25
+ if (target.__isReactive) {
26
+ return target;
27
+ }
28
+
29
+ const deps = new Map(); // property -> Set of watchers
30
+ dependencyMap.set(target, deps);
31
+
32
+ const proxy = new Proxy(target, {
33
+ get(obj, prop) {
34
+ if (prop === '__isReactive') return true;
35
+ if (prop === '__target') return obj;
36
+
37
+ // Track dependency
38
+ if (currentWatcher) {
39
+ if (!deps.has(prop)) {
40
+ deps.set(prop, new Set());
41
+ }
42
+ deps.get(prop).add(currentWatcher);
43
+ currentWatcher.deps.add(deps.get(prop));
44
+ }
45
+
46
+ const value = obj[prop];
47
+
48
+ // Recursively make nested objects reactive
49
+ if (typeof value === 'object' && value !== null && !value.__isReactive) {
50
+ obj[prop] = reactive(value);
51
+ return obj[prop];
52
+ }
53
+
54
+ return value;
55
+ },
56
+
57
+ set(obj, prop, value) {
58
+ const oldValue = obj[prop];
59
+
60
+ // Make nested objects reactive
61
+ if (typeof value === 'object' && value !== null) {
62
+ value = reactive(value);
63
+ }
64
+
65
+ obj[prop] = value;
66
+
67
+ // Trigger watchers if value changed
68
+ if (oldValue !== value && deps.has(prop)) {
69
+ const propDeps = deps.get(prop);
70
+ for (const watcher of propDeps) {
71
+ queueWatcher(watcher);
72
+ }
73
+ }
74
+
75
+ return true;
76
+ },
77
+
78
+ deleteProperty(obj, prop) {
79
+ if (prop in obj) {
80
+ delete obj[prop];
81
+ if (deps.has(prop)) {
82
+ const propDeps = deps.get(prop);
83
+ for (const watcher of propDeps) {
84
+ queueWatcher(watcher);
85
+ }
86
+ }
87
+ }
88
+ return true;
89
+ }
90
+ });
91
+
92
+ return proxy;
93
+ }
94
+
95
+ /**
96
+ * Watch for reactive changes and run callback
97
+ * @param {Function} fn - Function to run and track
98
+ * @param {Object} options - Options
99
+ * @returns {Function} - Stop watching function
100
+ */
101
+ export function watch(fn, options = {}) {
102
+ const watcher = {
103
+ fn,
104
+ deps: new Set(),
105
+ active: true,
106
+ immediate: options.immediate !== false
107
+ };
108
+
109
+ watchers.add(watcher);
110
+
111
+ // Run immediately to collect dependencies
112
+ if (watcher.immediate) {
113
+ runWatcher(watcher);
114
+ }
115
+
116
+ // Return cleanup function
117
+ return () => {
118
+ watcher.active = false;
119
+ watchers.delete(watcher);
120
+ cleanupWatcher(watcher);
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Run a watcher and track its dependencies
126
+ */
127
+ function runWatcher(watcher) {
128
+ if (!watcher.active) return;
129
+
130
+ // Cleanup old dependencies
131
+ cleanupWatcher(watcher);
132
+
133
+ // Track new dependencies
134
+ const prevWatcher = currentWatcher;
135
+ currentWatcher = watcher;
136
+
137
+ try {
138
+ watcher.fn();
139
+ } finally {
140
+ currentWatcher = prevWatcher;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Clean up watcher dependencies
146
+ */
147
+ function cleanupWatcher(watcher) {
148
+ for (const dep of watcher.deps) {
149
+ dep.delete(watcher);
150
+ }
151
+ watcher.deps.clear();
152
+ }
153
+
154
+ // Batch updates using microtask queue
155
+ let pendingWatchers = new Set();
156
+ let isPending = false;
157
+
158
+ /**
159
+ * Queue a watcher to run in the next microtask
160
+ */
161
+ function queueWatcher(watcher) {
162
+ if (!watcher.active) return;
163
+
164
+ pendingWatchers.add(watcher);
165
+
166
+ if (!isPending) {
167
+ isPending = true;
168
+ queueMicrotask(flushWatchers);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Flush all pending watchers
174
+ */
175
+ function flushWatchers() {
176
+ const watchersToRun = [...pendingWatchers];
177
+ pendingWatchers.clear();
178
+ isPending = false;
179
+
180
+ for (const watcher of watchersToRun) {
181
+ runWatcher(watcher);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Create a computed value that caches and auto-updates
187
+ * @param {Function} getter - Getter function
188
+ * @returns {Object} - Object with .value property
189
+ */
190
+ export function computed(getter) {
191
+ let cachedValue;
192
+ let dirty = true;
193
+
194
+ const watcher = {
195
+ fn: () => {
196
+ dirty = true;
197
+ // Trigger any watchers watching this computed
198
+ if (computedDeps.size > 0) {
199
+ for (const dep of computedDeps) {
200
+ queueWatcher(dep);
201
+ }
202
+ }
203
+ },
204
+ deps: new Set(),
205
+ active: true,
206
+ immediate: false
207
+ };
208
+
209
+ watchers.add(watcher);
210
+ const computedDeps = new Set();
211
+
212
+ return {
213
+ get value() {
214
+ // Track dependency on this computed
215
+ if (currentWatcher) {
216
+ computedDeps.add(currentWatcher);
217
+ }
218
+
219
+ if (dirty) {
220
+ const prevWatcher = currentWatcher;
221
+ currentWatcher = watcher;
222
+ cleanupWatcher(watcher);
223
+
224
+ try {
225
+ cachedValue = getter();
226
+ } finally {
227
+ currentWatcher = prevWatcher;
228
+ }
229
+
230
+ dirty = false;
231
+ }
232
+
233
+ return cachedValue;
234
+ },
235
+
236
+ stop() {
237
+ watcher.active = false;
238
+ watchers.delete(watcher);
239
+ cleanupWatcher(watcher);
240
+ computedDeps.clear();
241
+ }
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Create a simple signal (reactive value)
247
+ * @param {any} initialValue - Initial value
248
+ * @returns {[Function, Function]} - [getter, setter]
249
+ */
250
+ export function signal(initialValue) {
251
+ const state = reactive({ value: initialValue });
252
+
253
+ const get = () => state.value;
254
+ const set = (newValue) => {
255
+ state.value = newValue;
256
+ };
257
+
258
+ return [get, set];
259
+ }
260
+
261
+ /**
262
+ * Batch multiple updates into a single flush
263
+ * @param {Function} fn - Function containing updates
264
+ */
265
+ export function batch(fn) {
266
+ const prevPending = isPending;
267
+ isPending = true;
268
+
269
+ try {
270
+ fn();
271
+ } finally {
272
+ if (!prevPending) {
273
+ isPending = false;
274
+ queueMicrotask(flushWatchers);
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,5 @@
1
+ import type { TemplateResult } from './html.js';
2
+
3
+ export function render(result: TemplateResult, container: Element): void;
4
+
5
+ export function keyed(key: any, template: TemplateResult): TemplateResult;
package/src/render.js ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * DOM rendering and patching engine
3
+ */
4
+
5
+ import { isTemplateResult } from './html.js';
6
+
7
+ // Store instance state keyed by container
8
+ const instanceMap = new WeakMap();
9
+
10
+ // Unique key for tracking array items
11
+ const KEY_SYMBOL = Symbol('bedrock-key');
12
+
13
+ /**
14
+ * Render a template result into a container
15
+ * @param {TemplateResult} result - The template to render
16
+ * @param {Element} container - The container element
17
+ */
18
+ export function render(result, container) {
19
+ let instance = instanceMap.get(container);
20
+
21
+ if (!instance) {
22
+ // First render - create new instance
23
+ instance = createInstance(result, container);
24
+ instanceMap.set(container, instance);
25
+ } else if (instance.strings === result.strings) {
26
+ // Same template - update values only
27
+ updateInstance(instance, result.values);
28
+ } else {
29
+ // Different template - replace entirely
30
+ container.innerHTML = '';
31
+ instance = createInstance(result, container);
32
+ instanceMap.set(container, instance);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Create a new template instance
38
+ */
39
+ function createInstance(result, container) {
40
+ const template = result.getTemplate();
41
+ const fragment = template.element.content.cloneNode(true);
42
+
43
+ // Resolve node paths to actual nodes in the fragment
44
+ const parts = template.parts.map((part) => {
45
+ if (!part) return null;
46
+ const node = getNodeByPath(fragment, part.path);
47
+ return { ...part, node, value: undefined };
48
+ });
49
+
50
+ // Apply initial values before adding to DOM
51
+ for (let i = 0; i < result.values.length; i++) {
52
+ if (parts[i]) {
53
+ applyValue(parts[i], result.values[i]);
54
+ }
55
+ }
56
+
57
+ container.appendChild(fragment);
58
+
59
+ return {
60
+ strings: result.strings,
61
+ parts,
62
+ container
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Update an existing instance with new values
68
+ */
69
+ function updateInstance(instance, values) {
70
+ for (let i = 0; i < values.length; i++) {
71
+ const part = instance.parts[i];
72
+ if (part && part.value !== values[i]) {
73
+ applyValue(part, values[i]);
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Apply a value to a part
80
+ */
81
+ function applyValue(part, value) {
82
+ const oldValue = part.value;
83
+ part.value = value;
84
+
85
+ switch (part.type) {
86
+ case 'attribute':
87
+ applyAttribute(part.node, part.name, value);
88
+ break;
89
+ case 'property':
90
+ part.node[part.name] = value;
91
+ break;
92
+ case 'event':
93
+ applyEvent(part, value, oldValue);
94
+ break;
95
+ case 'node':
96
+ applyNode(part, value, oldValue);
97
+ break;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Apply an attribute value
103
+ */
104
+ function applyAttribute(node, name, value) {
105
+ if (value === null || value === undefined || value === false) {
106
+ node.removeAttribute(name);
107
+ } else if (value === true) {
108
+ node.setAttribute(name, '');
109
+ } else {
110
+ node.setAttribute(name, String(value));
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Apply an event handler
116
+ */
117
+ function applyEvent(part, value, oldValue) {
118
+ if (oldValue) {
119
+ part.node.removeEventListener(part.name, oldValue);
120
+ }
121
+ if (value) {
122
+ part.node.addEventListener(part.name, value);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Apply a node value (text, template, or array)
128
+ */
129
+ function applyNode(part, value, oldValue) {
130
+ const node = part.node;
131
+
132
+ if (value === null || value === undefined) {
133
+ clearNodePart(part);
134
+ } else if (isTemplateResult(value)) {
135
+ applyTemplateNode(part, value);
136
+ } else if (Array.isArray(value)) {
137
+ applyArrayNode(part, value);
138
+ } else {
139
+ // Primitive value - render as text
140
+ clearNodePart(part);
141
+ const textNode = document.createTextNode(String(value));
142
+ node.parentNode.insertBefore(textNode, node);
143
+ part.nodes = [textNode];
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Clear any rendered content for a node part
149
+ */
150
+ function clearNodePart(part) {
151
+ if (part.nodes) {
152
+ part.nodes.forEach(n => n.remove());
153
+ part.nodes = null;
154
+ }
155
+ if (part.templateInstance) {
156
+ part.templateInstance = null;
157
+ }
158
+ if (part.arrayItems) {
159
+ part.arrayItems.forEach(item => item.nodes.forEach(n => n.remove()));
160
+ part.arrayItems = null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Apply a template result to a node part
166
+ */
167
+ function applyTemplateNode(part, value) {
168
+ const marker = part.node;
169
+
170
+ // Check if we have an existing template instance with same structure
171
+ if (part.templateInstance && part.templateInstance.strings === value.strings) {
172
+ // Update existing template
173
+ updateInstance(part.templateInstance, value.values);
174
+ return;
175
+ }
176
+
177
+ // Clear any existing content
178
+ clearNodePart(part);
179
+
180
+ // Create new template instance
181
+ const template = value.getTemplate();
182
+ const fragment = template.element.content.cloneNode(true);
183
+
184
+ const parts = template.parts.map((p) => {
185
+ if (!p) return null;
186
+ const n = getNodeByPath(fragment, p.path);
187
+ return { ...p, node: n, value: undefined };
188
+ });
189
+
190
+ // Apply values
191
+ for (let i = 0; i < value.values.length; i++) {
192
+ if (parts[i]) {
193
+ applyValue(parts[i], value.values[i]);
194
+ }
195
+ }
196
+
197
+ // Track nodes being inserted
198
+ const nodes = Array.from(fragment.childNodes);
199
+ marker.parentNode.insertBefore(fragment, marker);
200
+
201
+ part.nodes = nodes;
202
+ part.templateInstance = { strings: value.strings, parts };
203
+ }
204
+
205
+ /**
206
+ * Apply an array of values/templates to a node part
207
+ */
208
+ function applyArrayNode(part, values) {
209
+ const marker = part.node;
210
+ const parent = marker.parentNode;
211
+ const oldItems = part.arrayItems || [];
212
+
213
+ // Build map of old items by key
214
+ const oldItemsByKey = new Map();
215
+ for (const item of oldItems) {
216
+ if (item.key !== undefined) {
217
+ oldItemsByKey.set(item.key, item);
218
+ }
219
+ }
220
+
221
+ const newItems = [];
222
+
223
+ // Process each new value
224
+ for (let i = 0; i < values.length; i++) {
225
+ const value = values[i];
226
+ const key = value && value[KEY_SYMBOL] !== undefined ? value[KEY_SYMBOL] : i;
227
+
228
+ let item = oldItemsByKey.get(key);
229
+
230
+ if (item) {
231
+ // Reuse existing item
232
+ oldItemsByKey.delete(key);
233
+ if (isTemplateResult(value)) {
234
+ if (item.instance && item.instance.strings === value.strings) {
235
+ updateInstance(item.instance, value.values);
236
+ } else {
237
+ // Different template - recreate
238
+ item.nodes.forEach(n => n.remove());
239
+ item = createArrayItem(value, key, marker, parent);
240
+ }
241
+ } else {
242
+ // Update text content
243
+ if (item.nodes[0]) {
244
+ item.nodes[0].textContent = String(value ?? '');
245
+ }
246
+ }
247
+ } else {
248
+ // Create new item
249
+ item = createArrayItem(value, key, marker, parent);
250
+ }
251
+
252
+ newItems.push(item);
253
+ }
254
+
255
+ // Remove old items that are no longer present
256
+ for (const item of oldItemsByKey.values()) {
257
+ item.nodes.forEach(n => n.remove());
258
+ }
259
+
260
+ // Reorder items to be before the marker
261
+ for (const item of newItems) {
262
+ for (const itemNode of item.nodes) {
263
+ parent.insertBefore(itemNode, marker);
264
+ }
265
+ }
266
+
267
+ part.arrayItems = newItems;
268
+ }
269
+
270
+ /**
271
+ * Create a new array item
272
+ */
273
+ function createArrayItem(value, key, marker, parent) {
274
+ if (isTemplateResult(value)) {
275
+ const template = value.getTemplate();
276
+ const fragment = template.element.content.cloneNode(true);
277
+
278
+ const parts = template.parts.map((p) => {
279
+ if (!p) return null;
280
+ const n = getNodeByPath(fragment, p.path);
281
+ return { ...p, node: n, value: undefined };
282
+ });
283
+
284
+ for (let i = 0; i < value.values.length; i++) {
285
+ if (parts[i]) {
286
+ applyValue(parts[i], value.values[i]);
287
+ }
288
+ }
289
+
290
+ const nodes = Array.from(fragment.childNodes);
291
+ parent.insertBefore(fragment, marker);
292
+
293
+ return {
294
+ key,
295
+ nodes,
296
+ instance: { strings: value.strings, parts }
297
+ };
298
+ } else {
299
+ const textNode = document.createTextNode(String(value ?? ''));
300
+ parent.insertBefore(textNode, marker);
301
+ return { key, nodes: [textNode], instance: null };
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Get a node by path from a root
307
+ */
308
+ function getNodeByPath(root, path) {
309
+ let node = root;
310
+ for (const index of path) {
311
+ if (!node.childNodes) return null;
312
+ node = node.childNodes[index];
313
+ if (!node) return null;
314
+ }
315
+ return node;
316
+ }
317
+
318
+ /**
319
+ * Create a keyed item for array rendering
320
+ * @param {any} key - Unique key for this item
321
+ * @param {TemplateResult} template - The template result
322
+ */
323
+ export function keyed(key, template) {
324
+ template[KEY_SYMBOL] = key;
325
+ return template;
326
+ }
@@ -0,0 +1,52 @@
1
+ import type { Component } from './component.js';
2
+
3
+ export interface RouteDefinition {
4
+ path: string;
5
+ component: string;
6
+ loader?: (params: Record<string, string>) => any | Promise<any>;
7
+ [key: string]: any;
8
+ }
9
+
10
+ export interface RouterOptions {
11
+ routes?: RouteDefinition[];
12
+ hash?: boolean;
13
+ base?: string;
14
+ }
15
+
16
+ export interface NavigationOptions {
17
+ replace?: boolean;
18
+ }
19
+
20
+ export class Router {
21
+ constructor(options?: RouterOptions);
22
+
23
+ start(): this;
24
+ stop(): void;
25
+ setOutlet(outlet: RouterOutlet | Element): void;
26
+
27
+ readonly currentPath: string;
28
+ navigate(path: string, options?: NavigationOptions): void;
29
+
30
+ addRoute(route: RouteDefinition): void;
31
+ removeRoute(path: string): void;
32
+
33
+ readonly routes: RouteDefinition[];
34
+ readonly useHash: boolean;
35
+
36
+ static instance: Router | null;
37
+ currentRoute?: RouteDefinition & { params?: Record<string, string> };
38
+ }
39
+
40
+ export class RouterOutlet extends Component {}
41
+
42
+ export class RouterLink extends Component {
43
+ to?: string;
44
+ replace: boolean;
45
+ readonly href: string;
46
+ }
47
+
48
+ export function createRouter(options?: RouterOptions): Router;
49
+
50
+ export function navigate(path: string, options?: NavigationOptions): void;
51
+
52
+ export function getParams(): Record<string, string>;