@esportsplus/template 0.15.17 → 0.15.19

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.
@@ -1,11 +1,12 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
2
  import { isArray, isFunction, isObject, isString } from '@esportsplus/utilities';
3
- import { ATTRIBUTE_STORE } from './constants.js';
4
3
  import { ondisconnect } from './slot.js';
5
4
  import { className, raf, removeAttribute, setAttribute } from './utilities.js';
6
5
  import q from '@esportsplus/queue';
7
6
  import event from './event.js';
8
7
  const EFFECT_KEY = Symbol();
8
+ const HYDRATE_KEY = Symbol();
9
+ const STORE_KEY = Symbol();
9
10
  const UPDATES_KEY = Symbol();
10
11
  const STATE_HYDRATING = 0;
11
12
  const STATE_NONE = 1;
@@ -13,7 +14,7 @@ const STATE_WAITING = 2;
13
14
  let delimiters = {
14
15
  class: ' ',
15
16
  style: ';'
16
- }, hydrating = {}, queue = q(64), scheduled = false;
17
+ }, queue = q(64), scheduled = false;
17
18
  function attribute(element, name, value) {
18
19
  if (value === '' || value === false || value == null) {
19
20
  removeAttribute.call(element, name);
@@ -139,7 +140,7 @@ function update(context, id, name, value, state) {
139
140
  store[name] = value;
140
141
  }
141
142
  if (state === STATE_HYDRATING) {
142
- hydrating[name] = value;
143
+ (context.element[HYDRATE_KEY] ??= {})[name] = value;
143
144
  }
144
145
  else {
145
146
  context.updates[name] = value;
@@ -153,13 +154,17 @@ function update(context, id, name, value, state) {
153
154
  }
154
155
  }
155
156
  const apply = (element) => {
156
- for (let key in hydrating) {
157
- attribute(element, key, hydrating[key]);
157
+ let attributes = element[HYDRATE_KEY];
158
+ if (!attributes) {
159
+ return;
160
+ }
161
+ for (let key in attributes) {
162
+ attribute(element, key, attributes[key]);
158
163
  }
159
- hydrating = {};
164
+ delete element[HYDRATE_KEY];
160
165
  };
161
166
  const spread = function (element, value) {
162
- let cache = element[ATTRIBUTE_STORE] ??= { [UPDATES_KEY]: {} }, context = {
167
+ let cache = (element[STORE_KEY] ??= { [UPDATES_KEY]: {} }), context = {
163
168
  element,
164
169
  store: cache,
165
170
  updates: cache[UPDATES_KEY],
@@ -1,4 +1,3 @@
1
- declare const ATTRIBUTE_STORE: unique symbol;
2
1
  declare const NODE_CLOSING = 1;
3
2
  declare const NODE_ELEMENT = 3;
4
3
  declare const NODE_SLOT = 4;
@@ -9,8 +8,7 @@ declare const REGEX_SLOT_NODES: RegExp;
9
8
  declare const RENDERABLE: unique symbol;
10
9
  declare const RENDERABLE_REACTIVE: unique symbol;
11
10
  declare const RENDERABLE_TEMPLATE: unique symbol;
12
- declare const SLOT_CLEANUP: unique symbol;
13
11
  declare const SLOT_HTML = "<!--$-->";
14
12
  declare const SLOT_MARKER = "{{$}}";
15
13
  declare const SLOT_MARKER_LENGTH: number;
16
- export { ATTRIBUTE_STORE, NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_CLEANUP, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
14
+ export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
@@ -1,4 +1,3 @@
1
- const ATTRIBUTE_STORE = Symbol();
2
1
  const NODE_CLOSING = 1;
3
2
  const NODE_COMMENT = 2;
4
3
  const NODE_ELEMENT = 3;
@@ -29,8 +28,7 @@ const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{
29
28
  const RENDERABLE = Symbol();
30
29
  const RENDERABLE_REACTIVE = Symbol();
31
30
  const RENDERABLE_TEMPLATE = Symbol();
32
- const SLOT_CLEANUP = Symbol();
33
31
  const SLOT_HTML = '<!--$-->';
34
32
  const SLOT_MARKER = '{{$}}';
35
33
  const SLOT_MARKER_LENGTH = SLOT_MARKER.length;
36
- export { ATTRIBUTE_STORE, NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_CLEANUP, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
34
+ export { NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST, REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH };
@@ -1,10 +1,7 @@
1
- import { Elements, RenderableReactive, RenderableTemplate, RenderedGroup } from '../types.js';
1
+ import { HydrateResult, RenderableReactive, RenderableTemplate } from '../types.js';
2
2
  import { Slot } from '../slot.js';
3
3
  declare const _default: {
4
- reactive: <T>(renderable: RenderableReactive<T>, slot: Slot) => RenderedGroup[];
5
- static: <T>(renderable: RenderableTemplate<T>) => {
6
- elements: Elements;
7
- fragment: Node;
8
- };
4
+ reactive: <T>(renderable: RenderableReactive<T>, slot: Slot) => HydrateResult[];
5
+ static: <T>(renderable: RenderableTemplate<T>) => HydrateResult;
9
6
  };
10
7
  export default _default;
@@ -5,17 +5,15 @@ import cache from './cache.js';
5
5
  function reactive(renderable, slot) {
6
6
  let array = renderable.values, factory = renderable.template, refresh = () => {
7
7
  slot.render(root(() => array.map(template)));
8
- }, renderer = (i, n) => {
9
- return root(() => array.map(template, i, n));
10
8
  }, template = function (data, i) {
11
9
  let renderable = factory.call(this, data, i);
12
- return render(renderable, cache.get(renderable));
10
+ return hydrate(renderable, cache.get(renderable));
13
11
  };
14
12
  array.on('pop', () => {
15
13
  slot.pop();
16
14
  });
17
15
  array.on('push', ({ items }) => {
18
- slot.push(...renderer(array.length - items.length));
16
+ slot.push(...root(() => array.map(template, array.length - items.length)));
19
17
  });
20
18
  array.on('reverse', refresh);
21
19
  array.on('shift', () => {
@@ -23,21 +21,23 @@ function reactive(renderable, slot) {
23
21
  });
24
22
  array.on('sort', refresh);
25
23
  array.on('splice', ({ deleteCount: d, items: i, start: s }) => {
26
- slot.splice(s, d, ...renderer(s, i.length));
24
+ slot.splice(s, d, ...root(() => array.map(template, s, i.length)));
27
25
  });
28
26
  array.on('unshift', ({ items }) => {
29
- slot.unshift(...renderer(0, items.length));
27
+ slot.unshift(...root(() => array.map(template, 0, items.length)));
30
28
  });
31
29
  return array.map(template);
32
30
  }
33
- function render(renderable, template) {
31
+ function hydrate(renderable, template) {
34
32
  let elements = [], fragment = cloneNode.call(template.fragment, true), slots = template.slots;
35
33
  if (slots !== null) {
36
34
  let node, previous, values = renderable.values;
37
35
  for (let i = slots.length - 1; i >= 0; i--) {
38
36
  let { fn, path, slot } = slots[i];
39
37
  if (path !== previous) {
40
- apply(node);
38
+ if (node) {
39
+ apply(node);
40
+ }
41
41
  node = fragment;
42
42
  previous = path;
43
43
  for (let o = 0, j = path.length; o < j; o++) {
@@ -58,6 +58,6 @@ export default {
58
58
  return reactive(renderable, slot);
59
59
  },
60
60
  static: (renderable) => {
61
- return render(renderable, renderable.template || (renderable.template = cache.get(renderable)));
61
+ return hydrate(renderable, renderable.template || (renderable.template = cache.get(renderable)));
62
62
  }
63
63
  };
package/build/slot.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Element, Elements, RenderedGroup } from './types.js';
1
+ import { Element, Elements, HydrateResult } from './types.js';
2
2
  declare class Slot {
3
3
  marker: Element;
4
4
  nodes: Elements[];
@@ -9,11 +9,11 @@ declare class Slot {
9
9
  anchor(index?: number): Element;
10
10
  clear(): void;
11
11
  pop(): Elements[] | undefined;
12
- push(...groups: RenderedGroup[]): number;
12
+ push(...groups: HydrateResult[]): number;
13
13
  render(input: unknown): this;
14
14
  shift(): Elements[] | undefined;
15
- splice(start: number, stop?: number, ...groups: RenderedGroup[]): Elements[];
16
- unshift(...groups: RenderedGroup[]): number;
15
+ splice(start: number, stop?: number, ...groups: HydrateResult[]): Elements[];
16
+ unshift(...groups: HydrateResult[]): number;
17
17
  }
18
18
  declare const ondisconnect: (element: Element, fn: VoidFunction) => void;
19
19
  declare const _default: (marker: Element, value: unknown) => Slot;
package/build/slot.js CHANGED
@@ -1,30 +1,28 @@
1
1
  import { effect, root } from '@esportsplus/reactivity';
2
2
  import { isArray, isFunction, isInstanceOf, isObject } from '@esportsplus/utilities';
3
- import { RENDERABLE, RENDERABLE_REACTIVE, SLOT_CLEANUP } from './constants.js';
3
+ import { RENDERABLE, RENDERABLE_REACTIVE } from './constants.js';
4
4
  import { hydrate } from './html/index.js';
5
- import { append, firstChild, fragment, microtask, nextSibling, nodeValue, raf, text } from './utilities.js';
5
+ import { append, cloneNode, firstChild, fragment, microtask, nextSibling, nodeValue, raf, text } from './utilities.js';
6
6
  import queue from '@esportsplus/queue';
7
- let cleanup = queue(64), fallback = fragment(''), scheduled = false;
7
+ const CLEANUP_KEY = Symbol();
8
+ let cleanup = queue(64), scheduled = false, template = fragment('');
8
9
  function after(anchor, groups) {
9
- let elements = [], n = groups.length;
10
- if (n) {
11
- let fragment = groups[0].fragment || fallback;
12
- if (n === 1) {
13
- elements.push(groups[0].elements);
14
- }
15
- else {
16
- for (let i = 1; i < n; i++) {
17
- let group = groups[i];
18
- if (group.fragment) {
19
- append.call(fragment, group.fragment);
20
- group.fragment = null;
21
- }
22
- elements.push(group.elements);
23
- }
10
+ let n = groups.length;
11
+ if (n === 0) {
12
+ return [];
13
+ }
14
+ let elements = [], fragment = groups[0].fragment || cloneNode.call(template);
15
+ if (n === 1) {
16
+ elements.push(groups[0].elements);
17
+ }
18
+ else {
19
+ for (let i = 1; i < n; i++) {
20
+ let { elements: e, fragment: f } = groups[i];
21
+ append.call(fragment, f);
22
+ elements.push(e);
24
23
  }
25
- anchor.after(fragment);
26
- groups[0].fragment = null;
27
24
  }
25
+ anchor.after(fragment);
28
26
  return elements;
29
27
  }
30
28
  function remove(...groups) {
@@ -32,8 +30,8 @@ function remove(...groups) {
32
30
  let group = groups[i];
33
31
  for (let j = 0, o = group.length; j < o; j++) {
34
32
  let item = group[j];
35
- if (item[SLOT_CLEANUP]) {
36
- cleanup.add(item[SLOT_CLEANUP]);
33
+ if (CLEANUP_KEY in item) {
34
+ cleanup.add(item[CLEANUP_KEY]);
37
35
  }
38
36
  item.remove();
39
37
  }
@@ -43,47 +41,50 @@ function remove(...groups) {
43
41
  }
44
42
  return groups;
45
43
  }
46
- function render(groups, input, slot) {
44
+ function render(elements, fragment, input, slot) {
47
45
  if (input === false || input == null || input === '') {
48
- return groups;
46
+ return;
49
47
  }
50
48
  if (isArray(input)) {
51
49
  for (let i = 0, n = input.length; i < n; i++) {
52
- render(groups, input[i]);
50
+ render(elements, fragment, input[i]);
53
51
  }
54
52
  }
55
53
  else if (isObject(input) && RENDERABLE in input) {
56
54
  if (input[RENDERABLE] === RENDERABLE_REACTIVE) {
57
- groups.push(...hydrate.reactive(input, slot));
55
+ let response = hydrate.reactive(input, slot);
56
+ for (let i = 0, n = response.length; i < n; i++) {
57
+ let { elements: e, fragment: f } = response[i];
58
+ append.call(fragment, f);
59
+ elements.push(e);
60
+ }
58
61
  }
59
62
  else {
60
- groups.push(hydrate.static(input));
63
+ let { elements: e, fragment: f } = hydrate.static(input);
64
+ append.call(fragment, f);
65
+ elements.push(e);
61
66
  }
62
67
  }
63
68
  else if (isInstanceOf(input, NodeList)) {
64
- let elements = [];
69
+ let e = [];
65
70
  for (let node = firstChild.call(input); node; node = nextSibling.call(node)) {
66
- elements.push(node);
71
+ e.push(node);
67
72
  }
68
- groups.push({ elements, fragment: null });
73
+ append.call(fragment, ...e);
74
+ elements.push(e);
69
75
  }
70
76
  else if (isInstanceOf(input, Node)) {
71
- groups.push({
72
- elements: [input],
73
- fragment: input
74
- });
77
+ append.call(fragment, input);
78
+ elements.push([input]);
75
79
  }
76
80
  else {
77
81
  let element = text(typeof input === 'string' ? input : String(input));
78
82
  if (slot) {
79
83
  slot.text = element;
80
84
  }
81
- groups.push({
82
- elements: [element],
83
- fragment: element
84
- });
85
+ append.call(fragment, element);
86
+ elements.push([element]);
85
87
  }
86
- return groups;
87
88
  }
88
89
  function schedule() {
89
90
  if (scheduled) {
@@ -125,7 +126,7 @@ class Slot {
125
126
  anchor(index = this.nodes.length - 1) {
126
127
  let node, nodes = this.nodes[index];
127
128
  if (nodes) {
128
- node = nodes.at(-1);
129
+ node = nodes[nodes.length - 1];
129
130
  }
130
131
  return node || this.marker;
131
132
  }
@@ -169,7 +170,10 @@ class Slot {
169
170
  }
170
171
  }
171
172
  this.clear();
172
- this.nodes = after(this.marker, render([], input, this));
173
+ let fragment = cloneNode.call(template), nodes = [];
174
+ render(nodes, fragment, input, this);
175
+ this.marker.after(fragment);
176
+ this.nodes = nodes;
173
177
  return this;
174
178
  }
175
179
  shift() {
@@ -187,7 +191,7 @@ class Slot {
187
191
  }
188
192
  }
189
193
  const ondisconnect = (element, fn) => {
190
- (element[SLOT_CLEANUP] ??= []).push(fn);
194
+ (element[CLEANUP_KEY] ??= []).push(fn);
191
195
  };
192
196
  export default (marker, value) => {
193
197
  return new Slot(marker).render(value);
package/build/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactiveArray } from '@esportsplus/reactivity';
2
- import { ATTRIBUTE_STORE, RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE, SLOT_CLEANUP } from './constants.js';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE } from './constants.js';
3
3
  import { firstChild } from './utilities.js';
4
4
  import attributes from './attributes.js';
5
5
  import slot from './slot.js';
@@ -18,11 +18,12 @@ type Attributes = {
18
18
  } & Record<PropertyKey, unknown>;
19
19
  type Effect<T> = () => EffectResponse<T>;
20
20
  type EffectResponse<T> = T extends [] ? EffectResponse<T[number]>[] : Primitive | Renderable<T>;
21
- type Element = HTMLElement & Attributes & {
22
- [ATTRIBUTE_STORE]?: Record<PropertyKey, unknown>;
23
- [SLOT_CLEANUP]?: VoidFunction[];
24
- } & Record<PropertyKey, unknown>;
21
+ type Element = HTMLElement & Attributes & Record<PropertyKey, unknown>;
25
22
  type Elements = Element[];
23
+ type HydrateResult = {
24
+ elements: Elements;
25
+ fragment: DocumentFragment | Node;
26
+ };
26
27
  type Primitive = bigint | boolean | null | number | string | undefined;
27
28
  type Renderable<T = unknown> = RenderableReactive<T> | RenderableTemplate<T>;
28
29
  type RenderableReactive<T = unknown> = Readonly<{
@@ -38,10 +39,6 @@ type RenderableTemplate<T> = {
38
39
  values: (RenderableValue<T> | RenderableValue<T>[])[];
39
40
  };
40
41
  type RenderableValue<T = unknown> = Attributes | Readonly<Attributes> | Readonly<Attributes[]> | Effect<T> | Primitive | Renderable;
41
- type RenderedGroup = {
42
- elements: Elements;
43
- fragment: DocumentFragment | Node | null;
44
- };
45
42
  type Template = {
46
43
  fragment: DocumentFragment;
47
44
  html: string;
@@ -52,4 +49,4 @@ type Template = {
52
49
  slot: number;
53
50
  }[] | null;
54
51
  };
55
- export type { Attributes, Effect, Element, Elements, Renderable, RenderableReactive, RenderableTemplate, RenderableValue, RenderedGroup, Template };
52
+ export type { Attributes, Effect, Element, Elements, HydrateResult, Renderable, RenderableReactive, RenderableTemplate, RenderableValue, Template };
package/build/types.js CHANGED
@@ -1 +1 @@
1
- import { ATTRIBUTE_STORE, RENDERABLE, SLOT_CLEANUP } from './constants.js';
1
+ import { RENDERABLE } from './constants.js';
@@ -1,9 +1,9 @@
1
1
  import { Element as E } from './types.js';
2
+ declare const append: (...nodes: (Node | string)[]) => void;
2
3
  declare const addEventListener: {
3
4
  <K extends keyof ElementEventMap>(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
4
5
  (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
5
6
  };
6
- declare const append: (...nodes: (Node | string)[]) => void;
7
7
  declare const removeEventListener: {
8
8
  <K extends keyof ElementEventMap>(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
9
9
  (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
@@ -1,8 +1,9 @@
1
1
  import { micro as m, raf as r } from '@esportsplus/tasks';
2
2
  let prototype, template = document.createElement('template'), t = document.createTextNode('');
3
+ prototype = DocumentFragment.prototype;
4
+ const append = prototype.append;
3
5
  prototype = Element.prototype;
4
6
  const addEventListener = prototype.addEventListener;
5
- const append = prototype.append;
6
7
  const removeEventListener = prototype.removeEventListener;
7
8
  const className = Object.getOwnPropertyDescriptor(prototype, 'className').set;
8
9
  const innerHTML = Object.getOwnPropertyDescriptor(prototype, 'innerHTML').set;
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "private": false,
15
15
  "type": "module",
16
16
  "types": "./build/index.d.ts",
17
- "version": "0.15.17",
17
+ "version": "0.15.19",
18
18
  "scripts": {
19
19
  "build": "tsc && tsc-alias",
20
20
  "-": "-"
package/src/attributes.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
2
  import { isArray, isFunction, isObject, isString } from '@esportsplus/utilities';
3
- import { ATTRIBUTE_STORE } from '~/constants';
4
3
  import { ondisconnect } from './slot';
5
4
  import { Attributes, Element } from './types';
6
5
  import { className, raf, removeAttribute, setAttribute } from './utilities';
@@ -10,8 +9,13 @@ import event from './event';
10
9
 
11
10
  const EFFECT_KEY = Symbol();
12
11
 
12
+ const HYDRATE_KEY = Symbol();
13
+
14
+ const STORE_KEY = Symbol();
15
+
13
16
  const UPDATES_KEY = Symbol();
14
17
 
18
+
15
19
  const STATE_HYDRATING = 0;
16
20
 
17
21
  const STATE_NONE = 1;
@@ -33,7 +37,6 @@ let delimiters: Record<string, string> = {
33
37
  class: ' ',
34
38
  style: ';'
35
39
  },
36
- hydrating: Record<string, unknown> = {},
37
40
  queue = q<Context>(64),
38
41
  scheduled = false;
39
42
 
@@ -213,7 +216,7 @@ function update(
213
216
  }
214
217
 
215
218
  if (state === STATE_HYDRATING) {
216
- hydrating[name] = value;
219
+ ((context.element[HYDRATE_KEY] ??= {}) as Record<PropertyKey, unknown>)[name] = value;
217
220
  }
218
221
  else {
219
222
  context.updates[name] = value;
@@ -231,15 +234,21 @@ function update(
231
234
 
232
235
 
233
236
  const apply = (element: Element) => {
234
- for (let key in hydrating) {
235
- attribute(element, key, hydrating[key]);
237
+ let attributes = element[HYDRATE_KEY] as Record<PropertyKey, unknown> | undefined;
238
+
239
+ if (!attributes) {
240
+ return;
241
+ }
242
+
243
+ for (let key in attributes) {
244
+ attribute(element, key, attributes[key]);
236
245
  }
237
246
 
238
- hydrating = {};
247
+ delete element[HYDRATE_KEY];
239
248
  };
240
249
 
241
250
  const spread = function (element: Element, value: Attributes | Attributes[]) {
242
- let cache = element[ATTRIBUTE_STORE] ??= { [UPDATES_KEY]: {} },
251
+ let cache = (element[STORE_KEY] ??= { [UPDATES_KEY]: {} }) as Record<PropertyKey, unknown>,
243
252
  context = {
244
253
  element,
245
254
  store: cache,
package/src/constants.ts CHANGED
@@ -1,6 +1,3 @@
1
- const ATTRIBUTE_STORE = Symbol();
2
-
3
-
4
1
  const NODE_CLOSING = 1;
5
2
 
6
3
  const NODE_COMMENT = 2;
@@ -46,8 +43,6 @@ const RENDERABLE_REACTIVE = Symbol();
46
43
  const RENDERABLE_TEMPLATE = Symbol();
47
44
 
48
45
 
49
- const SLOT_CLEANUP = Symbol();
50
-
51
46
  const SLOT_HTML = '<!--$-->';
52
47
 
53
48
  const SLOT_MARKER = '{{$}}';
@@ -56,9 +51,8 @@ const SLOT_MARKER_LENGTH = SLOT_MARKER.length;
56
51
 
57
52
 
58
53
  export {
59
- ATTRIBUTE_STORE,
60
54
  NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST,
61
55
  REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES,
62
56
  RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE,
63
- SLOT_CLEANUP, SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH
57
+ SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH
64
58
  };
@@ -1,5 +1,5 @@
1
1
  import { root } from '@esportsplus/reactivity';
2
- import { Element, Elements, Renderable, RenderableReactive, RenderableTemplate, RenderedGroup, Template } from '~/types';
2
+ import { Element, Elements, HydrateResult, Renderable, RenderableReactive, RenderableTemplate, Template } from '~/types';
3
3
  import { Slot } from '~/slot';
4
4
  import { cloneNode, firstChild, nextSibling } from '~/utilities';
5
5
  import { apply } from '~/attributes';
@@ -10,24 +10,19 @@ function reactive<T>(renderable: RenderableReactive<T>, slot: Slot) {
10
10
  let array = renderable.values,
11
11
  factory = renderable.template,
12
12
  refresh = () => {
13
- slot.render(
14
- root(() => array.map(template))
15
- );
16
- },
17
- renderer = (i: number, n?: number) => {
18
- return root(() => array.map(template, i, n)) as RenderedGroup[];
13
+ slot.render( root(() => array.map(template)) );
19
14
  },
20
15
  template = function(data, i) {
21
16
  let renderable = factory.call(this, data, i);
22
17
 
23
- return render(renderable, cache.get(renderable));
24
- } as Parameters<typeof array['map']>[0];
18
+ return hydrate<T>(renderable, cache.get(renderable));
19
+ } as (this: typeof array, ...args: Parameters<Parameters<typeof array['map']>[0]>) => HydrateResult;
25
20
 
26
21
  array.on('pop', () => {
27
22
  slot.pop();
28
23
  });
29
24
  array.on('push', ({ items }) => {
30
- slot.push(...renderer(array.length - items.length));
25
+ slot.push( ...root(() => array.map(template, array.length - items.length)) );
31
26
  });
32
27
  array.on('reverse', refresh);
33
28
  array.on('shift', () => {
@@ -35,16 +30,16 @@ function reactive<T>(renderable: RenderableReactive<T>, slot: Slot) {
35
30
  });
36
31
  array.on('sort', refresh);
37
32
  array.on('splice', ({ deleteCount: d, items: i, start: s }) => {
38
- slot.splice(s, d, ...renderer(s, i.length));
33
+ slot.splice(s, d, ...root(() => array.map(template, s, i.length)));
39
34
  });
40
35
  array.on('unshift', ({ items }) => {
41
- slot.unshift(...renderer(0, items.length));
36
+ slot.unshift( ...root(() => array.map(template, 0, items.length)) );
42
37
  });
43
38
 
44
- return array.map(template) as RenderedGroup[];
39
+ return array.map(template);
45
40
  }
46
41
 
47
- function render<T>(renderable: Renderable<T>, template: Template) {
42
+ function hydrate<T>(renderable: Renderable<T>, template: Template): HydrateResult {
48
43
  let elements: Elements = [],
49
44
  fragment = cloneNode.call(template.fragment, true),
50
45
  slots = template.slots;
@@ -58,7 +53,9 @@ function render<T>(renderable: Renderable<T>, template: Template) {
58
53
  let { fn, path, slot } = slots[i];
59
54
 
60
55
  if (path !== previous) {
61
- apply(node);
56
+ if (node) {
57
+ apply(node);
58
+ }
62
59
 
63
60
  node = fragment;
64
61
  previous = path;
@@ -88,6 +85,6 @@ export default {
88
85
  return reactive(renderable, slot);
89
86
  },
90
87
  static: <T>(renderable: RenderableTemplate<T>) => {
91
- return render(renderable, renderable.template || (renderable.template = cache.get(renderable)));
88
+ return hydrate(renderable, renderable.template || (renderable.template = cache.get(renderable)));
92
89
  }
93
90
  };
package/src/slot.ts CHANGED
@@ -1,44 +1,44 @@
1
1
  import { effect, root } from '@esportsplus/reactivity';
2
2
  import { isArray, isFunction, isInstanceOf, isObject } from '@esportsplus/utilities';
3
- import { RENDERABLE, RENDERABLE_REACTIVE, SLOT_CLEANUP } from './constants';
3
+ import { RENDERABLE, RENDERABLE_REACTIVE } from './constants';
4
4
  import { hydrate } from './html';
5
- import { Element, Elements, RenderableReactive, RenderableTemplate, RenderedGroup } from './types';
6
- import { append, firstChild, fragment, microtask, nextSibling, nodeValue, raf, text } from './utilities'
5
+ import { Element, Elements, HydrateResult, RenderableReactive, RenderableTemplate } from './types';
6
+ import { append, cloneNode, firstChild, fragment, microtask, nextSibling, nodeValue, raf, text } from './utilities'
7
7
  import queue from '@esportsplus/queue';
8
8
 
9
9
 
10
+ const CLEANUP_KEY = Symbol();
11
+
12
+
10
13
  let cleanup = queue<VoidFunction[]>(64),
11
- fallback = fragment(''),
12
- scheduled = false;
14
+ scheduled = false,
15
+ template = fragment('');
13
16
 
14
17
 
15
- function after(anchor: Element, groups: RenderedGroup[]) {
16
- let elements: Elements[] = [],
17
- n = groups.length;
18
+ function after(anchor: Element, groups: HydrateResult[]) {
19
+ let n = groups.length;
18
20
 
19
- if (n) {
20
- let fragment = groups[0].fragment || fallback;
21
+ if (n === 0) {
22
+ return [];
23
+ }
21
24
 
22
- if (n === 1) {
23
- elements.push( groups[0].elements );
24
- }
25
- else {
26
- for (let i = 1; i < n; i++) {
27
- let group = groups[i];
25
+ let elements: Elements[] = [],
26
+ fragment = groups[0].fragment || cloneNode.call(template);
28
27
 
29
- if (group.fragment) {
30
- append.call(fragment, group.fragment);
31
- group.fragment = null;
32
- }
28
+ if (n === 1) {
29
+ elements.push( groups[0].elements );
30
+ }
31
+ else {
32
+ for (let i = 1; i < n; i++) {
33
+ let { elements: e, fragment: f } = groups[i];
33
34
 
34
- elements.push(group.elements);
35
- }
35
+ append.call(fragment, f);
36
+ elements.push(e);
36
37
  }
37
-
38
- anchor.after(fragment);
39
- groups[0].fragment = null;
40
38
  }
41
39
 
40
+ anchor.after(fragment);
41
+
42
42
  return elements;
43
43
  }
44
44
 
@@ -49,8 +49,8 @@ function remove(...groups: Elements[]) {
49
49
  for (let j = 0, o = group.length; j < o; j++) {
50
50
  let item = group[j];
51
51
 
52
- if (item[SLOT_CLEANUP]) {
53
- cleanup.add(item[SLOT_CLEANUP]);
52
+ if (CLEANUP_KEY in item) {
53
+ cleanup.add(item[CLEANUP_KEY] as VoidFunction[]);
54
54
  }
55
55
 
56
56
  item.remove();
@@ -64,42 +64,47 @@ function remove(...groups: Elements[]) {
64
64
  return groups;
65
65
  }
66
66
 
67
- function render(groups: RenderedGroup[], input: unknown, slot?: Slot) {
67
+ function render(elements: Elements[], fragment: DocumentFragment | Node, input: unknown, slot?: Slot) {
68
68
  if (input === false || input == null || input === '') {
69
- return groups;
69
+ return;
70
70
  }
71
71
 
72
72
  if (isArray(input)) {
73
73
  for (let i = 0, n = input.length; i < n; i++) {
74
- render(groups, input[i]);
74
+ render(elements, fragment, input[i]);
75
75
  }
76
76
  }
77
77
  else if (isObject(input) && RENDERABLE in input) {
78
78
  if (input[RENDERABLE] === RENDERABLE_REACTIVE) {
79
- groups.push(
80
- ...hydrate.reactive(input as RenderableReactive, slot!)
81
- );
79
+ let response = hydrate.reactive(input as RenderableReactive, slot!);
80
+
81
+ for (let i = 0, n = response.length; i < n; i++) {
82
+ let { elements: e, fragment: f } = response[i];
83
+
84
+ append.call(fragment, f);
85
+ elements.push(e);
86
+ }
82
87
  }
83
88
  else {
84
- groups.push(
85
- hydrate.static(input as RenderableTemplate<unknown>)
86
- );
89
+ let { elements: e, fragment: f } = hydrate.static(input as RenderableTemplate<unknown>);
90
+
91
+ append.call(fragment, f);
92
+ elements.push(e);
87
93
  }
88
94
  }
89
95
  else if (isInstanceOf(input, NodeList)) {
90
- let elements: Elements = [];
96
+ let e: Elements = [];
91
97
 
92
98
  for (let node = firstChild.call(input); node; node = nextSibling.call(node)) {
93
- elements.push(node);
99
+ e.push(node);
94
100
  }
95
101
 
96
- groups.push({ elements, fragment: null });
102
+ append.call(fragment, ...e);
103
+ elements.push(e);
97
104
  }
98
105
  else if (isInstanceOf(input, Node)) {
99
- groups.push({
100
- elements: [ input as Element ],
101
- fragment: input as RenderedGroup['fragment']
102
- });
106
+ append.call(fragment, input);
107
+ elements.push([ input as Element ]);
103
108
  }
104
109
  else {
105
110
  let element = text( typeof input === 'string' ? input : String(input) );
@@ -108,13 +113,9 @@ function render(groups: RenderedGroup[], input: unknown, slot?: Slot) {
108
113
  slot.text = element;
109
114
  }
110
115
 
111
- groups.push({
112
- elements: [ element ],
113
- fragment: element
114
- });
116
+ append.call(fragment, element);
117
+ elements.push([ element ]);
115
118
  }
116
-
117
- return groups;
118
119
  }
119
120
 
120
121
  function schedule() {
@@ -174,7 +175,7 @@ class Slot {
174
175
  nodes = this.nodes[index];
175
176
 
176
177
  if (nodes) {
177
- node = nodes.at(-1);
178
+ node = nodes[nodes.length - 1];
178
179
  }
179
180
 
180
181
  return node || this.marker;
@@ -196,7 +197,7 @@ class Slot {
196
197
  return remove(group);
197
198
  }
198
199
 
199
- push(...groups: RenderedGroup[]) {
200
+ push(...groups: HydrateResult[]) {
200
201
  return this.nodes.push( ...after(this.anchor(), groups) );
201
202
  }
202
203
 
@@ -236,7 +237,14 @@ class Slot {
236
237
  }
237
238
 
238
239
  this.clear();
239
- this.nodes = after(this.marker, render([], input, this));
240
+
241
+ let fragment = cloneNode.call(template),
242
+ nodes: Elements[] = [];
243
+
244
+ render(nodes, fragment, input, this);
245
+
246
+ this.marker.after(fragment);
247
+ this.nodes = nodes;
240
248
 
241
249
  return this;
242
250
  }
@@ -251,7 +259,7 @@ class Slot {
251
259
  return remove(group);
252
260
  }
253
261
 
254
- splice(start: number, stop: number = this.nodes.length, ...groups: RenderedGroup[]) {
262
+ splice(start: number, stop: number = this.nodes.length, ...groups: HydrateResult[]) {
255
263
  return remove(
256
264
  ...this.nodes.splice(
257
265
  start,
@@ -261,14 +269,14 @@ class Slot {
261
269
  );
262
270
  }
263
271
 
264
- unshift(...groups: RenderedGroup[]) {
272
+ unshift(...groups: HydrateResult[]) {
265
273
  return this.nodes.unshift( ...after(this.marker, groups) );
266
274
  }
267
275
  }
268
276
 
269
277
 
270
278
  const ondisconnect = (element: Element, fn: VoidFunction) => {
271
- ( element[SLOT_CLEANUP] ??= [] ).push(fn);
279
+ ((element[CLEANUP_KEY] ??= []) as VoidFunction[]).push(fn);
272
280
  };
273
281
 
274
282
 
package/src/types.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import { ReactiveArray } from '@esportsplus/reactivity';
2
- import {
3
- ATTRIBUTE_STORE,
4
- RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE,
5
- SLOT_CLEANUP
6
- } from './constants';
2
+ import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE } from './constants';
7
3
  import { firstChild } from './utilities';
8
4
  import attributes from './attributes';
9
5
  import slot from './slot';
@@ -30,13 +26,12 @@ type Effect<T> = () => EffectResponse<T>;
30
26
 
31
27
  type EffectResponse<T> = T extends [] ? EffectResponse<T[number]>[] : Primitive | Renderable<T>;
32
28
 
33
- type Element = HTMLElement & Attributes & {
34
- [ATTRIBUTE_STORE]?: Record<PropertyKey, unknown>;
35
- [SLOT_CLEANUP]?: VoidFunction[]
36
- } & Record<PropertyKey, unknown>;
29
+ type Element = HTMLElement & Attributes & Record<PropertyKey, unknown>;
37
30
 
38
31
  type Elements = Element[];
39
32
 
33
+ type HydrateResult = { elements: Elements, fragment: DocumentFragment | Node };
34
+
40
35
  // Copied from '@esportsplus/utilities'
41
36
  // - Importing from ^ causes 'cannot be named without a reference to...' error
42
37
  type Primitive = bigint | boolean | null | number | string | undefined;
@@ -62,8 +57,6 @@ type RenderableTemplate<T> = {
62
57
 
63
58
  type RenderableValue<T = unknown> = Attributes | Readonly<Attributes> | Readonly<Attributes[]> | Effect<T> | Primitive | Renderable;
64
59
 
65
- type RenderedGroup = { elements: Elements, fragment: DocumentFragment | Node | null };
66
-
67
60
  type Template = {
68
61
  fragment: DocumentFragment;
69
62
  html: string;
@@ -79,6 +72,7 @@ type Template = {
79
72
  export type {
80
73
  Attributes,
81
74
  Effect, Element, Elements,
82
- Renderable, RenderableReactive, RenderableTemplate, RenderableValue, RenderedGroup,
75
+ HydrateResult,
76
+ Renderable, RenderableReactive, RenderableTemplate, RenderableValue,
83
77
  Template
84
78
  };
package/src/utilities.ts CHANGED
@@ -7,13 +7,15 @@ let prototype,
7
7
  t = document.createTextNode('');
8
8
 
9
9
 
10
+ prototype = DocumentFragment.prototype;
11
+
12
+ const append = prototype.append
13
+
10
14
  // https://github.com/localvoid/ivi/blob/master/packages/ivi/src/client/core.ts#L38
11
15
  prototype = Element.prototype;
12
16
 
13
17
  const addEventListener = prototype.addEventListener;
14
18
 
15
- const append = prototype.append;
16
-
17
19
  const removeEventListener = prototype.removeEventListener;
18
20
 
19
21
  const className = Object.getOwnPropertyDescriptor(prototype, 'className')!.set!;