@esportsplus/template 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @esportsplus/template might be problematic. Click here for more details.

@@ -1,4 +1,5 @@
1
1
  import { effect, root } from '@esportsplus/reactivity';
2
+ import { oncleanup } from './slot.js';
2
3
  import { className, isArray, isObject, raf, removeAttribute, setAttribute } from './utilities.js';
3
4
  import event from './event.js';
4
5
  let attributes = {}, delimiters = {
@@ -24,19 +25,24 @@ function attribute(element, name, value) {
24
25
  }
25
26
  function reactive(element, id, name, value, wait = false) {
26
27
  if (typeof value === 'function') {
27
- effect(() => {
28
+ let instance = effect(() => {
28
29
  let v = value(element);
29
30
  if (typeof v === 'function') {
30
- root(() => {
31
+ root((root) => {
32
+ instance.on('cleanup', () => root.dispose());
31
33
  reactive(element, id, name, v(element), wait);
32
34
  });
33
35
  }
36
+ else if (isArray(v) || isObject(v)) {
37
+ spread(element, v);
38
+ }
34
39
  else {
35
40
  raf.add(() => {
36
41
  update(element, id, name, v, wait);
37
42
  });
38
43
  }
39
44
  });
45
+ oncleanup(element, () => instance.dispose());
40
46
  wait = false;
41
47
  }
42
48
  else {
@@ -144,7 +150,7 @@ const spread = function (element, attributes) {
144
150
  for (let i = 0, n = attributes.length; i < n; i++) {
145
151
  let attrs = attributes[i];
146
152
  if (!isObject(attrs)) {
147
- continue;
153
+ throw new Error('@esportsplus/template: attributes must be of type `Attributes` or `Attributes[]`; Received ' + JSON.stringify(attributes));
148
154
  }
149
155
  for (let name in attrs) {
150
156
  set(element, attrs[name], name);
@@ -9,7 +9,8 @@ declare const RENDERABLE: unique symbol;
9
9
  declare const RENDERABLE_REACTIVE: unique symbol;
10
10
  declare const RENDERABLE_TEMPLATE: unique symbol;
11
11
  declare const SLOT: unique symbol;
12
+ declare const SLOT_CLEANUP: unique symbol;
12
13
  declare const SLOT_HTML = "<!--$-->";
13
14
  declare const SLOT_MARKER = "{{$}}";
14
15
  declare const SLOT_MARKER_LENGTH: number;
15
- export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
16
+ export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT, SLOT_CLEANUP, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
@@ -29,7 +29,8 @@ const RENDERABLE = Symbol();
29
29
  const RENDERABLE_REACTIVE = Symbol();
30
30
  const RENDERABLE_TEMPLATE = Symbol();
31
31
  const SLOT = Symbol();
32
+ const SLOT_CLEANUP = Symbol();
32
33
  const SLOT_HTML = '<!--$-->';
33
34
  const SLOT_MARKER = '{{$}}';
34
35
  const SLOT_MARKER_LENGTH = SLOT_MARKER.length;
35
- export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
36
+ export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT, SLOT_CLEANUP, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
package/build/event.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Element } from './types.js';
2
- declare const _default: (element: Element, event: string, listener: Function) => void;
2
+ declare const _default: (element: Element, event: `on${string}`, listener: Function) => any;
3
3
  export default _default;
package/build/event.js CHANGED
@@ -1,33 +1,15 @@
1
1
  import { root } from '@esportsplus/reactivity';
2
+ import { oncleanup } from './slot.js';
2
3
  import { addEventListener, defineProperty, parentElement } from './utilities.js';
3
- let capture = new Set(['blur', 'focus', 'scroll']), keys = {}, passive = new Set([
4
- 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousewheel',
5
- 'scroll',
6
- 'touchcancel', 'touchend', 'touchleave', 'touchmove', 'touchstart',
7
- 'wheel'
4
+ let capture = new Set(['onblur', 'onfocus', 'onscroll']), controllers = new Map(), keys = {}, passive = new Set([
5
+ 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel',
6
+ 'onscroll',
7
+ 'ontouchcancel', 'ontouchend', 'ontouchleave', 'ontouchmove', 'ontouchstart',
8
+ 'onwheel'
8
9
  ]);
9
- function register(event) {
10
- let key = keys[event] = Symbol(), type = event.slice(2);
11
- addEventListener.call(window.document, type, (e) => {
12
- let node = e.target;
13
- defineProperty(e, 'currentTarget', {
14
- configurable: true,
15
- get() {
16
- return node || window.document;
17
- }
18
- });
19
- while (node) {
20
- if (key in node) {
21
- return node[key].call(node, e);
22
- }
23
- node = parentElement.call(node);
24
- }
25
- }, {
26
- capture: capture.has(type),
27
- passive: passive.has(type)
28
- });
29
- return key;
30
- }
10
+ ['onmousemove', 'onmousewheel', 'onscroll', 'ontouchend', 'ontouchmove', 'ontouchstart', 'onwheel'].map(event => {
11
+ controllers.set(event, null);
12
+ });
31
13
  export default (element, event, listener) => {
32
14
  if (event === 'onmount') {
33
15
  let interval = setInterval(() => {
@@ -45,5 +27,48 @@ export default (element, event, listener) => {
45
27
  else if (event === 'onrender') {
46
28
  return root(() => listener(element));
47
29
  }
48
- element[keys[event] || register(event)] = listener;
30
+ let controller = controllers.get(event), signal;
31
+ if (controller === null) {
32
+ let { abort, signal } = new AbortController();
33
+ controllers.set(event, controller = {
34
+ abort,
35
+ signal,
36
+ listeners: 0,
37
+ });
38
+ }
39
+ if (controller) {
40
+ controller.listeners++;
41
+ oncleanup(element, () => {
42
+ if (--controller.listeners) {
43
+ return;
44
+ }
45
+ controller.abort();
46
+ controllers.set(event, null);
47
+ });
48
+ signal = controller.signal;
49
+ }
50
+ let key = keys[event];
51
+ if (!key) {
52
+ key = keys[event] = Symbol();
53
+ addEventListener.call(window.document, event.slice(2), (e) => {
54
+ let node = e.target;
55
+ defineProperty(e, 'currentTarget', {
56
+ configurable: true,
57
+ get() {
58
+ return node || window.document;
59
+ }
60
+ });
61
+ while (node) {
62
+ if (key in node) {
63
+ return node[key].call(node, e);
64
+ }
65
+ node = parentElement.call(node);
66
+ }
67
+ }, {
68
+ capture: capture.has(event),
69
+ passive: passive.has(event),
70
+ signal
71
+ });
72
+ }
73
+ element[key] = listener;
49
74
  };
package/build/slot.d.ts CHANGED
@@ -10,13 +10,14 @@ declare class Slot {
10
10
  set length(n: number);
11
11
  anchor(index?: number): Element;
12
12
  clear(): void;
13
- pop(): Elements | undefined;
13
+ pop(): Elements[] | undefined;
14
14
  push(...groups: Elements[]): number;
15
15
  render(input: unknown): this;
16
- shift(): Elements | undefined;
16
+ shift(): Elements[] | undefined;
17
17
  splice(start: number, stop?: number, ...groups: Elements[]): Elements[];
18
18
  unshift(...groups: Elements[]): number;
19
19
  }
20
+ declare const oncleanup: (element: Element, fn: VoidFunction) => void;
20
21
  declare const _default: (marker: Element, value: unknown) => Slot;
21
22
  export default _default;
22
- export { Slot };
23
+ export { oncleanup, Slot };
package/build/slot.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { effect, root, DIRTY } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, SLOT } from './constants.js';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, SLOT, SLOT_CLEANUP } from './constants.js';
3
3
  import { hydrate } from './html/index.js';
4
4
  import { firstChild, isArray, isObject, nextSibling, nodeValue, raf, text } from './utilities.js';
5
- let cleanup = [], key = Symbol(), scheduled = false;
6
- function afterGroups(anchor, groups) {
5
+ let cleanup = [], scheduled = false;
6
+ function after(anchor, groups) {
7
7
  for (let i = 0, n = groups.length; i < n; i++) {
8
8
  let group = groups[i];
9
9
  if (group.length) {
@@ -13,29 +13,13 @@ function afterGroups(anchor, groups) {
13
13
  }
14
14
  return groups;
15
15
  }
16
- function removeGroup(group) {
17
- if (group === undefined) {
18
- return group;
19
- }
20
- for (let i = 0, n = group.length; i < n; i++) {
21
- let item = group[i];
22
- if (key in item) {
23
- cleanup.push(item[key]);
24
- }
25
- item.remove();
26
- }
27
- if (!scheduled && cleanup.length) {
28
- schedule();
29
- }
30
- return group;
31
- }
32
- function removeGroups(groups) {
16
+ function remove(...groups) {
33
17
  for (let i = 0, n = groups.length; i < n; i++) {
34
18
  let group = groups[i];
35
19
  for (let j = 0, o = group.length; j < o; j++) {
36
20
  let item = group[j];
37
- if (key in item) {
38
- cleanup.push(item[key]);
21
+ if (item[SLOT_CLEANUP]) {
22
+ cleanup.push(...item[SLOT_CLEANUP]);
39
23
  }
40
24
  item.remove();
41
25
  }
@@ -55,12 +39,12 @@ function render(anchor, input, slot) {
55
39
  for (let i = 0, n = input.length; i < n; i++) {
56
40
  groups.push(render(null, input[i]));
57
41
  }
58
- return anchor ? afterGroups(anchor, groups) : groups;
42
+ return anchor ? after(anchor, groups) : groups;
59
43
  }
60
44
  let nodes = [];
61
45
  if (RENDERABLE in input) {
62
46
  if (input[RENDERABLE] === RENDERABLE_REACTIVE) {
63
- return afterGroups(anchor, hydrate.reactive(input, slot));
47
+ return after(anchor, hydrate.reactive(input, slot));
64
48
  }
65
49
  else {
66
50
  nodes = hydrate.static(input);
@@ -98,13 +82,16 @@ function schedule() {
98
82
  scheduled = true;
99
83
  raf.add(() => {
100
84
  try {
101
- let slot;
102
- while (slot = cleanup.pop()) {
103
- slot.clear();
85
+ let fn;
86
+ while (fn = cleanup.pop()) {
87
+ fn();
104
88
  }
105
89
  }
106
90
  catch (e) { }
107
91
  scheduled = false;
92
+ if (cleanup.length) {
93
+ schedule();
94
+ }
108
95
  });
109
96
  }
110
97
  class Slot {
@@ -113,7 +100,7 @@ class Slot {
113
100
  nodes;
114
101
  text = null;
115
102
  constructor(marker) {
116
- marker[key] = this;
103
+ oncleanup(marker, () => this.clear());
117
104
  this.marker = marker;
118
105
  this.nodes = [];
119
106
  }
@@ -131,14 +118,18 @@ class Slot {
131
118
  return node || this.marker;
132
119
  }
133
120
  clear() {
134
- removeGroups(this.nodes);
121
+ remove(...this.nodes);
135
122
  this.text = null;
136
123
  }
137
124
  pop() {
138
- return removeGroup(this.nodes.pop());
125
+ let group = this.nodes.pop();
126
+ if (!group) {
127
+ return undefined;
128
+ }
129
+ return remove(group);
139
130
  }
140
131
  push(...groups) {
141
- afterGroups(this.anchor(), groups);
132
+ after(this.anchor(), groups);
142
133
  for (let i = 0, n = groups.length; i < n; i++) {
143
134
  this.nodes.push(groups[i]);
144
135
  }
@@ -146,10 +137,11 @@ class Slot {
146
137
  }
147
138
  render(input) {
148
139
  if (typeof input === 'function') {
149
- effect((self) => {
140
+ let instance = effect((self) => {
150
141
  let v = input();
151
142
  if (typeof v === 'function') {
152
- root(() => {
143
+ root((root) => {
144
+ instance.on('cleanup', () => root.dispose());
153
145
  this.render(v());
154
146
  });
155
147
  }
@@ -162,6 +154,7 @@ class Slot {
162
154
  });
163
155
  }
164
156
  });
157
+ oncleanup(this.marker, () => instance.dispose());
165
158
  return this;
166
159
  }
167
160
  if (this.text) {
@@ -182,16 +175,23 @@ class Slot {
182
175
  return this;
183
176
  }
184
177
  shift() {
185
- return removeGroup(this.nodes.shift());
178
+ let group = this.nodes.shift();
179
+ if (!group) {
180
+ return undefined;
181
+ }
182
+ return remove(group);
186
183
  }
187
184
  splice(start, stop = this.nodes.length, ...groups) {
188
- return removeGroups(this.nodes.splice(start, stop, ...afterGroups(this.anchor(start), groups)));
185
+ return remove(...this.nodes.splice(start, stop, ...after(this.anchor(start), groups)));
189
186
  }
190
187
  unshift(...groups) {
191
- return this.nodes.unshift(...afterGroups(this.marker, groups));
188
+ return this.nodes.unshift(...after(this.marker, groups));
192
189
  }
193
190
  }
191
+ const oncleanup = (element, fn) => {
192
+ (element[SLOT_CLEANUP] ??= []).push(fn);
193
+ };
194
194
  export default (marker, value) => {
195
195
  return new Slot(marker).render(value);
196
196
  };
197
- export { Slot };
197
+ export { oncleanup, Slot };
package/build/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactiveArray } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE } from './constants.js';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_CLEANUP } from './constants.js';
3
3
  import { firstChild } from './utilities.js';
4
4
  import attributes from './attributes.js';
5
5
  import event from './event.js';
@@ -17,7 +17,9 @@ type Attributes = {
17
17
  } & Record<PropertyKey, unknown>;
18
18
  type Effect<T> = () => EffectResponse<T>;
19
19
  type EffectResponse<T> = T extends [] ? EffectResponse<T[number]>[] : Primitive | Renderable<T>;
20
- type Element = HTMLElement & Attributes & Record<PropertyKey, unknown>;
20
+ type Element = HTMLElement & Attributes & {
21
+ [SLOT_CLEANUP]?: VoidFunction[];
22
+ } & Record<PropertyKey, unknown>;
21
23
  type Elements = Element[];
22
24
  type Primitive = bigint | boolean | null | number | string | undefined;
23
25
  type Renderable<T = unknown> = RenderableReactive<T> | RenderableTemplate<T>;
package/build/types.js CHANGED
@@ -1 +1 @@
1
- import { RENDERABLE } from './constants.js';
1
+ import { RENDERABLE, SLOT_CLEANUP } from './constants.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
- "@esportsplus/reactivity": "^0.4.6",
4
+ "@esportsplus/reactivity": "^0.4.7",
5
5
  "@esportsplus/tasks": "^0.1.9",
6
6
  "@esportsplus/utilities": "^0.19.0"
7
7
  },
@@ -13,7 +13,7 @@
13
13
  "private": false,
14
14
  "type": "module",
15
15
  "types": "./build/index.d.ts",
16
- "version": "0.13.0",
16
+ "version": "0.15.0",
17
17
  "scripts": {
18
18
  "build": "tsc && tsc-alias",
19
19
  "-": "-"
package/src/attributes.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { effect, root } from '@esportsplus/reactivity';
2
+ import { oncleanup } from './slot';
2
3
  import { Attributes, Element } from './types';
3
4
  import { className, isArray, isObject, raf, removeAttribute, setAttribute } from './utilities';
4
5
  import event from './event';
@@ -33,20 +34,27 @@ function attribute(element: Element, name: string, value: unknown) {
33
34
 
34
35
  function reactive(element: Element, id: string, name: string, value: unknown, wait = false) {
35
36
  if (typeof value === 'function') {
36
- effect(() => {
37
- let v = (value as Function)(element);
37
+ let instance = effect(() => {
38
+ let v = (value as Function)(element);
39
+
40
+ if (typeof v === 'function') {
41
+ root((root) => {
42
+ instance.on('cleanup', () => root.dispose());
43
+ reactive(element, id, name, v(element), wait);
44
+ });
45
+ }
46
+ else if (isArray(v) || isObject(v)) {
47
+ spread(element, v as Attributes | Attributes[]);
48
+ }
49
+ else {
50
+ raf.add(() => {
51
+ update(element, id, name, v, wait);
52
+ });
53
+ }
54
+ });
55
+
56
+ oncleanup(element, () => instance.dispose());
38
57
 
39
- if (typeof v === 'function') {
40
- root(() => {
41
- reactive(element, id, name, v(element), wait);
42
- });
43
- }
44
- else {
45
- raf.add(() => {
46
- update(element, id, name, v, wait);
47
- });
48
- }
49
- });
50
58
  wait = false;
51
59
  }
52
60
  else {
@@ -67,7 +75,7 @@ function set(element: Element, value: unknown, name: string) {
67
75
  }
68
76
  else if (typeof value === 'function') {
69
77
  if (name.startsWith('on')) {
70
- event(element, name, value);
78
+ event(element, name as `on${string}`, value);
71
79
  }
72
80
  else {
73
81
  reactive(element, ('e' + store(element)[key]++), name, value, true);
@@ -184,7 +192,7 @@ const spread = function (element: Element, attributes: Attributes | Attributes[]
184
192
  let attrs = attributes[i];
185
193
 
186
194
  if (!isObject(attrs)) {
187
- continue;
195
+ throw new Error('@esportsplus/template: attributes must be of type `Attributes` or `Attributes[]`; Received ' + JSON.stringify(attributes));
188
196
  }
189
197
 
190
198
  for (let name in attrs) {
@@ -197,5 +205,6 @@ const spread = function (element: Element, attributes: Attributes | Attributes[]
197
205
  }
198
206
  };
199
207
 
208
+
200
209
  export default { apply, spread };
201
210
  export { apply, spread }
package/src/constants.ts CHANGED
@@ -45,6 +45,8 @@ const RENDERABLE_TEMPLATE = Symbol();
45
45
 
46
46
  const SLOT = Symbol();
47
47
 
48
+ const SLOT_CLEANUP = Symbol();
49
+
48
50
  const SLOT_HTML = '<!--$-->';
49
51
 
50
52
  const SLOT_MARKER = '{{$}}';
@@ -64,6 +66,7 @@ export {
64
66
  RENDERABLE_REACTIVE,
65
67
  RENDERABLE_TEMPLATE,
66
68
  SLOT,
69
+ SLOT_CLEANUP,
67
70
  SLOT_HTML,
68
71
  SLOT_MARKER,
69
72
  SLOT_MARKER_LENGTH
package/src/event.ts CHANGED
@@ -1,51 +1,31 @@
1
1
  import { root } from '@esportsplus/reactivity';
2
+ import { oncleanup } from './slot';
2
3
  import { Element } from './types';
3
4
  import { addEventListener, defineProperty, parentElement } from './utilities';
4
5
 
5
6
 
6
- let capture = new Set(['blur', 'focus', 'scroll']),
7
+ let capture = new Set<`on${string}`>(['onblur', 'onfocus', 'onscroll']),
8
+ controllers = new Map<
9
+ `on${string}`,
10
+ (AbortController & { listeners: number }) | null
11
+ >(),
7
12
  keys: Record<string, symbol> = {},
8
- passive = new Set([
9
- 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mousewheel',
10
- 'scroll',
11
- 'touchcancel', 'touchend', 'touchleave', 'touchmove', 'touchstart',
12
- 'wheel'
13
+ passive = new Set<`on${string}`>([
14
+ 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel',
15
+ 'onscroll',
16
+ 'ontouchcancel', 'ontouchend', 'ontouchleave', 'ontouchmove', 'ontouchstart',
17
+ 'onwheel'
13
18
  ]);
14
19
 
15
20
 
16
- function register(event: string) {
17
- let key = keys[event] = Symbol(),
18
- type = event.slice(2);
21
+ (['onmousemove', 'onmousewheel', 'onscroll', 'ontouchend', 'ontouchmove', 'ontouchstart', 'onwheel'] as `on${string}`[]).map(event => {
22
+ controllers.set(event, null);
23
+ });
19
24
 
20
- addEventListener.call(window.document, type, (e) => {
21
- let node = e.target as Element | null;
22
25
 
23
- defineProperty(e, 'currentTarget', {
24
- configurable: true,
25
- get() {
26
- return node || window.document;
27
- }
28
- });
29
-
30
- while (node) {
31
- if (key in node) {
32
- return (node[key] as Function).call(node, e);
33
- }
34
-
35
- node = parentElement.call(node);
36
- }
37
- }, {
38
- capture: capture.has(type),
39
- passive: passive.has(type)
40
- });
41
-
42
- return key;
43
- }
44
-
45
-
46
- export default (element: Element, event: string, listener: Function): void => {
26
+ export default (element: Element, event: `on${string}`, listener: Function) => {
47
27
  if (event === 'onmount') {
48
- let interval: ReturnType<typeof setInterval> = setInterval(() => {
28
+ let interval = setInterval(() => {
49
29
  retry--;
50
30
 
51
31
  if (element.isConnected) {
@@ -66,5 +46,64 @@ export default (element: Element, event: string, listener: Function): void => {
66
46
  return root(() => listener(element));
67
47
  }
68
48
 
69
- element[keys[event] || register(event)] = listener;
49
+ let controller = controllers.get(event),
50
+ signal: AbortController['signal'] | undefined;
51
+
52
+ if (controller === null) {
53
+ let { abort, signal } = new AbortController();
54
+
55
+ controllers.set(
56
+ event,
57
+ controller = {
58
+ abort,
59
+ signal,
60
+ listeners: 0,
61
+ }
62
+ );
63
+ }
64
+
65
+ if (controller) {
66
+ controller.listeners++;
67
+
68
+ oncleanup(element, () => {
69
+ if (--controller.listeners) {
70
+ return;
71
+ }
72
+
73
+ controller.abort();
74
+ controllers.set(event, null);
75
+ });
76
+ signal = controller.signal;
77
+ }
78
+
79
+ let key = keys[event];
80
+
81
+ if (!key) {
82
+ key = keys[event] = Symbol();
83
+
84
+ addEventListener.call(window.document, event.slice(2), (e) => {
85
+ let node = e.target as Element | null;
86
+
87
+ defineProperty(e, 'currentTarget', {
88
+ configurable: true,
89
+ get() {
90
+ return node || window.document;
91
+ }
92
+ });
93
+
94
+ while (node) {
95
+ if (key in node) {
96
+ return (node[key] as Function).call(node, e);
97
+ }
98
+
99
+ node = parentElement.call(node);
100
+ }
101
+ }, {
102
+ capture: capture.has(event),
103
+ passive: passive.has(event),
104
+ signal
105
+ });
106
+ }
107
+
108
+ element[key] = listener;
70
109
  };
package/src/slot.ts CHANGED
@@ -1,17 +1,15 @@
1
1
  import { effect, root, DIRTY } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, SLOT } from './constants';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, SLOT, SLOT_CLEANUP } from './constants';
3
3
  import { hydrate } from './html';
4
4
  import { Element, Elements, RenderableReactive, RenderableTemplate } from './types';
5
5
  import { firstChild, isArray, isObject, nextSibling, nodeValue, raf, text } from './utilities'
6
6
 
7
7
 
8
- let cleanup: Slot[] = [],
9
- // Using a private symbol since 'SLOT' is used as a different flag in 'render.ts'
10
- key = Symbol(),
8
+ let cleanup: VoidFunction[] = [],
11
9
  scheduled = false;
12
10
 
13
11
 
14
- function afterGroups(anchor: Element, groups: Elements[]) {
12
+ function after(anchor: Element, groups: Elements[]) {
15
13
  for (let i = 0, n = groups.length; i < n; i++) {
16
14
  let group = groups[i];
17
15
 
@@ -24,37 +22,15 @@ function afterGroups(anchor: Element, groups: Elements[]) {
24
22
  return groups;
25
23
  }
26
24
 
27
- function removeGroup(group?: Elements) {
28
- if (group === undefined) {
29
- return group;
30
- }
31
-
32
- for (let i = 0, n = group.length; i < n; i++) {
33
- let item = group[i];
34
-
35
- if (key in item) {
36
- cleanup.push(item[key] as Slot);
37
- }
38
-
39
- item.remove();
40
- }
41
-
42
- if (!scheduled && cleanup.length) {
43
- schedule();
44
- }
45
-
46
- return group;
47
- }
48
-
49
- function removeGroups(groups: Elements[]) {
25
+ function remove(...groups: Elements[]) {
50
26
  for (let i = 0, n = groups.length; i < n; i++) {
51
27
  let group = groups[i];
52
28
 
53
29
  for (let j = 0, o = group.length; j < o; j++) {
54
30
  let item = group[j];
55
31
 
56
- if (key in item) {
57
- cleanup.push(item[key] as Slot);
32
+ if (item[SLOT_CLEANUP]) {
33
+ cleanup.push(...item[SLOT_CLEANUP]);
58
34
  }
59
35
 
60
36
  item.remove();
@@ -80,14 +56,14 @@ function render(anchor: Element | null, input: unknown, slot?: Slot): Elements |
80
56
  groups.push( render(null, input[i]) as Elements );
81
57
  }
82
58
 
83
- return anchor ? afterGroups(anchor, groups) : groups;
59
+ return anchor ? after(anchor, groups) : groups;
84
60
  }
85
61
 
86
62
  let nodes: Elements = [];
87
63
 
88
64
  if (RENDERABLE in input) {
89
65
  if (input[RENDERABLE] === RENDERABLE_REACTIVE) {
90
- return afterGroups(anchor!, hydrate.reactive(input as RenderableReactive, slot!));
66
+ return after(anchor!, hydrate.reactive(input as RenderableReactive, slot!));
91
67
  }
92
68
  else {
93
69
  nodes = hydrate.static(input as RenderableTemplate<unknown>);
@@ -135,15 +111,19 @@ function schedule() {
135
111
 
136
112
  raf.add(() => {
137
113
  try {
138
- let slot;
114
+ let fn;
139
115
 
140
- while (slot = cleanup.pop()) {
141
- slot.clear();
116
+ while (fn = cleanup.pop()) {
117
+ fn();
142
118
  }
143
119
  }
144
- catch(e) {}
120
+ catch(e) { }
145
121
 
146
122
  scheduled = false;
123
+
124
+ if (cleanup.length) {
125
+ schedule();
126
+ }
147
127
  });
148
128
  }
149
129
 
@@ -157,7 +137,7 @@ class Slot {
157
137
 
158
138
 
159
139
  constructor(marker: Element) {
160
- marker[key] = this;
140
+ oncleanup(marker, () => this.clear());
161
141
 
162
142
  this.marker = marker;
163
143
  this.nodes = [];
@@ -185,16 +165,22 @@ class Slot {
185
165
  }
186
166
 
187
167
  clear() {
188
- removeGroups(this.nodes);
168
+ remove(...this.nodes);
189
169
  this.text = null;
190
170
  }
191
171
 
192
172
  pop() {
193
- return removeGroup(this.nodes.pop());
173
+ let group = this.nodes.pop();
174
+
175
+ if (!group) {
176
+ return undefined;
177
+ }
178
+
179
+ return remove(group);
194
180
  }
195
181
 
196
182
  push(...groups: Elements[]) {
197
- afterGroups(this.anchor(), groups);
183
+ after(this.anchor(), groups);
198
184
 
199
185
  for (let i = 0, n = groups.length; i < n; i++) {
200
186
  this.nodes.push(groups[i]);
@@ -205,23 +191,26 @@ class Slot {
205
191
 
206
192
  render(input: unknown) {
207
193
  if (typeof input === 'function') {
208
- effect((self) => {
209
- let v = (input as Function)();
210
-
211
- if (typeof v === 'function') {
212
- root(() => {
213
- this.render(v());
214
- });
215
- }
216
- else if (self.state === DIRTY) {
217
- this.render(v);
218
- }
219
- else {
220
- raf.add(() => {
194
+ let instance = effect((self) => {
195
+ let v = (input as Function)();
196
+
197
+ if (typeof v === 'function') {
198
+ root((root) => {
199
+ instance.on('cleanup', () => root.dispose());
200
+ this.render(v());
201
+ });
202
+ }
203
+ else if (self.state === DIRTY) {
221
204
  this.render(v);
222
- });
223
- }
224
- });
205
+ }
206
+ else {
207
+ raf.add(() => {
208
+ this.render(v);
209
+ });
210
+ }
211
+ });
212
+
213
+ oncleanup(this.marker, () => instance.dispose());
225
214
 
226
215
  return this;
227
216
  }
@@ -252,22 +241,33 @@ class Slot {
252
241
  }
253
242
 
254
243
  shift() {
255
- return removeGroup(this.nodes.shift());
244
+ let group = this.nodes.shift();
245
+
246
+ if (!group) {
247
+ return undefined;
248
+ }
249
+
250
+ return remove(group);
256
251
  }
257
252
 
258
253
  splice(start: number, stop: number = this.nodes.length, ...groups: Elements[]) {
259
- return removeGroups(
260
- this.nodes.splice(start, stop, ...afterGroups(this.anchor(start), groups))
254
+ return remove(
255
+ ...this.nodes.splice(start, stop, ...after(this.anchor(start), groups))
261
256
  );
262
257
  }
263
258
 
264
259
  unshift(...groups: Elements[]) {
265
- return this.nodes.unshift(...afterGroups(this.marker, groups));
260
+ return this.nodes.unshift(...after(this.marker, groups));
266
261
  }
267
262
  }
268
263
 
269
264
 
265
+ const oncleanup = (element: Element, fn: VoidFunction) => {
266
+ ( element[SLOT_CLEANUP] ??= [] ).push(fn);
267
+ };
268
+
269
+
270
270
  export default (marker: Element, value: unknown) => {
271
271
  return new Slot(marker).render(value);
272
272
  };
273
- export { Slot };
273
+ export { oncleanup, Slot };
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactiveArray } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE } from './constants';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_CLEANUP } from './constants';
3
3
  import { firstChild } from './utilities';
4
4
  import attributes from './attributes';
5
5
  import event from './event';
@@ -26,7 +26,7 @@ type Effect<T> = () => EffectResponse<T>;
26
26
 
27
27
  type EffectResponse<T> = T extends [] ? EffectResponse<T[number]>[] : Primitive | Renderable<T>;
28
28
 
29
- type Element = HTMLElement & Attributes & Record<PropertyKey, unknown>;
29
+ type Element = HTMLElement & Attributes & { [SLOT_CLEANUP]?: VoidFunction[] } & Record<PropertyKey, unknown>;
30
30
 
31
31
  type Elements = Element[];
32
32