@xtia/jel 0.6.3 → 0.6.5

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/README.md CHANGED
@@ -137,4 +137,29 @@ element.events.mousemove
137
137
  .apply(([x, y]) => console.log("mouse @ ", x, y));
138
138
  ```
139
139
 
140
- For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
140
+ For RxJS users, events can be observed with `fromEvent(element.events, "mousemove")`.
141
+
142
+ ## Reactive styles
143
+
144
+ Style properties can be emitter subscriptions:
145
+
146
+ ```ts
147
+ const mousePosition$ = $(document.body).events.mousemove
148
+ .map(ev => ({x: ev.clientX, y: ev.clientY}));
149
+
150
+ const virtualCursor = $.div({
151
+ classes: "virtual-cursor",
152
+ style: {
153
+ left: mousePosition$.map(v => v.x + "px"),
154
+ top: mousePosition$.map(v => v.y + "px")
155
+ }
156
+ });
157
+ ```
158
+
159
+ Emitters for this purpose can be Jel events, [@xtia/timeline](https://github.com/tiadrop/timeline) progressions, RxJS Observables or any object with either `subscribe()` or `listen()` that returns teardown logic.
160
+
161
+ ```ts
162
+ import { animate } from "@xtia/timeline";
163
+
164
+ button.style.opacity = animate(500).tween(0, 1);
165
+ ```
package/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity } from "./internal/types";
2
- export { $ } from "./internal/element";
2
+ import { $ } from "./internal/element";
3
3
  export { createEntity } from "./internal/util";
4
4
  export { createEventSource, interval } from "./internal/emitter";
5
+ export { $ };
6
+ export declare const $body: import(".").DomEntity<HTMLElement>;
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
- export { $ } from "./internal/element";
1
+ import { $ } from "./internal/element";
2
2
  export { createEntity } from "./internal/util";
3
3
  export { createEventSource, interval } from "./internal/emitter";
4
+ export { $ };
5
+ export const $body = $(document.body);
@@ -2,7 +2,7 @@ import { attribsProxy, eventsProxy, styleProxy } from "./proxy";
2
2
  import { entityDataSymbol, isContent, isJelEntity } from "./util";
3
3
  const elementWrapCache = new WeakMap();
4
4
  const recursiveAppend = (parent, c) => {
5
- if (c === null)
5
+ if (c === null || c === undefined)
6
6
  return;
7
7
  if (Array.isArray(c)) {
8
8
  c.forEach(item => recursiveAppend(parent, item));
@@ -52,8 +52,9 @@ function createElement(tag, descriptor = {}) {
52
52
  domElement.setAttribute(k, v === true ? k : v);
53
53
  });
54
54
  }
55
- if (descriptor.content !== undefined)
56
- recursiveAppend(domElement, descriptor.content);
55
+ if ("content" in descriptor) {
56
+ ent.content = descriptor.content;
57
+ }
57
58
  if (descriptor.style) {
58
59
  ent.style(descriptor.style);
59
60
  }
@@ -127,6 +128,9 @@ function observeMutations() {
127
128
  subtree: true
128
129
  });
129
130
  }
131
+ function isReactiveSource(value) {
132
+ return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
133
+ }
130
134
  function getWrappedElement(element) {
131
135
  if (!elementWrapCache.has(element)) {
132
136
  const setCSSVariable = (k, v) => {
@@ -137,53 +141,69 @@ function getWrappedElement(element) {
137
141
  element.style.setProperty("--" + k, v);
138
142
  }
139
143
  };
140
- const styleListeners = {};
141
- function addStyleListener(prop, source) {
144
+ const listeners = {
145
+ style: {},
146
+ cssVariable: {},
147
+ content: {},
148
+ };
149
+ function addListener(type, prop, source) {
150
+ const set = {
151
+ style: (v) => element.style[prop] = v,
152
+ cssVariable: (v) => setCSSVariable(prop, v),
153
+ content: (v) => {
154
+ element.innerHTML = "";
155
+ recursiveAppend(element, v);
156
+ }
157
+ }[type];
142
158
  const subscribe = "subscribe" in source
143
- ? () => source.subscribe(v => element.style[prop] = v)
144
- : () => source.listen(v => element.style[prop] = v);
145
- styleListeners[prop] = {
159
+ ? () => source.subscribe(set)
160
+ : () => source.listen(set);
161
+ listeners[type][prop] = {
146
162
  subscribe,
147
163
  unsubscribe: element.isConnected ? subscribe() : null,
148
164
  };
149
165
  if (!elementMutationMap.has(element)) {
150
166
  elementMutationMap.set(element, {
151
167
  add: () => {
152
- Object.values(styleListeners).forEach(l => { var _a; return l.unsubscribe = (_a = l.subscribe) === null || _a === void 0 ? void 0 : _a.call(l); });
168
+ Object.values(listeners).forEach(group => {
169
+ Object.values(group).forEach(l => { var _a; return l.unsubscribe = (_a = l.subscribe) === null || _a === void 0 ? void 0 : _a.call(l); });
170
+ });
153
171
  },
154
172
  remove: () => {
155
- Object.values(styleListeners).forEach(l => {
156
- var _a;
157
- (_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
158
- l.unsubscribe = null;
173
+ Object.values(listeners).forEach(group => {
174
+ Object.values(group).forEach(l => {
175
+ var _a;
176
+ (_a = l.unsubscribe) === null || _a === void 0 ? void 0 : _a.call(l);
177
+ l.unsubscribe = null;
178
+ });
159
179
  });
160
180
  }
161
181
  });
162
182
  }
163
183
  observeMutations();
164
184
  }
165
- function removeStyleListener(prop) {
166
- if (styleListeners[prop].unsubscribe) {
167
- styleListeners[prop].unsubscribe();
185
+ function removeListener(type, prop) {
186
+ if (listeners[type][prop].unsubscribe) {
187
+ listeners[type][prop].unsubscribe();
168
188
  }
169
- delete styleListeners[prop];
170
- if (Object.keys(styleListeners).length == 0) {
189
+ delete listeners[type][prop];
190
+ if (!Object.keys(listeners).some(group => Object.keys(group).length == 0)) {
171
191
  elementMutationMap.delete(element);
172
192
  }
173
193
  }
174
194
  function setStyle(prop, value) {
175
- if (styleListeners[prop])
176
- removeStyleListener(prop);
195
+ if (listeners.style[prop])
196
+ removeListener("style", prop);
177
197
  if (typeof value == "object" && value) {
178
198
  if ("listen" in value || "subscribe" in value) {
179
- addStyleListener(prop, value);
199
+ addListener("style", prop, value);
180
200
  return;
181
201
  }
182
202
  value = value.toString();
183
203
  }
184
204
  if (value === undefined) {
185
- return prop in styleListeners
186
- ? styleListeners[prop].subscribe
205
+ return prop in listeners
206
+ ? listeners.style[prop].subscribe
187
207
  : element.style[prop];
188
208
  }
189
209
  element.style[prop] = value;
@@ -199,12 +219,27 @@ function getWrappedElement(element) {
199
219
  });
200
220
  },
201
221
  append(...content) {
222
+ var _a;
223
+ if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
224
+ removeListener("content", "");
202
225
  recursiveAppend(element, content);
203
226
  },
204
227
  remove: () => element.remove(),
205
228
  setCSSVariable(variableNameOrTable, value) {
206
229
  if (typeof variableNameOrTable == "object") {
207
- Object.entries(variableNameOrTable).forEach(([k, v]) => setCSSVariable(k, v));
230
+ Object.entries(variableNameOrTable).forEach(([k, v]) => {
231
+ if (isReactiveSource(v)) {
232
+ addListener("cssVariable", k, v);
233
+ return;
234
+ }
235
+ setCSSVariable(k, v);
236
+ });
237
+ return;
238
+ }
239
+ if (listeners.cssVariable[variableNameOrTable])
240
+ removeListener("cssVariable", variableNameOrTable);
241
+ if (isReactiveSource(value)) {
242
+ addListener("cssVariable", variableNameOrTable, value);
208
243
  return;
209
244
  }
210
245
  setCSSVariable(variableNameOrTable, value);
@@ -231,6 +266,13 @@ function getWrappedElement(element) {
231
266
  });
232
267
  },
233
268
  set content(v) {
269
+ var _a;
270
+ if ((_a = listeners.content) === null || _a === void 0 ? void 0 : _a[""])
271
+ removeListener("content", "");
272
+ if (isReactiveSource(v)) {
273
+ addListener("content", "", v);
274
+ return;
275
+ }
234
276
  element.innerHTML = "";
235
277
  recursiveAppend(element, v);
236
278
  },
@@ -1,9 +1,14 @@
1
1
  type Handler<T> = (value: T) => void;
2
2
  export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
3
3
  export type UnsubscribeFunc = () => void;
4
- export declare class Emitter<T> {
4
+ export type Listenable<T> = {
5
+ subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
6
+ } | {
7
+ listen: (callback: (value: T) => void) => UnsubscribeFunc;
8
+ };
9
+ export declare class EventEmitter<T> {
5
10
  protected onListen: ListenFunc<T>;
6
- protected constructor(onListen: ListenFunc<T>);
11
+ constructor(onListen: ListenFunc<T>);
7
12
  protected transform<R = T>(handler: (value: T, emit: (value: R) => void) => void): (fn: (v: R) => void) => UnsubscribeFunc;
8
13
  /**
9
14
  * Compatibility alias for `apply()` - registers a function to receive emitted values
@@ -22,13 +27,13 @@ export declare class Emitter<T> {
22
27
  * @param mapFunc
23
28
  * @returns Listenable: emits transformed values
24
29
  */
25
- map<R>(mapFunc: (value: T) => R): Emitter<R>;
30
+ map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
26
31
  /**
27
32
  * Creates a chainable emitter that selectively forwards emissions along the chain
28
33
  * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
29
34
  * @returns Listenable: emits values that pass the filter
30
35
  */
31
- filter(check: (value: T) => boolean): Emitter<T>;
36
+ filter(check: (value: T) => boolean): EventEmitter<T>;
32
37
  /**
33
38
  * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
34
39
  * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
@@ -36,7 +41,7 @@ export declare class Emitter<T> {
36
41
  * If no `compare` function is provided, values will be compared via `===`
37
42
  * @returns Listenable: emits non-repeating values
38
43
  */
39
- dedupe(compare?: (a: T, b: T) => boolean): Emitter<T>;
44
+ dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
40
45
  /**
41
46
  * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
42
47
  *
@@ -48,7 +53,7 @@ export declare class Emitter<T> {
48
53
  * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
49
54
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
50
55
  */
51
- tap(cb: Handler<T>): Emitter<T>;
56
+ tap(cb: Handler<T>): EventEmitter<T>;
52
57
  /**
53
58
  * Immediately passes this emitter to a callback and returns this emitter
54
59
  *
@@ -66,39 +71,41 @@ export declare class Emitter<T> {
66
71
  * ```
67
72
  * @param cb
68
73
  */
69
- fork(cb: (branch: this) => void): this;
70
- }
71
- export declare class EventEmitter<T> extends Emitter<T> {
72
- constructor(listen: ListenFunc<T>);
74
+ fork(...cb: ((branch: this) => void)[]): this;
73
75
  debounce(ms: number): EventEmitter<T>;
74
76
  throttle(ms: number): EventEmitter<T>;
75
- batch(ms: number): Emitter<T[]>;
77
+ batch(ms: number): EventEmitter<T[]>;
76
78
  /**
79
+ * Creates a chainable emitter that
77
80
  * **Experimental**: May change in future revisions
78
- * Note: potential leak - This link will remain subscribed to the parent
79
- * until it emits, regardless of subscriptions to this link.
81
+ * Note: only listens to the parent while at least one downstream subscription is present
80
82
  * @param notifier
81
83
  * @returns
82
84
  */
83
85
  once(): EventEmitter<T>;
86
+ delay(ms: number): EventEmitter<T>;
84
87
  scan<S>(updater: (state: S, value: T) => S, initial: S): EventEmitter<S>;
85
88
  buffer(count: number): EventEmitter<T[]>;
86
89
  /**
87
90
  * **Experimental**: May change in future revisions
88
- * Note: potential leak - This link will remain subscribed to the parent
89
- * until emission limit is reached, regardless of subscriptions to this link.
90
- * @param notifier
91
+ * Note: only listens to the notifier while at least one downstream subscription is present
92
+ * @param limit
91
93
  * @returns
92
94
  */
93
95
  take(limit: number): EventEmitter<T>;
94
96
  /**
95
97
  * **Experimental**: May change in future revisions
96
- * Note: potential leak - This link will remain subscribed to the notifier
97
- * until it emits, regardless of subscriptions to this link.
98
+ * Note: only listens to the notifier while at least one downstream subscription is present
98
99
  * @param notifier
99
100
  * @returns
100
101
  */
101
- takeUntil(notifier: Emitter<any>): Emitter<T>;
102
+ takeUntil(notifier: Listenable<any>): EventEmitter<T>;
103
+ /**
104
+ * Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
105
+ * Disconnects from the parent and becomes inert when the predicate returns false
106
+ * @param predicate Callback to determine whether to keep forwarding
107
+ */
108
+ takeWhile(predicate: (value: T) => boolean): EventEmitter<T>;
102
109
  /**
103
110
  * Creates a chainable emitter that immediately emits a value to every new subscriber,
104
111
  * then forwards parent emissions
@@ -106,46 +113,19 @@ export declare class EventEmitter<T> extends Emitter<T> {
106
113
  * @returns A new emitter that emits a value to new subscribers and forwards all values from the parent
107
114
  */
108
115
  immediate(value: T): EventEmitter<T>;
109
- cached(): EventEmitter<T>;
110
- /**
111
- * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
112
- * @param mapFunc
113
- * @returns Listenable: emits transformed values
114
- */
115
- map<R>(mapFunc: (value: T) => R): EventEmitter<R>;
116
- /**
117
- * Creates a chainable emitter that selectively forwards emissions along the chain
118
- * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
119
- * @returns Listenable: emits values that pass the filter
120
- */
121
- filter(check: (value: T) => boolean): EventEmitter<T>;
122
116
  /**
123
- * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
124
- * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
125
- *
126
- * If no `compare` function is provided, values will be compared via `===`
127
- * @returns Listenable: emits non-repeating values
128
- */
129
- dedupe(compare?: (a: T, b: T) => boolean): EventEmitter<T>;
130
- /**
131
- * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
132
- *
133
- * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
134
- * All listeners attached to the returned emitter receive the same values as the parent emitter.
135
- *
136
- * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
137
- *
138
- * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
139
- * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
117
+ * Creates a chainable emitter that forwards its parent's emissions, and
118
+ * immediately emits the latest value to new subscribers
119
+ * @returns
140
120
  */
141
- tap(cb: Handler<T>): EventEmitter<T>;
121
+ cached(): EventEmitter<T>;
142
122
  }
143
123
  /**
144
124
  * Creates a linked Emitter and emit() pair
145
125
  * @example
146
126
  * ```ts
147
- * function createForm(options: { onsubmit?: (data: FormData) => void }) {
148
- * const submitEvents = createEventSource(options.onsubmit);
127
+ * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
128
+ * const submitEvents = createEventSource(options?.onsubmit);
149
129
  * const form = $.form({
150
130
  * on: {
151
131
  * submit: (e) => {
@@ -182,4 +162,10 @@ export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLas
182
162
  export declare function interval(t: number | {
183
163
  asMilliseconds: number;
184
164
  }): EventEmitter<number>;
165
+ export declare function timeoutx(t: number | {
166
+ asMilliseconds: number;
167
+ }): EventEmitter<void>;
168
+ export declare function timeout(t: number | {
169
+ asMilliseconds: number;
170
+ }): EventEmitter<void>;
185
171
  export {};
@@ -1,4 +1,4 @@
1
- export class Emitter {
1
+ export class EventEmitter {
2
2
  constructor(onListen) {
3
3
  this.onListen = onListen;
4
4
  }
@@ -36,7 +36,7 @@ export class Emitter {
36
36
  */
37
37
  map(mapFunc) {
38
38
  const listen = this.transform((value, emit) => emit(mapFunc(value)));
39
- return new Emitter(listen);
39
+ return new EventEmitter(listen);
40
40
  }
41
41
  /**
42
42
  * Creates a chainable emitter that selectively forwards emissions along the chain
@@ -45,7 +45,7 @@ export class Emitter {
45
45
  */
46
46
  filter(check) {
47
47
  const listen = this.transform((value, emit) => check(value) && emit(value));
48
- return new Emitter(listen);
48
+ return new EventEmitter(listen);
49
49
  }
50
50
  /**
51
51
  * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
@@ -64,7 +64,7 @@ export class Emitter {
64
64
  previous = { value };
65
65
  }
66
66
  });
67
- return new Emitter(listen);
67
+ return new EventEmitter(listen);
68
68
  }
69
69
  /**
70
70
  * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
@@ -82,7 +82,7 @@ export class Emitter {
82
82
  cb(value);
83
83
  emit(value);
84
84
  });
85
- return new Emitter(listen);
85
+ return new EventEmitter(listen);
86
86
  }
87
87
  /**
88
88
  * Immediately passes this emitter to a callback and returns this emitter
@@ -101,15 +101,10 @@ export class Emitter {
101
101
  * ```
102
102
  * @param cb
103
103
  */
104
- fork(cb) {
105
- cb(this);
104
+ fork(...cb) {
105
+ cb.forEach(cb => cb(this));
106
106
  return this;
107
107
  }
108
- }
109
- export class EventEmitter extends Emitter {
110
- constructor(listen) {
111
- super(listen);
112
- }
113
108
  debounce(ms) {
114
109
  let reset = null;
115
110
  const listen = this.transform((value, emit) => {
@@ -153,20 +148,37 @@ export class EventEmitter extends Emitter {
153
148
  return new EventEmitter(listen);
154
149
  }
155
150
  /**
151
+ * Creates a chainable emitter that
156
152
  * **Experimental**: May change in future revisions
157
- * Note: potential leak - This link will remain subscribed to the parent
158
- * until it emits, regardless of subscriptions to this link.
153
+ * Note: only listens to the parent while at least one downstream subscription is present
159
154
  * @param notifier
160
155
  * @returns
161
156
  */
162
157
  once() {
163
- const { emit, listen } = createListenable();
164
- const unsub = this.apply(v => {
165
- unsub();
166
- emit(v);
167
- });
158
+ let parentUnsubscribe = null;
159
+ let completed = false;
160
+ const clear = () => {
161
+ if (parentUnsubscribe) {
162
+ parentUnsubscribe();
163
+ parentUnsubscribe = null;
164
+ }
165
+ };
166
+ const { emit, listen } = createListenable(() => {
167
+ if (completed)
168
+ return;
169
+ parentUnsubscribe = this.apply(v => {
170
+ completed = true;
171
+ clear();
172
+ emit(v);
173
+ });
174
+ }, clear);
168
175
  return new EventEmitter(listen);
169
176
  }
177
+ delay(ms) {
178
+ return new EventEmitter(this.transform((value, emit) => {
179
+ setTimeout(() => emit(value), ms);
180
+ }));
181
+ }
170
182
  scan(updater, initial) {
171
183
  let state = initial;
172
184
  const listen = this.transform((value, emit) => {
@@ -188,41 +200,101 @@ export class EventEmitter extends Emitter {
188
200
  }
189
201
  /**
190
202
  * **Experimental**: May change in future revisions
191
- * Note: potential leak - This link will remain subscribed to the parent
192
- * until emission limit is reached, regardless of subscriptions to this link.
193
- * @param notifier
203
+ * Note: only listens to the notifier while at least one downstream subscription is present
204
+ * @param limit
194
205
  * @returns
195
206
  */
196
207
  take(limit) {
197
- const { emit, listen } = createListenable();
208
+ let sourceUnsub = null;
198
209
  let count = 0;
199
- const unsub = this.apply(v => {
200
- if (count < limit) {
201
- emit(v);
202
- count++;
203
- if (count >= limit) {
204
- unsub();
205
- }
210
+ let completed = false;
211
+ const { emit, listen } = createListenable(() => {
212
+ if (completed)
213
+ return;
214
+ if (!sourceUnsub) {
215
+ sourceUnsub = this.apply(v => {
216
+ if (count < limit) {
217
+ emit(v);
218
+ count++;
219
+ if (count >= limit) {
220
+ completed = true;
221
+ if (sourceUnsub) {
222
+ sourceUnsub();
223
+ sourceUnsub = null;
224
+ }
225
+ }
226
+ }
227
+ });
228
+ }
229
+ }, () => {
230
+ if (sourceUnsub) {
231
+ sourceUnsub();
232
+ sourceUnsub = null;
206
233
  }
207
234
  });
208
235
  return new EventEmitter(listen);
209
236
  }
210
237
  /**
211
238
  * **Experimental**: May change in future revisions
212
- * Note: potential leak - This link will remain subscribed to the notifier
213
- * until it emits, regardless of subscriptions to this link.
239
+ * Note: only listens to the notifier while at least one downstream subscription is present
214
240
  * @param notifier
215
241
  * @returns
216
242
  */
217
243
  takeUntil(notifier) {
218
- const { emit, listen } = createListenable();
219
- const unsub = this.apply(v => {
220
- emit(v);
221
- });
222
- const unsubNotifier = notifier.apply(() => {
223
- unsub();
224
- unsubNotifier();
225
- });
244
+ let parentUnsubscribe = null;
245
+ let notifierUnsub = null;
246
+ let completed = false;
247
+ const clear = () => {
248
+ if (parentUnsubscribe) {
249
+ parentUnsubscribe();
250
+ parentUnsubscribe = null;
251
+ }
252
+ if (notifierUnsub) {
253
+ notifierUnsub();
254
+ notifierUnsub = null;
255
+ }
256
+ };
257
+ const { emit, listen } = createListenable(() => {
258
+ if (completed)
259
+ return;
260
+ parentUnsubscribe = this.apply(emit);
261
+ const handler = () => {
262
+ completed = true;
263
+ clear();
264
+ };
265
+ notifierUnsub = "subscribe" in notifier
266
+ ? notifier.subscribe(handler)
267
+ : notifier.listen(handler);
268
+ }, clear);
269
+ return new EventEmitter(listen);
270
+ }
271
+ /**
272
+ * Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
273
+ * Disconnects from the parent and becomes inert when the predicate returns false
274
+ * @param predicate Callback to determine whether to keep forwarding
275
+ */
276
+ takeWhile(predicate) {
277
+ let parentUnsubscribe = null;
278
+ let completed = false;
279
+ const clear = () => {
280
+ if (parentUnsubscribe) {
281
+ parentUnsubscribe();
282
+ parentUnsubscribe = null;
283
+ }
284
+ };
285
+ const { emit, listen } = createListenable(() => {
286
+ if (completed)
287
+ return;
288
+ parentUnsubscribe = this.apply(v => {
289
+ if (predicate(v)) {
290
+ emit(v);
291
+ }
292
+ else {
293
+ completed = true;
294
+ clear();
295
+ }
296
+ });
297
+ }, clear);
226
298
  return new EventEmitter(listen);
227
299
  }
228
300
  /**
@@ -237,6 +309,11 @@ export class EventEmitter extends Emitter {
237
309
  return this.onListen(handle);
238
310
  });
239
311
  }
312
+ /**
313
+ * Creates a chainable emitter that forwards its parent's emissions, and
314
+ * immediately emits the latest value to new subscribers
315
+ * @returns
316
+ */
240
317
  cached() {
241
318
  let cache = null;
242
319
  let unsub = null;
@@ -254,68 +331,13 @@ export class EventEmitter extends Emitter {
254
331
  return listen(handler);
255
332
  });
256
333
  }
257
- /**
258
- * Creates a chainable emitter that applies arbitrary transformation to values emitted by its parent
259
- * @param mapFunc
260
- * @returns Listenable: emits transformed values
261
- */
262
- map(mapFunc) {
263
- const listen = this.transform((value, emit) => emit(mapFunc(value)));
264
- return new EventEmitter(listen);
265
- }
266
- /**
267
- * Creates a chainable emitter that selectively forwards emissions along the chain
268
- * @param check Function that takes an emitted value and returns true if the emission should be forwarded along the chain
269
- * @returns Listenable: emits values that pass the filter
270
- */
271
- filter(check) {
272
- const listen = this.transform((value, emit) => check(value) && emit(value));
273
- return new EventEmitter(listen);
274
- }
275
- /**
276
- * Creates a chainable emitter that discards emitted values that are the same as the last value emitted by the new emitter
277
- * @param compare Optional function that takes the previous and next values and returns true if they should be considered equal
278
- *
279
- * If no `compare` function is provided, values will be compared via `===`
280
- * @returns Listenable: emits non-repeating values
281
- */
282
- dedupe(compare) {
283
- let previous = null;
284
- const listen = this.transform((value, emit) => {
285
- if (!previous || (compare
286
- ? !compare(previous.value, value)
287
- : (previous.value !== value))) {
288
- emit(value);
289
- previous = { value };
290
- }
291
- });
292
- return new EventEmitter(listen);
293
- }
294
- /**
295
- * Creates a chainable emitter that mirrors emissions from the parent emitter, invoking the provided callback `cb` as a side effect for each emission.
296
- *
297
- * The callback `cb` is called exactly once per parent emission, regardless of how many listeners are attached to the returned emitter.
298
- * All listeners attached to the returned emitter receive the same values as the parent emitter.
299
- *
300
- * *Note*, the side effect `cb` is only invoked when there is at least one listener attached to the returned emitter
301
- *
302
- * @param cb A function to be called as a side effect for each value emitted by the parent emitter.
303
- * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
304
- */
305
- tap(cb) {
306
- const listen = this.transform((value, emit) => {
307
- cb(value);
308
- emit(value);
309
- });
310
- return new EventEmitter(listen);
311
- }
312
334
  }
313
335
  /**
314
336
  * Creates a linked Emitter and emit() pair
315
337
  * @example
316
338
  * ```ts
317
- * function createForm(options: { onsubmit?: (data: FormData) => void }) {
318
- * const submitEvents = createEventSource(options.onsubmit);
339
+ * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
340
+ * const submitEvents = createEventSource(options?.onsubmit);
319
341
  * const form = $.form({
320
342
  * on: {
321
343
  * submit: (e) => {
@@ -381,3 +403,12 @@ export function interval(t) {
381
403
  }, () => clearInterval(intervalId));
382
404
  return new EventEmitter(listen);
383
405
  }
406
+ export function timeoutx(t) {
407
+ return interval(t).once().map(() => { });
408
+ }
409
+ export function timeout(t) {
410
+ const ms = typeof t === "number" ? t : t.asMilliseconds;
411
+ const { emit, listen } = createListenable();
412
+ setTimeout(emit, ms);
413
+ return new EventEmitter(listen);
414
+ }
@@ -38,7 +38,7 @@ export type ElementDescriptor<Tag extends string> = {
38
38
  [E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
39
39
  };
40
40
  style?: StylesDescriptor;
41
- cssVariables?: Record<string, CSSValue>;
41
+ cssVariables?: Record<string, CSSValue | ReactiveSource<CSSValue>>;
42
42
  } & (Tag extends TagWithValue ? {
43
43
  value?: string | number;
44
44
  } : {}) & (Tag extends ContentlessTag ? {} : {
@@ -63,8 +63,8 @@ type ElementAPI<T extends HTMLElement> = {
63
63
  };
64
64
  readonly events: EventsAccessor;
65
65
  readonly style: StyleAccessor;
66
- setCSSVariable(variableName: string, value: CSSValue): void;
67
- setCSSVariable(table: Record<string, CSSValue>): void;
66
+ setCSSVariable(variableName: string, value: CSSValue | ReactiveSource<CSSValue>): void;
67
+ setCSSVariable(table: Record<string, CSSValue | ReactiveSource<CSSValue>>): void;
68
68
  qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
69
69
  remove(): void;
70
70
  getRect(): DOMRect;
@@ -74,7 +74,7 @@ type ElementAPI<T extends HTMLElement> = {
74
74
  } & (T extends ContentlessElement ? {} : {
75
75
  append(...content: DOMContent[]): void;
76
76
  innerHTML: string;
77
- content: DOMContent;
77
+ content: DOMContent | ReactiveSource<DOMContent>;
78
78
  }) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
79
79
  value: string;
80
80
  select(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"
@@ -15,7 +15,11 @@
15
15
  },
16
16
  "./internal/*": null
17
17
  },
18
- "description": "Lightweight DOM manipulation and componentisation",
18
+ "scripts": {
19
+ "prepublishOnly": "cp ../README.md .",
20
+ "postpublish": "rm README.md"
21
+ },
22
+ "description": "Lightweight DOM manipulation, componentisation and reactivity",
19
23
  "keywords": [
20
24
  "dom"
21
25
  ],