@xtia/jel 0.6.4 → 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);
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.6.4",
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
  ],