@xtia/jel 0.6.5 → 0.7.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.
package/README.md CHANGED
@@ -42,7 +42,6 @@ body.append([
42
42
  ))
43
43
  )
44
44
  ])
45
-
46
45
  ```
47
46
 
48
47
  ## `DOMContent`
@@ -141,20 +140,33 @@ For RxJS users, events can be observed with `fromEvent(element.events, "mousemov
141
140
 
142
141
  ## Reactive styles
143
142
 
144
- Style properties can be emitter subscriptions:
143
+ Style properties, content and class presence can be emitter subscriptions:
145
144
 
146
145
  ```ts
147
146
  const mousePosition$ = $(document.body).events.mousemove
148
147
  .map(ev => ({x: ev.clientX, y: ev.clientY}));
149
148
 
150
149
  const virtualCursor = $.div({
151
- classes: "virtual-cursor",
150
+ classes: {
151
+ "virtual-cursor": true,
152
+ "near-top": mousePosition$.map(v => v.y < 100)
153
+ },
152
154
  style: {
153
155
  left: mousePosition$.map(v => v.x + "px"),
154
156
  top: mousePosition$.map(v => v.y + "px")
155
157
  }
156
158
  });
159
+
160
+ virtualCursor.classes.toggle(
161
+ "near-left",
162
+ mousePosition$.map(v => v.x < 100>)
163
+ );
164
+
165
+ h1.content = websocket$
166
+ .filter(msg => msg.type == "title")
167
+ .map(msg => msg.text);
157
168
  ```
169
+ Removing an element from the page will unsubscribe from any attached stream, and resubscribe if subsequently appended.
158
170
 
159
171
  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
172
 
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { DomEntity, ElementClassDescriptor, ElementDescriptor, DOMContent, DomHelper, StyleAccessor, JelEntity } from "./internal/types";
2
2
  import { $ } from "./internal/element";
3
3
  export { createEntity } from "./internal/util";
4
- export { createEventSource, interval } from "./internal/emitter";
4
+ export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter } from "./internal/emitter";
5
5
  export { $ };
6
6
  export declare const $body: import(".").DomEntity<HTMLElement>;
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { $ } from "./internal/element";
2
2
  export { createEntity } from "./internal/util";
3
- export { createEventSource, interval } from "./internal/emitter";
3
+ export { createEventSource, interval, timeout, SubjectEmitter, toEventEmitter } from "./internal/emitter";
4
4
  export { $ };
5
5
  export const $body = $(document.body);
@@ -1,2 +1,18 @@
1
- import { DomHelper } from "./types";
1
+ import { DomHelper, EmitterLike } from "./types";
2
2
  export declare const $: DomHelper;
3
+ export declare class ClassAccessor {
4
+ private classList;
5
+ private listen;
6
+ private unlisten;
7
+ constructor(classList: DOMTokenList, listen: (className: string, stream: EmitterLike<boolean>) => void, unlisten: (classNames: string[]) => void);
8
+ add(...className: string[]): void;
9
+ remove(...className: string[]): void;
10
+ toggle(className: string, value?: boolean): boolean;
11
+ toggle(className: string, value: EmitterLike<boolean>): void;
12
+ contains(className: string): boolean;
13
+ get length(): number;
14
+ toString(): string;
15
+ replace(token: string, newToken: string): void;
16
+ forEach(cb: (token: string, idx: number) => void): void;
17
+ map<R>(cb: (token: string, idx: number) => R): R[];
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { attribsProxy, eventsProxy, styleProxy } from "./proxy";
2
- import { entityDataSymbol, isContent, isJelEntity } from "./util";
2
+ import { entityDataSymbol, isContent, isJelEntity, isReactiveSource } from "./util";
3
3
  const elementWrapCache = new WeakMap();
4
4
  const recursiveAppend = (parent, c) => {
5
5
  if (c === null || c === undefined)
@@ -18,9 +18,9 @@ const recursiveAppend = (parent, c) => {
18
18
  };
19
19
  function createElement(tag, descriptor = {}) {
20
20
  if (isContent(descriptor))
21
- return createElement(tag, {
21
+ descriptor = {
22
22
  content: descriptor,
23
- });
23
+ };
24
24
  const domElement = document.createElement(tag);
25
25
  const ent = getWrappedElement(domElement);
26
26
  const applyClasses = (classes) => {
@@ -34,8 +34,12 @@ function createElement(tag, descriptor = {}) {
34
34
  if (classes === undefined)
35
35
  return;
36
36
  Object.entries(classes).forEach(([className, state]) => {
37
- if (state)
37
+ if (isReactiveSource(state)) {
38
+ ent.classes.toggle(className, state);
39
+ }
40
+ else if (state) {
38
41
  applyClasses(className);
42
+ }
39
43
  });
40
44
  };
41
45
  applyClasses(descriptor.classes || []);
@@ -64,6 +68,8 @@ function createElement(tag, descriptor = {}) {
64
68
  if (descriptor.on) {
65
69
  Object.entries(descriptor.on).forEach(([eventName, handler]) => ent.events[eventName].apply(handler));
66
70
  }
71
+ if (descriptor.init)
72
+ descriptor.init(ent);
67
73
  return ent;
68
74
  }
69
75
  ;
@@ -128,9 +134,6 @@ function observeMutations() {
128
134
  subtree: true
129
135
  });
130
136
  }
131
- function isReactiveSource(value) {
132
- return typeof value == "object" && value && ("listen" in value || "subscribe" in value);
133
- }
134
137
  function getWrappedElement(element) {
135
138
  if (!elementWrapCache.has(element)) {
136
139
  const setCSSVariable = (k, v) => {
@@ -145,6 +148,7 @@ function getWrappedElement(element) {
145
148
  style: {},
146
149
  cssVariable: {},
147
150
  content: {},
151
+ class: {},
148
152
  };
149
153
  function addListener(type, prop, source) {
150
154
  const set = {
@@ -153,7 +157,8 @@ function getWrappedElement(element) {
153
157
  content: (v) => {
154
158
  element.innerHTML = "";
155
159
  recursiveAppend(element, v);
156
- }
160
+ },
161
+ class: (v) => element.classList.toggle(prop, v),
157
162
  }[type];
158
163
  const subscribe = "subscribe" in source
159
164
  ? () => source.subscribe(set)
@@ -166,7 +171,7 @@ function getWrappedElement(element) {
166
171
  elementMutationMap.set(element, {
167
172
  add: () => {
168
173
  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); });
174
+ Object.values(group).forEach(l => l.unsubscribe = l.subscribe());
170
175
  });
171
176
  },
172
177
  remove: () => {
@@ -214,9 +219,11 @@ function getWrappedElement(element) {
214
219
  },
215
220
  get element() { return element; },
216
221
  on(eventId, handler) {
217
- element.addEventListener(eventId, eventData => {
222
+ const fn = (eventData) => {
218
223
  handler.call(domEntity, eventData);
219
- });
224
+ };
225
+ element.addEventListener(eventId, fn);
226
+ return () => element.removeEventListener(eventId, fn);
220
227
  },
221
228
  append(...content) {
222
229
  var _a;
@@ -334,10 +341,65 @@ function getWrappedElement(element) {
334
341
  }
335
342
  },
336
343
  style: new Proxy(setStyle, styleProxy),
337
- classes: element.classList,
344
+ classes: new ClassAccessor(element.classList, (className, stream) => addListener("class", className, stream), (classNames) => {
345
+ classNames.forEach(c => {
346
+ if (listeners.class[c])
347
+ removeListener("class", c);
348
+ });
349
+ }),
338
350
  events: new Proxy(element, eventsProxy)
339
351
  };
340
352
  elementWrapCache.set(element, domEntity);
341
353
  }
342
354
  return elementWrapCache.get(element);
343
355
  }
356
+ export class ClassAccessor {
357
+ constructor(classList, listen, unlisten) {
358
+ this.classList = classList;
359
+ this.listen = listen;
360
+ this.unlisten = unlisten;
361
+ }
362
+ add(...className) {
363
+ this.unlisten(className);
364
+ this.classList.add(...className);
365
+ }
366
+ remove(...className) {
367
+ this.unlisten(className);
368
+ this.classList.remove(...className);
369
+ }
370
+ toggle(className, value) {
371
+ this.unlisten([className]);
372
+ if (isReactiveSource(value)) {
373
+ this.listen(className, value);
374
+ return;
375
+ }
376
+ return this.classList.toggle(className, value);
377
+ }
378
+ contains(className) {
379
+ return this.classList.contains(className);
380
+ }
381
+ get length() {
382
+ return this.classList.length;
383
+ }
384
+ toString() {
385
+ return this.classList.toString();
386
+ }
387
+ replace(token, newToken) {
388
+ this.unlisten([token, newToken]);
389
+ this.classList.replace(token, newToken);
390
+ }
391
+ forEach(cb) {
392
+ this.classList.forEach(cb);
393
+ }
394
+ map(cb) {
395
+ const result = [];
396
+ const entries = this.classList.entries();
397
+ let entry = entries.next();
398
+ while (!entry.done) {
399
+ const [idx, value] = entry.value;
400
+ result.push(cb(value, idx));
401
+ entry = entries.next();
402
+ }
403
+ return result;
404
+ }
405
+ }
@@ -1,11 +1,7 @@
1
+ import { EmitterLike } from "./types";
1
2
  type Handler<T> = (value: T) => void;
2
3
  export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
3
4
  export type UnsubscribeFunc = () => void;
4
- export type Listenable<T> = {
5
- subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
6
- } | {
7
- listen: (callback: (value: T) => void) => UnsubscribeFunc;
8
- };
9
5
  export declare class EventEmitter<T> {
10
6
  protected onListen: ListenFunc<T>;
11
7
  constructor(onListen: ListenFunc<T>);
@@ -99,7 +95,7 @@ export declare class EventEmitter<T> {
99
95
  * @param notifier
100
96
  * @returns
101
97
  */
102
- takeUntil(notifier: Listenable<any>): EventEmitter<T>;
98
+ takeUntil(notifier: EmitterLike<any>): EventEmitter<T>;
103
99
  /**
104
100
  * Creates a chainable emitter that forwards its parent's emissions while the predicate returns true
105
101
  * Disconnects from the parent and becomes inert when the predicate returns false
@@ -119,9 +115,15 @@ export declare class EventEmitter<T> {
119
115
  * @returns
120
116
  */
121
117
  cached(): EventEmitter<T>;
118
+ /**
119
+ * Creates a chainable emitter that forwards emissions from the parent and any of the provided emitters
120
+ * @param emitters
121
+ */
122
+ or(...emitters: EmitterLike<T>[]): EmitterLike<T>;
123
+ or<U>(...emitters: EmitterLike<U>[]): EmitterLike<T | U>;
122
124
  }
123
125
  /**
124
- * Creates a linked Emitter and emit() pair
126
+ * Creates a linked EventEmitter and emit() pair
125
127
  * @example
126
128
  * ```ts
127
129
  * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
@@ -155,17 +157,39 @@ export declare function createEventSource<T>(initialHandler?: Handler<T>): {
155
157
  emit: (value: T) => void;
156
158
  emitter: EventEmitter<T>;
157
159
  };
158
- export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLast?: () => void): {
160
+ export declare function createListenable<T>(sourceListen?: () => UnsubscribeFunc | undefined): {
159
161
  listen: (fn: (v: T) => void) => UnsubscribeFunc;
160
162
  emit: (value: T) => void;
161
163
  };
162
- export declare function interval(t: number | {
164
+ export declare function interval(ms: number | {
163
165
  asMilliseconds: number;
164
166
  }): EventEmitter<number>;
165
- export declare function timeoutx(t: number | {
166
- asMilliseconds: number;
167
- }): EventEmitter<void>;
168
167
  export declare function timeout(t: number | {
169
168
  asMilliseconds: number;
170
169
  }): EventEmitter<void>;
170
+ export declare class SubjectEmitter<T> extends EventEmitter<T> {
171
+ private emit;
172
+ private _value;
173
+ constructor(initial: T);
174
+ get value(): T;
175
+ next(value: T): void;
176
+ }
177
+ type EventSource<T, E extends string> = {
178
+ on: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
179
+ } | {
180
+ on: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
181
+ off: (eventName: E, handler: (value: T) => void) => void;
182
+ } | {
183
+ addEventListener: (eventName: E, handler: (value: T) => void) => UnsubscribeFunc;
184
+ } | {
185
+ addEventListener: (eventName: E, handler: (value: T) => void) => void | UnsubscribeFunc;
186
+ removeEventListener: (eventName: E, handler: (value: T) => void) => void;
187
+ };
188
+ /**
189
+ * Create an EventEmitter from an event source. Event sources can be RxJS observables, existing EventEmitters, or objects that
190
+ * provide a subscribe()/listen() => UnsubscribeFunc method.
191
+ * @param source
192
+ */
193
+ export declare function toEventEmitter<T>(source: EmitterLike<T>): EventEmitter<T>;
194
+ export declare function toEventEmitter<T, E extends string>(source: EventSource<T, E>, eventName: E): EventEmitter<T>;
171
195
  export {};
@@ -1,16 +1,12 @@
1
+ import { isReactiveSource } from "./util";
1
2
  export class EventEmitter {
2
3
  constructor(onListen) {
3
4
  this.onListen = onListen;
4
5
  }
5
6
  transform(handler) {
6
- let parentUnsubscribe = null;
7
- const parentListen = this.onListen;
8
- const { emit, listen } = createListenable(() => parentUnsubscribe = parentListen(value => {
7
+ const { emit, listen } = createListenable(() => this.onListen(value => {
9
8
  handler(value, emit);
10
- }), () => {
11
- parentUnsubscribe();
12
- parentUnsubscribe = null;
13
- });
9
+ }));
14
10
  return listen;
15
11
  }
16
12
  /**
@@ -171,7 +167,8 @@ export class EventEmitter {
171
167
  clear();
172
168
  emit(v);
173
169
  });
174
- }, clear);
170
+ return clear;
171
+ });
175
172
  return new EventEmitter(listen);
176
173
  }
177
174
  delay(ms) {
@@ -226,11 +223,7 @@ export class EventEmitter {
226
223
  }
227
224
  });
228
225
  }
229
- }, () => {
230
- if (sourceUnsub) {
231
- sourceUnsub();
232
- sourceUnsub = null;
233
- }
226
+ return sourceUnsub;
234
227
  });
235
228
  return new EventEmitter(listen);
236
229
  }
@@ -245,14 +238,8 @@ export class EventEmitter {
245
238
  let notifierUnsub = null;
246
239
  let completed = false;
247
240
  const clear = () => {
248
- if (parentUnsubscribe) {
249
- parentUnsubscribe();
250
- parentUnsubscribe = null;
251
- }
252
- if (notifierUnsub) {
253
- notifierUnsub();
254
- notifierUnsub = null;
255
- }
241
+ parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
242
+ notifierUnsub === null || notifierUnsub === void 0 ? void 0 : notifierUnsub();
256
243
  };
257
244
  const { emit, listen } = createListenable(() => {
258
245
  if (completed)
@@ -262,10 +249,9 @@ export class EventEmitter {
262
249
  completed = true;
263
250
  clear();
264
251
  };
265
- notifierUnsub = "subscribe" in notifier
266
- ? notifier.subscribe(handler)
267
- : notifier.listen(handler);
268
- }, clear);
252
+ notifierUnsub = toEventEmitter(notifier).listen(handler);
253
+ return clear;
254
+ });
269
255
  return new EventEmitter(listen);
270
256
  }
271
257
  /**
@@ -274,14 +260,8 @@ export class EventEmitter {
274
260
  * @param predicate Callback to determine whether to keep forwarding
275
261
  */
276
262
  takeWhile(predicate) {
277
- let parentUnsubscribe = null;
263
+ let parentUnsubscribe;
278
264
  let completed = false;
279
- const clear = () => {
280
- if (parentUnsubscribe) {
281
- parentUnsubscribe();
282
- parentUnsubscribe = null;
283
- }
284
- };
285
265
  const { emit, listen } = createListenable(() => {
286
266
  if (completed)
287
267
  return;
@@ -291,10 +271,12 @@ export class EventEmitter {
291
271
  }
292
272
  else {
293
273
  completed = true;
294
- clear();
274
+ parentUnsubscribe();
275
+ parentUnsubscribe = undefined;
295
276
  }
296
277
  });
297
- }, clear);
278
+ return () => parentUnsubscribe === null || parentUnsubscribe === void 0 ? void 0 : parentUnsubscribe();
279
+ });
298
280
  return new EventEmitter(listen);
299
281
  }
300
282
  /**
@@ -316,24 +298,25 @@ export class EventEmitter {
316
298
  */
317
299
  cached() {
318
300
  let cache = null;
319
- let unsub = null;
320
- const { listen, emit } = createListenable(() => {
321
- unsub = this.onListen((value => {
322
- cache = { value };
323
- emit(value);
324
- }));
325
- }, () => {
326
- unsub();
327
- });
301
+ const { listen, emit } = createListenable(() => this.onListen((value => {
302
+ cache = { value };
303
+ emit(value);
304
+ })));
328
305
  return new EventEmitter(handler => {
329
306
  if (cache)
330
307
  handler(cache.value);
331
308
  return listen(handler);
332
309
  });
333
310
  }
311
+ or(...emitters) {
312
+ return new EventEmitter(handler => {
313
+ const unsubs = [this, ...emitters].map(e => toEventEmitter(e).listen(handler));
314
+ return () => unsubs.forEach(unsub => unsub());
315
+ });
316
+ }
334
317
  }
335
318
  /**
336
- * Creates a linked Emitter and emit() pair
319
+ * Creates a linked EventEmitter and emit() pair
337
320
  * @example
338
321
  * ```ts
339
322
  * function createForm(options?: { onsubmit?: (data: FormData) => void }) {
@@ -372,13 +355,14 @@ export function createEventSource(initialHandler) {
372
355
  emitter: new EventEmitter(listen)
373
356
  };
374
357
  }
375
- export function createListenable(onAddFirst, onRemoveLast) {
358
+ export function createListenable(sourceListen) {
376
359
  const handlers = [];
360
+ let onRemoveLast;
377
361
  const addListener = (fn) => {
378
362
  const unique = { fn };
379
363
  handlers.push(unique);
380
- if (onAddFirst && handlers.length == 1)
381
- onAddFirst();
364
+ if (sourceListen && handlers.length == 1)
365
+ onRemoveLast = sourceListen();
382
366
  return () => {
383
367
  const idx = handlers.indexOf(unique);
384
368
  if (idx === -1)
@@ -393,22 +377,82 @@ export function createListenable(onAddFirst, onRemoveLast) {
393
377
  emit: (value) => handlers.forEach(h => h.fn(value)),
394
378
  };
395
379
  }
396
- export function interval(t) {
380
+ export function interval(ms) {
397
381
  let intervalId = null;
398
382
  let idx = 0;
399
383
  const { emit, listen } = createListenable(() => {
400
384
  intervalId = setInterval(() => {
401
385
  emit(idx++);
402
- }, typeof t == "number" ? t : t.asMilliseconds);
403
- }, () => clearInterval(intervalId));
386
+ }, typeof ms == "number" ? ms : ms.asMilliseconds);
387
+ return () => clearInterval(intervalId);
388
+ });
404
389
  return new EventEmitter(listen);
405
390
  }
406
- export function timeoutx(t) {
407
- return interval(t).once().map(() => { });
408
- }
409
391
  export function timeout(t) {
410
392
  const ms = typeof t === "number" ? t : t.asMilliseconds;
411
- const { emit, listen } = createListenable();
412
- setTimeout(emit, ms);
393
+ const targetTime = Date.now() + ms;
394
+ let timeoutId = null;
395
+ const { emit, listen } = createListenable(() => {
396
+ const reminaingMs = targetTime - Date.now();
397
+ if (reminaingMs < 0)
398
+ return;
399
+ timeoutId = setTimeout(emit, reminaingMs);
400
+ return () => clearTimeout(timeoutId);
401
+ });
413
402
  return new EventEmitter(listen);
414
403
  }
404
+ export class SubjectEmitter extends EventEmitter {
405
+ constructor(initial) {
406
+ const { emit, listen } = createListenable();
407
+ super(h => {
408
+ h(this._value);
409
+ return listen(h);
410
+ });
411
+ this.emit = emit;
412
+ this._value = initial;
413
+ }
414
+ get value() {
415
+ return this._value;
416
+ }
417
+ next(value) {
418
+ this._value = value;
419
+ this.emit(value);
420
+ }
421
+ }
422
+ export function toEventEmitter(source, eventName) {
423
+ if (source instanceof EventEmitter)
424
+ return source;
425
+ if (eventName !== undefined) {
426
+ // addEL()
427
+ if ("addEventListener" in source) {
428
+ if ("removeEventListener" in source && typeof source.removeEventListener == "function") {
429
+ return new EventEmitter(h => {
430
+ var _a;
431
+ return (_a = source.addEventListener(eventName, h)) !== null && _a !== void 0 ? _a : (() => source.removeEventListener(eventName, h));
432
+ });
433
+ }
434
+ return new EventEmitter(h => {
435
+ return source.addEventListener(eventName, h);
436
+ });
437
+ }
438
+ // on()
439
+ if ("on" in source) {
440
+ if ("off" in source && typeof source.off == "function") {
441
+ return new EventEmitter(h => {
442
+ var _a;
443
+ return (_a = source.on(eventName, h)) !== null && _a !== void 0 ? _a : (() => source.off(eventName, h));
444
+ });
445
+ }
446
+ return new EventEmitter(h => {
447
+ return source.on(eventName, h);
448
+ });
449
+ }
450
+ }
451
+ if (isReactiveSource(source)) {
452
+ const subscribe = "subscribe" in source
453
+ ? (h) => source.subscribe(h)
454
+ : (h) => source.listen(h);
455
+ return new EventEmitter(subscribe);
456
+ }
457
+ throw new Error("Invalid event source");
458
+ }
package/internal/proxy.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from "./emitter";
1
+ import { toEventEmitter } from "./emitter";
2
2
  export const styleProxy = {
3
3
  get(style, prop) {
4
4
  return style(prop);
@@ -42,13 +42,6 @@ export const eventsProxy = {
42
42
  if (key == "removeEventListener") {
43
43
  return (name, handler) => element.removeEventListener(name, handler);
44
44
  }
45
- const listen = (handler) => {
46
- const wrappedHandler = (event) => handler(event);
47
- element.addEventListener(key, wrappedHandler);
48
- return () => {
49
- element.removeEventListener(key, wrappedHandler);
50
- };
51
- };
52
- return new EventEmitter(listen);
45
+ return toEventEmitter(element, key);
53
46
  }
54
47
  };
@@ -1,13 +1,15 @@
1
+ import { type ClassAccessor } from "./element";
1
2
  import { EventEmitter, UnsubscribeFunc } from "./emitter";
2
3
  import { entityDataSymbol } from "./util";
3
- export type ElementClassDescriptor = string | Record<string, boolean | undefined> | undefined | ElementClassDescriptor[];
4
+ export type ElementClassDescriptor = string | Record<string, boolean | EmitterLike<boolean> | undefined> | undefined | ElementClassDescriptor[];
4
5
  export type DOMContent = number | null | string | Element | JelEntity<object> | Text | DOMContent[];
5
6
  export type DomEntity<T extends HTMLElement> = JelEntity<ElementAPI<T>>;
6
- export type ReactiveSource<T> = ({
7
- listen: (handler: (value: T) => void) => UnsubscribeFunc;
7
+ export type HTMLTag = keyof HTMLElementTagNameMap;
8
+ export type EmitterLike<T> = {
9
+ subscribe: (callback: (value: T) => void) => UnsubscribeFunc;
8
10
  } | {
9
- subscribe: (handler: (value: T) => void) => UnsubscribeFunc;
10
- });
11
+ listen: (callback: (value: T) => void) => UnsubscribeFunc;
12
+ };
11
13
  export type CSSValue = string | number | null | HexCodeContainer;
12
14
  export type CSSProperty = keyof StylesDescriptor;
13
15
  type HexCodeContainer = {
@@ -18,10 +20,10 @@ export type StylesDescriptor = {
18
20
  [K in keyof CSSStyleDeclaration as [
19
21
  K,
20
22
  CSSStyleDeclaration[K]
21
- ] extends [string, string] ? K : never]+?: CSSValue | ReactiveSource<CSSValue>;
23
+ ] extends [string, string] ? K : never]+?: CSSValue | EmitterLike<CSSValue>;
22
24
  };
23
- export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | ReactiveSource<CSSValue>) => void);
24
- export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | ReactiveSource<CSSValue>);
25
+ export type SetStyleFunc = ((property: CSSProperty, value: CSSValue | EmitterLike<CSSValue>) => void);
26
+ export type SetGetStyleFunc = SetStyleFunc & ((property: CSSProperty) => string | EmitterLike<CSSValue>);
25
27
  export type StyleAccessor = ((styles: StylesDescriptor) => void) & StylesDescriptor & SetStyleFunc;
26
28
  type ContentlessTag = "area" | "br" | "hr" | "iframe" | "input" | "textarea" | "img" | "canvas" | "link" | "meta" | "source" | "embed" | "track" | "base";
27
29
  type TagWithHref = "a" | "link" | "base";
@@ -31,18 +33,19 @@ type TagWithWidthHeight = "canvas" | "img" | "embed" | "iframe" | "video";
31
33
  type TagWithType = "input" | "source" | "button";
32
34
  type TagWithName = 'input' | 'textarea' | 'select' | 'form';
33
35
  type ContentlessElement = HTMLElementTagNameMap[ContentlessTag];
34
- export type ElementDescriptor<Tag extends string> = {
36
+ export type ElementDescriptor<Tag extends HTMLTag> = {
35
37
  classes?: ElementClassDescriptor;
36
38
  attribs?: Record<string, string | number | boolean>;
37
39
  on?: {
38
40
  [E in keyof HTMLElementEventMap]+?: (event: HTMLElementEventMap[E]) => void;
39
41
  };
40
42
  style?: StylesDescriptor;
41
- cssVariables?: Record<string, CSSValue | ReactiveSource<CSSValue>>;
43
+ cssVariables?: Record<string, CSSValue | EmitterLike<CSSValue>>;
44
+ init?: (entity: DomEntity<HTMLElementTagNameMap[Tag]>) => void;
42
45
  } & (Tag extends TagWithValue ? {
43
46
  value?: string | number;
44
47
  } : {}) & (Tag extends ContentlessTag ? {} : {
45
- content?: DOMContent | ReactiveSource<DOMContent>;
48
+ content?: DOMContent | EmitterLike<DOMContent>;
46
49
  }) & (Tag extends TagWithSrc ? {
47
50
  src?: string;
48
51
  } : {}) & (Tag extends TagWithHref ? {
@@ -57,24 +60,24 @@ export type ElementDescriptor<Tag extends string> = {
57
60
  } : {});
58
61
  type ElementAPI<T extends HTMLElement> = {
59
62
  readonly element: T;
60
- readonly classes: DOMTokenList;
63
+ readonly classes: ClassAccessor;
61
64
  readonly attribs: {
62
65
  [key: string]: string | null;
63
66
  };
64
67
  readonly events: EventsAccessor;
65
68
  readonly style: StyleAccessor;
66
- setCSSVariable(variableName: string, value: CSSValue | ReactiveSource<CSSValue>): void;
67
- setCSSVariable(table: Record<string, CSSValue | ReactiveSource<CSSValue>>): void;
69
+ setCSSVariable(variableName: string, value: CSSValue | EmitterLike<CSSValue>): void;
70
+ setCSSVariable(table: Record<string, CSSValue | EmitterLike<CSSValue>>): void;
68
71
  qsa(selector: string): (Element | DomEntity<HTMLElement>)[];
69
72
  remove(): void;
70
73
  getRect(): DOMRect;
71
74
  focus(): void;
72
75
  blur(): void;
73
- on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): void;
76
+ on<E extends keyof HTMLElementEventMap>(eventId: E, handler: (this: ElementAPI<T>, data: HTMLElementEventMap[E]) => void): UnsubscribeFunc;
74
77
  } & (T extends ContentlessElement ? {} : {
75
78
  append(...content: DOMContent[]): void;
76
79
  innerHTML: string;
77
- content: DOMContent | ReactiveSource<DOMContent>;
80
+ content: DOMContent | EmitterLike<DOMContent>;
78
81
  }) & (T extends HTMLElementTagNameMap[TagWithValue] ? {
79
82
  value: string;
80
83
  select(): void;
@@ -98,26 +101,26 @@ export type DomHelper = ((
98
101
  /**
99
102
  * Creates an element of the specified tag
100
103
  */
101
- <T extends keyof HTMLElementTagNameMap>(tagName: T, descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>) & (
104
+ <T extends HTMLTag>(tagName: T, descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>) & (
102
105
  /**
103
106
  * Creates an element of the specified tag
104
107
  */
105
- <T extends keyof HTMLElementTagNameMap>(tagName: T) => DomEntity<HTMLElementTagNameMap[T]>) & (
108
+ <T extends HTMLTag>(tagName: T) => DomEntity<HTMLElementTagNameMap[T]>) & (
106
109
  /**
107
110
  * Creates an element with ID and classes as specified by a selector-like string
108
111
  */
109
- <T extends keyof HTMLElementTagNameMap>(selector: `${T}#${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
112
+ <T extends HTMLTag>(selector: `${T}#${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
110
113
  /**
111
114
  * Creates an element with ID and classes as specified by a selector-like string
112
115
  */
113
- <T extends keyof HTMLElementTagNameMap>(selector: `${T}.${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
116
+ <T extends HTMLTag>(selector: `${T}.${string}`, content?: T extends ContentlessTag ? void : DOMContent) => DomEntity<HTMLElementTagNameMap[T]>) & (
114
117
  /**
115
118
  * Wraps an existing element as a DomEntity
116
119
  */
117
120
  <T extends HTMLElement>(element: T) => DomEntity<T>) & {
118
- [T in keyof HTMLElementTagNameMap]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
121
+ [T in HTMLTag]: (descriptor: ElementDescriptor<T>) => DomEntity<HTMLElementTagNameMap[T]>;
119
122
  } & {
120
- [T in keyof HTMLElementTagNameMap]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>;
123
+ [T in HTMLTag]: T extends ContentlessTag ? () => DomEntity<HTMLElementTagNameMap[T]> : (content?: DOMContent) => DomEntity<HTMLElementTagNameMap[T]>;
121
124
  });
122
125
  type JelEntityData = {
123
126
  dom: DOMContent;
@@ -1,6 +1,6 @@
1
- import { DOMContent, ElementDescriptor, JelEntity } from "./types";
1
+ import { DOMContent, ElementDescriptor, EmitterLike, HTMLTag, JelEntity } from "./types";
2
2
  export declare const entityDataSymbol: unique symbol;
3
- export declare const isContent: (value: DOMContent | ElementDescriptor<string> | undefined) => value is DOMContent;
3
+ export declare const isContent: <T extends HTMLTag>(value: DOMContent | ElementDescriptor<T> | undefined) => value is DOMContent;
4
4
  export declare function isJelEntity(content: DOMContent): content is JelEntity<object>;
5
5
  /**
6
6
  * Wraps an object such that it can be appended as DOM content while retaining its original API
@@ -9,3 +9,4 @@ export declare function isJelEntity(content: DOMContent): content is JelEntity<o
9
9
  */
10
10
  export declare function createEntity<API extends object>(content: DOMContent, api: API extends DOMContent ? never : API): JelEntity<API>;
11
11
  export declare function createEntity(content: DOMContent): JelEntity<void>;
12
+ export declare function isReactiveSource(value: any): value is EmitterLike<any>;
package/internal/util.js CHANGED
@@ -26,3 +26,7 @@ export function createEntity(content, api) {
26
26
  });
27
27
  }
28
28
  ;
29
+ export function isReactiveSource(value) {
30
+ return typeof value == "object" && value && (("listen" in value && typeof value.listen == "function")
31
+ || ("subscribe" in value && typeof value.subscribe == "function"));
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/jel",
3
- "version": "0.6.5",
3
+ "version": "0.7.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/jel-ts",
6
6
  "type": "github"