@fluffjs/fluff 0.0.3

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,24 @@
1
+ interface PipeInstance {
2
+ transform: (value: unknown, ...args: unknown[]) => unknown;
3
+ }
4
+ interface PipeClass {
5
+ __pipeName?: string;
6
+ new (): PipeInstance;
7
+ }
8
+ type Constructor = new (...args: unknown[]) => object;
9
+ export interface ComponentConfig {
10
+ selector: string;
11
+ templateUrl?: string;
12
+ template?: string;
13
+ styleUrl?: string;
14
+ styles?: string;
15
+ pipes?: PipeClass[];
16
+ }
17
+ export interface ComponentMetadata extends ComponentConfig {
18
+ inputs: Map<string, string>;
19
+ outputs: Map<string, string>;
20
+ }
21
+ export declare function Component(config: ComponentConfig): <T extends Constructor>(target: T) => T;
22
+ export declare function getComponentMetadata(target: Constructor): ComponentMetadata | undefined;
23
+ export declare function getAllComponents(): Map<Constructor, ComponentMetadata>;
24
+ export {};
@@ -0,0 +1,27 @@
1
+ const componentRegistry = new Map();
2
+ export function Component(config) {
3
+ return function (target) {
4
+ const metadata = {
5
+ ...config, inputs: new Map(), outputs: new Map()
6
+ };
7
+ componentRegistry.set(target, metadata);
8
+ if (config.pipes && config.pipes.length > 0) {
9
+ const pipesObj = {};
10
+ for (const PipeClassItem of config.pipes) {
11
+ const pipeName = PipeClassItem.__pipeName;
12
+ if (pipeName) {
13
+ const instance = new PipeClassItem();
14
+ pipesObj[pipeName] = (value, ...args) => instance.transform(value, ...args);
15
+ }
16
+ }
17
+ Reflect.set(target.prototype, '__pipes', pipesObj);
18
+ }
19
+ return target;
20
+ };
21
+ }
22
+ export function getComponentMetadata(target) {
23
+ return componentRegistry.get(target);
24
+ }
25
+ export function getAllComponents() {
26
+ return componentRegistry;
27
+ }
@@ -0,0 +1 @@
1
+ export declare function HostBinding(hostProperty: string): PropertyDecorator;
@@ -0,0 +1,15 @@
1
+ function isHostBindingArray(val) {
2
+ return Array.isArray(val);
3
+ }
4
+ export function HostBinding(hostProperty) {
5
+ return function (target, propertyKey) {
6
+ const ctor = target.constructor;
7
+ const key = String(propertyKey);
8
+ const existing = Reflect.get(ctor, '__hostBindings');
9
+ const bindings = isHostBindingArray(existing) ? existing : [];
10
+ if (!isHostBindingArray(existing)) {
11
+ Reflect.set(ctor, '__hostBindings', bindings);
12
+ }
13
+ bindings.push({ property: key, hostProperty });
14
+ };
15
+ }
@@ -0,0 +1 @@
1
+ export declare function HostListener(eventName: string): MethodDecorator;
@@ -0,0 +1,16 @@
1
+ function isHostListenerArray(val) {
2
+ return Array.isArray(val);
3
+ }
4
+ export function HostListener(eventName) {
5
+ return function (target, propertyKey, descriptor) {
6
+ const ctor = target.constructor;
7
+ const key = String(propertyKey);
8
+ const existing = Reflect.get(ctor, '__hostListeners');
9
+ const listeners = isHostListenerArray(existing) ? existing : [];
10
+ if (!isHostListenerArray(existing)) {
11
+ Reflect.set(ctor, '__hostListeners', listeners);
12
+ }
13
+ listeners.push({ method: key, event: eventName });
14
+ return descriptor;
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export declare function Input(bindingName?: string): PropertyDecorator;
@@ -0,0 +1,16 @@
1
+ import { getComponentMetadata } from './Component.js';
2
+ function isConstructor(val) {
3
+ return typeof val === 'function';
4
+ }
5
+ export function Input(bindingName) {
6
+ return function (target, propertyKey) {
7
+ const { constructor } = target;
8
+ if (!isConstructor(constructor))
9
+ return;
10
+ const metadata = getComponentMetadata(constructor);
11
+ if (metadata) {
12
+ const name = bindingName ?? String(propertyKey);
13
+ metadata.inputs.set(String(propertyKey), name);
14
+ }
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export declare function Output(bindingName?: string): PropertyDecorator;
@@ -0,0 +1,16 @@
1
+ import { getComponentMetadata } from './Component.js';
2
+ function isConstructor(val) {
3
+ return typeof val === 'function';
4
+ }
5
+ export function Output(bindingName) {
6
+ return function (target, propertyKey) {
7
+ const { constructor } = target;
8
+ if (!isConstructor(constructor))
9
+ return;
10
+ const metadata = getComponentMetadata(constructor);
11
+ if (metadata) {
12
+ const name = bindingName ?? String(propertyKey);
13
+ metadata.outputs.set(String(propertyKey), name);
14
+ }
15
+ };
16
+ }
@@ -0,0 +1,8 @@
1
+ type PipeConstructor = (new (...args: unknown[]) => unknown) & {
2
+ __pipeName?: string;
3
+ };
4
+ export declare function Pipe(name: string): <T extends PipeConstructor>(target: T) => T;
5
+ export interface PipeTransform<TArgs extends unknown[] = unknown[], TReturn = unknown> {
6
+ transform: (value: unknown, ...args: TArgs) => TReturn;
7
+ }
8
+ export {};
@@ -0,0 +1,6 @@
1
+ export function Pipe(name) {
2
+ return function (target) {
3
+ target.__pipeName = name;
4
+ return target;
5
+ };
6
+ }
@@ -0,0 +1 @@
1
+ export declare function Reactive(): PropertyDecorator;
@@ -0,0 +1,5 @@
1
+ export function Reactive() {
2
+ return (_target, _propertyKey) => {
3
+ // Dummy decorator, replaced by compiler
4
+ };
5
+ }
@@ -0,0 +1 @@
1
+ export declare function ViewChild(refOrSelector: string): PropertyDecorator;
@@ -0,0 +1,15 @@
1
+ function isViewChildArray(val) {
2
+ return Array.isArray(val);
3
+ }
4
+ export function ViewChild(refOrSelector) {
5
+ return function (target, propertyKey) {
6
+ const ctor = target.constructor;
7
+ const key = String(propertyKey);
8
+ const existing = Reflect.get(ctor, '__viewChildren');
9
+ const children = isViewChildArray(existing) ? existing : [];
10
+ if (!isViewChildArray(existing)) {
11
+ Reflect.set(ctor, '__viewChildren', children);
12
+ }
13
+ children.push({ property: key, selector: refOrSelector });
14
+ };
15
+ }
@@ -0,0 +1 @@
1
+ export declare function Watch(..._properties: string[]): MethodDecorator;
@@ -0,0 +1,9 @@
1
+ export function Watch(..._properties) {
2
+ return function (target, propertyKey, descriptor) {
3
+ const original = descriptor.value;
4
+ if (typeof original === 'function') {
5
+ Reflect.set(target, `__watch_${String(propertyKey)}`, original);
6
+ }
7
+ return descriptor;
8
+ };
9
+ }
@@ -0,0 +1,5 @@
1
+ export declare const enum Direction {
2
+ Any = 0,
3
+ Inbound = 1,
4
+ Outbound = 2
5
+ }
@@ -0,0 +1,6 @@
1
+ export var Direction;
2
+ (function (Direction) {
3
+ Direction[Direction["Any"] = 0] = "Any";
4
+ Direction[Direction["Inbound"] = 1] = "Inbound";
5
+ Direction[Direction["Outbound"] = 2] = "Outbound";
6
+ })(Direction || (Direction = {}));
package/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { Component } from './decorators/Component.js';
2
+ export type { ComponentConfig, ComponentMetadata } from './decorators/Component.js';
3
+ export { HostBinding } from './decorators/HostBinding.js';
4
+ export { HostListener } from './decorators/HostListener.js';
5
+ export { Input } from './decorators/Input.js';
6
+ export { Output } from './decorators/Output.js';
7
+ export { Pipe } from './decorators/Pipe.js';
8
+ export type { PipeTransform } from './decorators/Pipe.js';
9
+ export { Reactive } from './decorators/Reactive.js';
10
+ export { ViewChild } from './decorators/ViewChild.js';
11
+ export { Watch } from './decorators/Watch.js';
12
+ export { FluffElement } from './runtime/FluffElement.js';
13
+ export { Property } from './utils/Property.js';
14
+ export { Publisher } from './utils/Publisher.js';
15
+ export { Direction } from './enums/Direction.js';
16
+ export type { OnInit } from './interfaces/OnInit.js';
17
+ export type { OnDestroy } from './interfaces/OnDestroy.js';
18
+ export type { Subscription } from './interfaces/Subscription.js';
package/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { Component } from './decorators/Component.js';
2
+ export { HostBinding } from './decorators/HostBinding.js';
3
+ export { HostListener } from './decorators/HostListener.js';
4
+ export { Input } from './decorators/Input.js';
5
+ export { Output } from './decorators/Output.js';
6
+ export { Pipe } from './decorators/Pipe.js';
7
+ export { Reactive } from './decorators/Reactive.js';
8
+ export { ViewChild } from './decorators/ViewChild.js';
9
+ export { Watch } from './decorators/Watch.js';
10
+ export { FluffElement } from './runtime/FluffElement.js';
11
+ export { Property } from './utils/Property.js';
12
+ export { Publisher } from './utils/Publisher.js';
13
+ export { Direction } from './enums/Direction.js';
@@ -0,0 +1,3 @@
1
+ export interface OnDestroy {
2
+ onDestroy: () => void;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export interface OnInit {
2
+ onInit: () => void;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export interface Subscription {
2
+ unsubscribe: () => void;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@fluffjs/fluff",
3
+ "version": "0.0.3",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "module": "./index.js",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "import": "./index.js",
13
+ "default": "./index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "**/*.js",
18
+ "**/*.d.ts",
19
+ "!*.tsbuildinfo"
20
+ ],
21
+ "dependencies": {},
22
+ "publishConfig": {
23
+ "access": "public"
24
+ }
25
+ }
@@ -0,0 +1,39 @@
1
+ import type { Subscription } from '../interfaces/Subscription.js';
2
+ import type { Property } from '../utils/Property.js';
3
+ import type { Publisher } from '../utils/Publisher.js';
4
+ export declare abstract class FluffElement extends HTMLElement {
5
+ protected __pipes: Record<string, (value: unknown, ...args: unknown[]) => unknown>;
6
+ private _subscriptions;
7
+ protected readonly _shadowRoot: ShadowRoot;
8
+ private _initialized;
9
+ constructor();
10
+ connectedCallback(): void;
11
+ disconnectedCallback(): void;
12
+ protected abstract __render(): void;
13
+ protected abstract __setupBindings(): void;
14
+ protected __addSubscription(sub: Subscription): void;
15
+ protected __pipe(name: string, value: unknown, ...args: unknown[]): unknown;
16
+ $watch: (_properties: string[], callback: () => void) => Subscription;
17
+ protected __getShadowRoot(): ShadowRoot;
18
+ protected __getElement(id: string): Element | null;
19
+ private isHTMLElement;
20
+ private isPublisher;
21
+ private isProperty;
22
+ protected __setText(id: string, text: string): void;
23
+ protected __bindText(id: string, getter: () => string): void;
24
+ protected __setProperty(id: string, prop: string, value: unknown): void;
25
+ protected __addClass(id: string, className: string): void;
26
+ protected __removeClass(id: string, className: string): void;
27
+ protected __bindEvent(id: string, event: string, handler: (e: Event) => void): void;
28
+ protected __bindPropertyChange<T>(prop: Property<T>, callback: (val: T) => void): void;
29
+ protected __connectProperties<T>(source: Property<T>, target: Property<T>): void;
30
+ protected __connectOutput<T>(source: Publisher<T>, handler: (val: T) => void): void;
31
+ protected __bindOutput(id: string, outputName: string, handler: (val: Event) => void): void;
32
+ protected __setChildProperty(el: Element, propName: string, value: unknown): void;
33
+ protected __bindToChild(id: string, propName: string, value: unknown): void;
34
+ protected __setChildPropertyDeferred(el: Element, propName: string, value: unknown): void;
35
+ protected __bindOutputOnElement(el: Element, outputName: string, handler: (val: Event) => void): void;
36
+ protected __wireEvents(container: Element, attrPrefix: string): void;
37
+ private __buildRefLookups;
38
+ private __getReactiveProp;
39
+ }
@@ -0,0 +1,272 @@
1
+ export class FluffElement extends HTMLElement {
2
+ __pipes = {};
3
+ _subscriptions = [];
4
+ _shadowRoot;
5
+ _initialized = false;
6
+ constructor() {
7
+ super();
8
+ this._shadowRoot = this.attachShadow({ mode: 'open' });
9
+ }
10
+ connectedCallback() {
11
+ if (!this._initialized) {
12
+ this.__render();
13
+ this.__setupBindings();
14
+ this._initialized = true;
15
+ if ('onInit' in this && typeof this.onInit === 'function') {
16
+ this.onInit();
17
+ }
18
+ }
19
+ }
20
+ disconnectedCallback() {
21
+ if ('onDestroy' in this && typeof this.onDestroy === 'function') {
22
+ this.onDestroy();
23
+ }
24
+ for (const sub of this._subscriptions) {
25
+ sub.unsubscribe();
26
+ }
27
+ this._subscriptions = [];
28
+ }
29
+ __addSubscription(sub) {
30
+ this._subscriptions.push(sub);
31
+ }
32
+ __pipe(name, value, ...args) {
33
+ const pipe = this.__pipes[name];
34
+ if (!pipe) {
35
+ console.warn(`Pipe "${name}" not found`);
36
+ return value;
37
+ }
38
+ return pipe(value, ...args);
39
+ }
40
+ $watch = (_properties, callback) => {
41
+ callback();
42
+ return {
43
+ unsubscribe: () => {
44
+ }
45
+ };
46
+ };
47
+ __getShadowRoot() {
48
+ return this._shadowRoot;
49
+ }
50
+ __getElement(id) {
51
+ return this._shadowRoot.querySelector(`[data-lid="${id}"]`);
52
+ }
53
+ isHTMLElement(el) {
54
+ return el !== null;
55
+ }
56
+ isPublisher(value) {
57
+ return typeof value === 'object' && value !== null && 'subscribe' in value && typeof value.subscribe === 'function';
58
+ }
59
+ isProperty(value) {
60
+ return typeof value === 'object' && value !== null && 'onChange' in value && 'setValue' in value;
61
+ }
62
+ __setText(id, text) {
63
+ const el = this.__getElement(id);
64
+ if (el)
65
+ el.textContent = text;
66
+ }
67
+ __bindText(id, getter) {
68
+ const el = this.__getElement(id);
69
+ if (!el)
70
+ return;
71
+ const expr = el.getAttribute('data-text-bind') ?? '';
72
+ const propMatch = /this\.([a-zA-Z_][a-zA-Z0-9_]*)/.exec(expr);
73
+ const update = () => {
74
+ try {
75
+ el.textContent = getter();
76
+ }
77
+ catch {
78
+ el.textContent = '';
79
+ }
80
+ };
81
+ if (propMatch) {
82
+ const [, propName] = propMatch;
83
+ const reactiveProp = this.__getReactiveProp(propName);
84
+ if (reactiveProp) {
85
+ this.__bindPropertyChange(reactiveProp, update);
86
+ }
87
+ }
88
+ update();
89
+ }
90
+ __setProperty(id, prop, value) {
91
+ const el = this.__getElement(id);
92
+ if (this.isHTMLElement(el)) {
93
+ if (prop in el) {
94
+ Reflect.set(el, prop, value);
95
+ }
96
+ else {
97
+ el.setAttribute(prop, String(value));
98
+ }
99
+ }
100
+ }
101
+ __addClass(id, className) {
102
+ const el = this.__getElement(id);
103
+ if (this.isHTMLElement(el))
104
+ el.classList.add(className);
105
+ }
106
+ __removeClass(id, className) {
107
+ const el = this.__getElement(id);
108
+ if (this.isHTMLElement(el))
109
+ el.classList.remove(className);
110
+ }
111
+ __bindEvent(id, event, handler) {
112
+ const el = this.__getElement(id);
113
+ if (el) {
114
+ el.addEventListener(event, handler);
115
+ }
116
+ }
117
+ __bindPropertyChange(prop, callback) {
118
+ const sub = prop.onChange.subscribe(callback);
119
+ this._subscriptions.push(sub);
120
+ const currentVal = prop.getValue();
121
+ if (currentVal !== null) {
122
+ callback(currentVal);
123
+ }
124
+ }
125
+ __connectProperties(source, target) {
126
+ const sub = source.onChange.subscribe((val) => {
127
+ target.setValue(val, true);
128
+ });
129
+ this._subscriptions.push(sub);
130
+ const currentVal = source.getValue();
131
+ if (currentVal !== null) {
132
+ target.setValue(currentVal, true);
133
+ }
134
+ }
135
+ __connectOutput(source, handler) {
136
+ const sub = source.subscribe(handler);
137
+ this._subscriptions.push(sub);
138
+ }
139
+ __bindOutput(id, outputName, handler) {
140
+ const el = this.__getElement(id);
141
+ if (el)
142
+ this.__bindOutputOnElement(el, outputName, handler);
143
+ }
144
+ __setChildProperty(el, propName, value) {
145
+ const prop = Reflect.get(el, propName);
146
+ if (this.isProperty(prop)) {
147
+ prop.setValue(value, true);
148
+ }
149
+ else if (propName in el) {
150
+ Reflect.set(el, propName, value);
151
+ }
152
+ else {
153
+ el.setAttribute(propName, String(value));
154
+ }
155
+ }
156
+ __bindToChild(id, propName, value) {
157
+ const el = this.__getElement(id);
158
+ if (!el)
159
+ return;
160
+ this.__setChildPropertyDeferred(el, propName, value);
161
+ }
162
+ __setChildPropertyDeferred(el, propName, value) {
163
+ if (Reflect.get(el, propName) !== undefined) {
164
+ this.__setChildProperty(el, propName, value);
165
+ return;
166
+ }
167
+ const tagName = el.tagName.toLowerCase();
168
+ if (tagName.includes('-')) {
169
+ customElements.whenDefined(tagName)
170
+ .then(() => {
171
+ this.__setChildProperty(el, propName, value);
172
+ })
173
+ .catch((e) => {
174
+ console.error(e);
175
+ });
176
+ }
177
+ else {
178
+ this.__setChildProperty(el, propName, value);
179
+ }
180
+ }
181
+ __bindOutputOnElement(el, outputName, handler) {
182
+ const maybeOutput = Reflect.get(el, outputName);
183
+ if (this.isPublisher(maybeOutput)) {
184
+ this.__connectOutput(maybeOutput, handler);
185
+ return;
186
+ }
187
+ const tagName = el.tagName.toLowerCase();
188
+ if (tagName.includes('-')) {
189
+ customElements.whenDefined(tagName)
190
+ .then(() => {
191
+ const innerOutput = Reflect.get(el, outputName);
192
+ if (this.isPublisher(innerOutput)) {
193
+ this.__connectOutput(innerOutput, handler);
194
+ }
195
+ else {
196
+ el.addEventListener(outputName, handler);
197
+ }
198
+ })
199
+ .catch((e) => {
200
+ console.error(e);
201
+ });
202
+ }
203
+ else {
204
+ el.addEventListener(outputName, handler);
205
+ }
206
+ }
207
+ __wireEvents(container, attrPrefix) {
208
+ for (const el of Array.from(container.querySelectorAll(`[${attrPrefix}]`))) {
209
+ const attr = el.getAttribute(attrPrefix);
210
+ if (!attr)
211
+ continue;
212
+ const eventParts = attrPrefix.split('-event-');
213
+ const [, eventPart] = eventParts;
214
+ if (!eventPart)
215
+ continue;
216
+ const [baseEvent, ...modifiers] = eventPart.split('-');
217
+ let handler = attr;
218
+ if (attr.startsWith('[')) {
219
+ try {
220
+ const parsed = JSON.parse(attr);
221
+ if (Array.isArray(parsed) && typeof parsed[1] === 'string') {
222
+ [, handler] = parsed;
223
+ }
224
+ }
225
+ catch {
226
+ }
227
+ }
228
+ el.addEventListener(baseEvent, (ev) => {
229
+ if (modifiers.length > 0 && ev instanceof KeyboardEvent) {
230
+ const keyEv = ev;
231
+ for (const mod of modifiers) {
232
+ if (mod === 'enter' && keyEv.key !== 'Enter')
233
+ return;
234
+ if (mod === 'escape' && keyEv.key !== 'Escape')
235
+ return;
236
+ if (mod === 'space' && keyEv.key !== ' ')
237
+ return;
238
+ }
239
+ }
240
+ const refLookups = this.__buildRefLookups(handler);
241
+ // Dynamic event handlers are generated by the compiler from template attributes.
242
+ // The Function constructor is required to execute these at runtime.
243
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-type-assertion
244
+ const fn = Function('$event', refLookups + 'return this.' + handler)
245
+ .bind(this);
246
+ fn(ev);
247
+ });
248
+ }
249
+ }
250
+ __buildRefLookups(expr) {
251
+ const shadow = this._shadowRoot;
252
+ const refEls = shadow.querySelectorAll('[data-ref]');
253
+ let code = '';
254
+ for (const el of Array.from(refEls)) {
255
+ const refName = el.getAttribute('data-ref');
256
+ if (refName && new RegExp(`\\b${refName}\\b`).test(expr)) {
257
+ code += `const ${refName} = this._shadowRoot.querySelector('[data-ref="${refName}"]');`;
258
+ }
259
+ }
260
+ return code;
261
+ }
262
+ __getReactiveProp(propName) {
263
+ const key = `__${propName}`;
264
+ if (key in this) {
265
+ const candidate = Reflect.get(this, key);
266
+ if (this.isProperty(candidate)) {
267
+ return candidate;
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+ }
@@ -0,0 +1,16 @@
1
+ import { Direction } from '../enums/Direction.js';
2
+ import type { Subscription } from '../interfaces/Subscription.js';
3
+ import { Publisher } from '../utils/Publisher.js';
4
+ export declare class Property<T> {
5
+ readonly onChange: Publisher<T>;
6
+ readonly onInboundChange: Publisher<T>;
7
+ readonly onOutboundChange: Publisher<T>;
8
+ private value?;
9
+ private committed;
10
+ private _isChanging;
11
+ constructor(initialValue?: T);
12
+ setValue(val: T, inbound?: boolean, commit?: boolean): void;
13
+ triggerChange(direction?: Direction): Promise<void>;
14
+ subscribe(direction: Direction, cb: (val: T) => void): Subscription;
15
+ getValue(): T | null;
16
+ }
@@ -0,0 +1,97 @@
1
+ import { Direction } from '../enums/Direction.js';
2
+ import { Publisher } from '../utils/Publisher.js';
3
+ function safeStringify(obj) {
4
+ const seen = new WeakSet();
5
+ return JSON.stringify(obj, (_key, value) => {
6
+ if (typeof value === 'object' && value !== null) {
7
+ if (seen.has(value)) {
8
+ return '[Circular]';
9
+ }
10
+ seen.add(value);
11
+ }
12
+ return value;
13
+ });
14
+ }
15
+ export class Property {
16
+ onChange = new Publisher();
17
+ onInboundChange = new Publisher();
18
+ onOutboundChange = new Publisher();
19
+ value;
20
+ committed = true;
21
+ _isChanging = false;
22
+ constructor(initialValue) {
23
+ this.value = initialValue;
24
+ }
25
+ setValue(val, inbound = false, commit = true) {
26
+ if (this._isChanging) {
27
+ throw new Error('Binding loop detected: setValue called while change is in progress');
28
+ }
29
+ const changed = val !== this.value && safeStringify(val) !== safeStringify(this.value);
30
+ if (!changed)
31
+ return;
32
+ this._isChanging = true;
33
+ try {
34
+ this.value = val;
35
+ this.onChange.emit(val)
36
+ .catch((e) => {
37
+ console.error(e);
38
+ });
39
+ if (!commit) {
40
+ this.committed = false;
41
+ }
42
+ if (inbound) {
43
+ this.committed = true;
44
+ this.onInboundChange.emit(val)
45
+ .catch((e) => {
46
+ console.error(e);
47
+ });
48
+ }
49
+ else if (commit || !this.committed) {
50
+ this.committed = true;
51
+ if (this.value !== undefined) {
52
+ this.onOutboundChange.emit(this.value)
53
+ .catch((e) => {
54
+ console.error(e);
55
+ });
56
+ }
57
+ }
58
+ }
59
+ finally {
60
+ this._isChanging = false;
61
+ }
62
+ }
63
+ async triggerChange(direction = Direction.Any) {
64
+ if (this.value === undefined)
65
+ return;
66
+ await this.onChange.emit(this.value);
67
+ if (direction == Direction.Outbound) {
68
+ await this.onOutboundChange.emit(this.value);
69
+ }
70
+ if (direction == Direction.Inbound) {
71
+ await this.onOutboundChange.emit(this.value);
72
+ }
73
+ }
74
+ subscribe(direction, cb) {
75
+ if (direction == Direction.Inbound) {
76
+ return this.onInboundChange.subscribe((val) => {
77
+ cb(val);
78
+ });
79
+ }
80
+ else if (direction == Direction.Outbound) {
81
+ return this.onOutboundChange.subscribe((val) => {
82
+ cb(val);
83
+ });
84
+ }
85
+ else {
86
+ return this.onChange.subscribe((val) => {
87
+ cb(val);
88
+ });
89
+ }
90
+ }
91
+ getValue() {
92
+ if (this.value === undefined) {
93
+ return null;
94
+ }
95
+ return this.value;
96
+ }
97
+ }
@@ -0,0 +1,9 @@
1
+ import type { Subscription } from '../interfaces/Subscription.js';
2
+ type Callback<T> = (value: T) => void | Promise<void>;
3
+ export declare class Publisher<T> {
4
+ private readonly callbacks;
5
+ emit(value: T): Promise<void>;
6
+ subscribe(callback: Callback<T>): Subscription;
7
+ subscribeOnce(callback: Callback<T>): Subscription;
8
+ }
9
+ export {};
@@ -0,0 +1,33 @@
1
+ export class Publisher {
2
+ callbacks = new Set();
3
+ async emit(value) {
4
+ for (const callback of this.callbacks) {
5
+ try {
6
+ await callback(value);
7
+ }
8
+ catch (e) {
9
+ console.error(e);
10
+ }
11
+ }
12
+ }
13
+ subscribe(callback) {
14
+ this.callbacks.add(callback);
15
+ return {
16
+ unsubscribe: () => {
17
+ this.callbacks.delete(callback);
18
+ },
19
+ };
20
+ }
21
+ subscribeOnce(callback) {
22
+ const wrappedCallback = async (value) => {
23
+ this.callbacks.delete(wrappedCallback);
24
+ await callback(value);
25
+ };
26
+ this.callbacks.add(wrappedCallback);
27
+ return {
28
+ unsubscribe: () => {
29
+ this.callbacks.delete(wrappedCallback);
30
+ },
31
+ };
32
+ }
33
+ }