@granularjs/core 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.
Files changed (70) hide show
  1. package/README.md +576 -0
  2. package/dist/granular.min.js +2 -0
  3. package/dist/granular.min.js.map +7 -0
  4. package/package.json +54 -0
  5. package/src/core/bootstrap.js +63 -0
  6. package/src/core/collections/observable-array.js +204 -0
  7. package/src/core/component/function-component.js +82 -0
  8. package/src/core/context.js +172 -0
  9. package/src/core/dom/dom.js +25 -0
  10. package/src/core/dom/element.js +725 -0
  11. package/src/core/dom/error-boundary.js +111 -0
  12. package/src/core/dom/input-format.js +82 -0
  13. package/src/core/dom/list.js +185 -0
  14. package/src/core/dom/portal.js +57 -0
  15. package/src/core/dom/tags.js +182 -0
  16. package/src/core/dom/virtual-list.js +242 -0
  17. package/src/core/dom/when.js +138 -0
  18. package/src/core/events/event-hub.js +97 -0
  19. package/src/core/forms/form.js +127 -0
  20. package/src/core/internal/symbols.js +5 -0
  21. package/src/core/network/websocket.js +165 -0
  22. package/src/core/query/query-client.js +529 -0
  23. package/src/core/reactivity/after-flush.js +20 -0
  24. package/src/core/reactivity/computed.js +51 -0
  25. package/src/core/reactivity/concat.js +89 -0
  26. package/src/core/reactivity/dirty-host.js +162 -0
  27. package/src/core/reactivity/observe.js +421 -0
  28. package/src/core/reactivity/persist.js +180 -0
  29. package/src/core/reactivity/resolve.js +8 -0
  30. package/src/core/reactivity/signal.js +97 -0
  31. package/src/core/reactivity/state.js +294 -0
  32. package/src/core/renderable/render-string.js +51 -0
  33. package/src/core/renderable/renderable.js +21 -0
  34. package/src/core/renderable/renderer.js +66 -0
  35. package/src/core/router/router.js +865 -0
  36. package/src/core/runtime.js +28 -0
  37. package/src/index.js +42 -0
  38. package/types/core/bootstrap.d.ts +11 -0
  39. package/types/core/collections/observable-array.d.ts +25 -0
  40. package/types/core/component/function-component.d.ts +14 -0
  41. package/types/core/context.d.ts +29 -0
  42. package/types/core/dom/dom.d.ts +13 -0
  43. package/types/core/dom/element.d.ts +10 -0
  44. package/types/core/dom/error-boundary.d.ts +8 -0
  45. package/types/core/dom/input-format.d.ts +6 -0
  46. package/types/core/dom/list.d.ts +8 -0
  47. package/types/core/dom/portal.d.ts +8 -0
  48. package/types/core/dom/tags.d.ts +114 -0
  49. package/types/core/dom/virtual-list.d.ts +8 -0
  50. package/types/core/dom/when.d.ts +13 -0
  51. package/types/core/events/event-hub.d.ts +48 -0
  52. package/types/core/forms/form.d.ts +9 -0
  53. package/types/core/internal/symbols.d.ts +4 -0
  54. package/types/core/network/websocket.d.ts +18 -0
  55. package/types/core/query/query-client.d.ts +73 -0
  56. package/types/core/reactivity/after-flush.d.ts +4 -0
  57. package/types/core/reactivity/computed.d.ts +1 -0
  58. package/types/core/reactivity/concat.d.ts +1 -0
  59. package/types/core/reactivity/dirty-host.d.ts +42 -0
  60. package/types/core/reactivity/observe.d.ts +10 -0
  61. package/types/core/reactivity/persist.d.ts +1 -0
  62. package/types/core/reactivity/resolve.d.ts +1 -0
  63. package/types/core/reactivity/signal.d.ts +11 -0
  64. package/types/core/reactivity/state.d.ts +14 -0
  65. package/types/core/renderable/render-string.d.ts +2 -0
  66. package/types/core/renderable/renderable.d.ts +15 -0
  67. package/types/core/renderable/renderer.d.ts +38 -0
  68. package/types/core/router/router.d.ts +57 -0
  69. package/types/core/runtime.d.ts +26 -0
  70. package/types/index.d.ts +2 -0
@@ -0,0 +1,51 @@
1
+ import { after } from './observe.js';
2
+ import { isSignal, readSignal } from './signal.js';
3
+ import { isState, isStatePath, isComputed, readState, state } from './state.js';
4
+
5
+ function asComputed(value) {
6
+ if (isComputed(value)) return value;
7
+ if (isSignal(value)) {
8
+ const current = readSignal(value);
9
+ if (typeof current === 'function') {
10
+ return (...args) => {
11
+ const next = readSignal(value);
12
+ if (typeof next === 'function') return next(...args);
13
+ return undefined;
14
+ };
15
+ }
16
+ return after(value).compute((next) => next);
17
+ }
18
+ if (isState(value) || isStatePath(value)) {
19
+ const current = readState(value);
20
+ if (typeof current === 'function') {
21
+ return (...args) => {
22
+ const next = readState(value);
23
+ if (typeof next === 'function') return next(...args);
24
+ return undefined;
25
+ };
26
+ }
27
+ return after(value).compute((next) => next);
28
+ }
29
+ if (typeof value === 'function') return value;
30
+ return after(state(value)).compute((next) => next);
31
+ }
32
+
33
+ export function computed(input) {
34
+ if (isSignal(input) || isState(input) || isStatePath(input)) {
35
+ return asComputed(input);
36
+ }
37
+ if (!input || typeof input !== 'object') {
38
+ return asComputed(input);
39
+ }
40
+ const cache = new Map();
41
+ return new Proxy(input, {
42
+ get(target, prop) {
43
+ if (typeof prop === 'symbol') return target[prop];
44
+ if (cache.has(prop)) return cache.get(prop);
45
+ const value = target[prop];
46
+ const resolved = asComputed(value);
47
+ cache.set(prop, resolved);
48
+ return resolved;
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,89 @@
1
+ import { after } from './observe.js';
2
+ import { resolve } from './resolve.js';
3
+ import { isSignal } from './signal.js';
4
+ import { isState, isStatePath, isComputed } from './state.js';
5
+
6
+ function isObject(value) {
7
+ return value !== null && typeof value === 'object';
8
+ }
9
+
10
+ function isReactive(value) {
11
+ return isSignal(value) || isState(value) || isStatePath(value) || isComputed(value);
12
+ }
13
+
14
+ function isTuple(value) {
15
+ if (!Array.isArray(value) || value.length !== 2) return false;
16
+ const source = value[0];
17
+ const mapper = value[1];
18
+ if (isReactive(source)) return typeof mapper === 'function' || typeof mapper === 'string';
19
+ return typeof mapper === 'function' || typeof mapper === 'string';
20
+ }
21
+
22
+ function normalizeParts(values, out = []) {
23
+ for (const value of values) {
24
+ if (isTuple(value)) {
25
+ out.push(value);
26
+ continue;
27
+ }
28
+ if (Array.isArray(value)) {
29
+ normalizeParts(value, out);
30
+ continue;
31
+ }
32
+ out.push(value);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function extractOptions(parts) {
38
+ if (!parts.length) return { parts, options: { separator: '', filterFalsy: false } };
39
+ const last = parts[parts.length - 1];
40
+ if (
41
+ isObject(last) &&
42
+ !Array.isArray(last) &&
43
+ !isReactive(last) &&
44
+ (Object.prototype.hasOwnProperty.call(last, 'separator') ||
45
+ Object.prototype.hasOwnProperty.call(last, 'filterFalsy'))
46
+ ) {
47
+ const options = {
48
+ separator: last.separator ?? '',
49
+ filterFalsy: last.filterFalsy ?? false,
50
+ };
51
+ return { parts: parts.slice(0, -1), options };
52
+ }
53
+ return { parts, options: { separator: '', filterFalsy: false } };
54
+ }
55
+
56
+ function collectTargets(value, targets) {
57
+ if (isReactive(value)) targets.push(value);
58
+ if (Array.isArray(value) && value.length) {
59
+ const source = value[0];
60
+ if (isReactive(source)) targets.push(source);
61
+ }
62
+ }
63
+
64
+ function resolvePart(part) {
65
+ if (Array.isArray(part)) {
66
+ const source = part[0];
67
+ const mapper = part[1];
68
+ const value = resolve(source);
69
+ if (typeof mapper === 'function') return mapper(value);
70
+ if (typeof mapper === 'string') return value ? mapper : '';
71
+ return value;
72
+ }
73
+ if (typeof part === 'function') return part();
74
+ return resolve(part);
75
+ }
76
+
77
+ export function concat(...input) {
78
+ const normalized = normalizeParts(input);
79
+ const { parts, options } = extractOptions(normalized);
80
+ const targets = [];
81
+ for (const part of parts) collectTargets(part, targets);
82
+ const build = () => {
83
+ const values = parts.map(resolvePart).map((value) => (value == null ? '' : String(value)));
84
+ const filtered = options.filterFalsy ? values.filter(Boolean) : values;
85
+ return filtered.join(options.separator);
86
+ };
87
+ if (!targets.length) return build();
88
+ return after(targets).compute(build);
89
+ }
@@ -0,0 +1,162 @@
1
+ import { AfterFlush } from './after-flush.js';
2
+ import { INTERNAL } from '../internal/symbols.js';
3
+ import { Renderable } from '../renderable/renderable.js';
4
+ import { isObservableArray } from '../collections/observable-array.js';
5
+ import { EventHub } from '../events/event-hub.js';
6
+
7
+ /**
8
+ * Base class that provides:
9
+ * - property instrumentation (dirty tracking)
10
+ * - microtask-batched flushing
11
+ * - subscription mechanism for template bindings
12
+ *
13
+ * This is part of the core runtime and is inherited by `Component`.
14
+ */
15
+ export class DirtyHost extends Renderable {
16
+ #dirty = new Set();
17
+ #scheduled = false;
18
+ #subscribers = new Map();
19
+ #boundProps = new Set();
20
+ #values = new Map();
21
+ #observableUnsubs = new Map();
22
+ #events = new EventHub();
23
+
24
+ /**
25
+ * Registers BEFORE hooks. Handlers may return false to cancel.
26
+ * Example: `store.before().set(({ prop, next }) => next !== null)`
27
+ */
28
+ before() {
29
+ return this.#events.phase('before');
30
+ }
31
+
32
+ /**
33
+ * Registers AFTER hooks.
34
+ * Example: `store.after().flush(({ props }) => console.log(props))`
35
+ */
36
+ after() {
37
+ return this.#events.phase('after');
38
+ }
39
+
40
+ emitBefore(type, payload, ctx) {
41
+ return this.#events.emitBefore(type, payload, ctx);
42
+ }
43
+
44
+ emitAfter(type, payload, ctx) {
45
+ this.#events.emitAfter(type, payload, ctx);
46
+ }
47
+
48
+ /**
49
+ * Batches multiple assignments into a single flush.
50
+ *
51
+ * @param {() => void} cb
52
+ */
53
+ set(cb) {
54
+ const wasScheduled = this.#scheduled;
55
+ this.#scheduled = true;
56
+ try {
57
+ cb();
58
+ } finally {
59
+ this.#scheduled = wasScheduled;
60
+ this.update();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Flushes all dirty properties, notifying any subscribers registered by bindings.
66
+ * Usually you don't need to call this manually, because assignments trigger a microtask flush.
67
+ */
68
+ update() {
69
+ if (this.#dirty.size === 0) return;
70
+ const props = Array.from(this.#dirty);
71
+ this.#dirty.clear();
72
+ for (const prop of props) {
73
+ const subs = this.#subscribers.get(prop);
74
+ if (!subs) continue;
75
+ for (const fn of subs) fn();
76
+ }
77
+ this.#events.emitAfter('flush', { props }, { target: this });
78
+ AfterFlush.schedule();
79
+ }
80
+
81
+ /**
82
+ * Internal: subscribes to a property changes on this instance.
83
+ *
84
+ * @param {string} prop
85
+ * @param {() => void} fn
86
+ * @returns {() => void} unsubscribe
87
+ */
88
+ [INTERNAL.subscribeProp](prop, fn) {
89
+ let set = this.#subscribers.get(prop);
90
+ if (!set) {
91
+ set = new Set();
92
+ this.#subscribers.set(prop, set);
93
+ }
94
+ set.add(fn);
95
+ return () => set.delete(fn);
96
+ }
97
+
98
+ /**
99
+ * Internal: instruments a property by defining a getter/setter on the instance.
100
+ * The setter marks the property dirty and schedules a flush.
101
+ *
102
+ * @param {string} prop
103
+ */
104
+ [INTERNAL.instrumentBoundProp](prop) {
105
+ if (this.#boundProps.has(prop)) return;
106
+ this.#boundProps.add(prop);
107
+
108
+ const desc = Object.getOwnPropertyDescriptor(this, prop);
109
+ if (desc && desc.configurable === false) return;
110
+
111
+ this.#values.set(prop, this[prop]);
112
+ this.#wireObservable(prop, this.#values.get(prop));
113
+
114
+ Object.defineProperty(this, prop, {
115
+ get: () => this.#values.get(prop),
116
+ set: (v) => {
117
+ const prev = this.#values.get(prop);
118
+ if (prev === v) return;
119
+ const ok = this.#events.emitBefore('set', { prop, prev, next: v }, { target: this });
120
+ if (!ok) return;
121
+ this.#values.set(prop, v);
122
+ this.#wireObservable(prop, v);
123
+ this.#markDirty(prop);
124
+ this.#events.emitAfter('set', { prop, prev, next: v }, { target: this });
125
+ },
126
+ enumerable: true,
127
+ configurable: true,
128
+ });
129
+ }
130
+
131
+ #wireObservable(prop, value) {
132
+ const prevUnsub = this.#observableUnsubs.get(prop);
133
+ if (prevUnsub) {
134
+ prevUnsub();
135
+ this.#observableUnsubs.delete(prop);
136
+ }
137
+
138
+ if (!isObservableArray(value)) return;
139
+ if (typeof value.subscribe !== 'function') return;
140
+
141
+ const unsub = value.subscribe(() => {
142
+ // Array mutated without reassigning the prop; still notify.
143
+ this.#markDirty(prop);
144
+ });
145
+ this.#observableUnsubs.set(prop, unsub);
146
+ }
147
+
148
+ #markDirty(prop) {
149
+ this.#dirty.add(prop);
150
+ this.#scheduleFlush();
151
+ }
152
+
153
+ #scheduleFlush() {
154
+ if (this.#scheduled) return;
155
+ this.#scheduled = true;
156
+ queueMicrotask(() => {
157
+ this.#scheduled = false;
158
+ this.update();
159
+ });
160
+ }
161
+ }
162
+