@esportsplus/template 0.16.1 → 0.17.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.

package/src/constants.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { fragment } from './utilities';
2
2
 
3
3
 
4
+ const CLEANUP = Symbol();
5
+
6
+
4
7
  const EMPTY_FRAGMENT = fragment('');
5
8
 
6
9
 
@@ -44,9 +47,21 @@ const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{
44
47
 
45
48
  const RENDERABLE = Symbol();
46
49
 
47
- const RENDERABLE_REACTIVE = Symbol();
50
+ const RENDERABLE_ARRAY = 0;
51
+
52
+ const RENDERABLE_FRAGMENT = 1;
53
+
54
+ const RENDERABLE_HTML_FRAGMENT = 2;
55
+
56
+ const RENDERABLE_HTML_REACTIVE_ARRAY = 3;
57
+
58
+ const RENDERABLE_NODE = 4;
48
59
 
49
- const RENDERABLE_TEMPLATE = Symbol();
60
+ const RENDERABLE_NODE_LIST = 5;
61
+
62
+ const RENDERABLE_TEXT = 6;
63
+
64
+ const RENDERABLE_VOID = 7;
50
65
 
51
66
 
52
67
  const SLOT_HTML = '<!--$-->';
@@ -56,10 +71,22 @@ const SLOT_MARKER = '{{$}}';
56
71
  const SLOT_MARKER_LENGTH = SLOT_MARKER.length;
57
72
 
58
73
 
74
+ const STATE_HYDRATING = 0;
75
+
76
+ const STATE_NONE = 1;
77
+
78
+ const STATE_WAITING = 2;
79
+
80
+
59
81
  export {
82
+ CLEANUP,
60
83
  EMPTY_FRAGMENT,
61
84
  NODE_CLOSING, NODE_ELEMENT, NODE_SLOT, NODE_VOID, NODE_WHITELIST,
62
85
  REGEX_EMPTY_TEXT_NODES, REGEX_SLOT_NODES,
63
- RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE,
64
- SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH
86
+ RENDERABLE, RENDERABLE_ARRAY, RENDERABLE_FRAGMENT,
87
+ RENDERABLE_HTML_FRAGMENT, RENDERABLE_HTML_REACTIVE_ARRAY,
88
+ RENDERABLE_NODE, RENDERABLE_NODE_LIST, RENDERABLE_TEXT,
89
+ RENDERABLE_VOID,
90
+ SLOT_HTML, SLOT_MARKER, SLOT_MARKER_LENGTH,
91
+ STATE_HYDRATING, STATE_NONE, STATE_WAITING
65
92
  };
package/src/event.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { root } from '@esportsplus/reactivity';
2
2
  import { defineProperty } from '@esportsplus/utilities';
3
- import { ondisconnect } from './slot';
4
3
  import { Element } from './types';
5
4
  import { addEventListener, parentElement, raf } from './utilities';
5
+ import { ondisconnect } from './slot/cleanup';
6
6
 
7
7
 
8
8
  let capture = new Set<`on${string}`>(['onblur', 'onfocus', 'onscroll']),
@@ -91,15 +91,15 @@ export default (element: Element, event: `on${string}`, listener: Function): voi
91
91
  addEventListener.call(window.document, event.slice(2), (e) => {
92
92
  let node = e.target as Element | null;
93
93
 
94
- defineProperty(e, 'currentTarget', {
95
- configurable: true,
96
- get() {
97
- return node || window.document;
98
- }
99
- });
100
-
101
94
  while (node) {
102
95
  if (key in node) {
96
+ defineProperty(e, 'currentTarget', {
97
+ configurable: true,
98
+ get() {
99
+ return node || window.document;
100
+ }
101
+ });
102
+
103
103
  return (node[key] as Function).call(node, e);
104
104
  }
105
105
 
@@ -1,125 +1,53 @@
1
- import { root } from '@esportsplus/reactivity';
2
- import { EMPTY_FRAGMENT } from '~/constants';
3
- import { Elements, Fragment, RenderableReactive, RenderableTemplate } from '~/types';
4
- import { Slot } from '~/slot';
1
+ import { Fragment, RenderableValues, Template } from '~/types';
5
2
  import { cloneNode } from '~/utilities';
6
- import cache from './cache';
7
3
 
8
4
 
9
- function reactive<T>(elements: Elements[], fragment: Fragment, renderable: RenderableReactive<T>, slot: Slot) {
10
- let array = renderable.values,
11
- factory = renderable.template,
12
- refresh = () => {
13
- root(() => array.map(template));
5
+ export default ({ fragment, slots }: Template, values: RenderableValues[]): Fragment => {
6
+ let clone = cloneNode.call(fragment, true);
14
7
 
15
- slot.clear();
16
- slot.anchor().after(fragment);
17
- slot.nodes = elements;
18
-
19
- reset();
20
- },
21
- reset = () => {
22
- elements = [];
23
- fragment = cloneNode.call(EMPTY_FRAGMENT);
24
- },
25
- template = function(data, i) {
26
- hydrate(elements, fragment, factory.call(this, data, i));
27
- } as (this: typeof array, ...args: Parameters<Parameters<typeof array['map']>[0]>) => void;
28
-
29
- array.on('clear', () => slot.clear());
30
- array.on('pop', () => slot.pop());
31
- array.on('reverse', refresh);
32
- array.on('shift', () => slot.shift());
33
- array.on('sort', refresh);
34
-
35
- array.on('push', ({ items }) => {
36
- let anchor = slot.anchor();
37
-
38
- elements = slot.nodes;
39
-
40
- root(() => array.map(template, array.length - items.length));
41
-
42
- anchor.after(fragment);
43
- reset();
44
- });
45
- array.on('splice', ({ deleteCount: d, items: i, start: s }) => {
46
- if (array.length === 0) {
47
- slot.clear();
48
- return;
49
- }
50
-
51
- root(() => array.map(template, s, i.length))
52
-
53
- slot.splice(s, d, fragment, ...elements);
54
- reset();
55
- });
56
- array.on('unshift', ({ items }) => {
57
- root(() => array.map(template, 0, items.length))
58
-
59
- slot.unshift(fragment, ...elements);
60
- reset();
61
- });
62
-
63
- root(() => array.map(template));
64
- reset();
65
- }
66
-
67
- function hydrate<T>(elements: Elements[] | null, fragment: Fragment, renderable: RenderableTemplate<T>) {
68
- let { fragment: frag, slots } = cache.get(renderable.literals),
69
- clone = cloneNode.call(frag, true);
8
+ if (slots === null) {
9
+ return clone as Fragment;
10
+ }
70
11
 
71
- if (slots !== null) {
72
- let node,
73
- nodePath,
74
- parent,
75
- parentPath,
76
- values = renderable.values;
12
+ let node,
13
+ nodePath,
14
+ parent,
15
+ parentPath;
77
16
 
78
- for (let i = 0, n = slots.length; i < n; i++) {
79
- let { fn, path, slot } = slots[i],
80
- pp = path.parent,
81
- pr = path.relative;
17
+ for (let i = 0, n = slots.length; i < n; i++) {
18
+ let { fn, path, slot } = slots[i],
19
+ pp = path.parent,
20
+ pr = path.relative;
82
21
 
83
- if (pp !== parentPath) {
84
- if (pp === nodePath) {
85
- parent = node;
86
- parentPath = nodePath;
22
+ if (pp !== parentPath) {
23
+ if (pp === nodePath) {
24
+ parent = node;
25
+ parentPath = nodePath;
87
26
 
88
- nodePath = undefined;
89
- }
90
- else {
91
- parent = clone;
92
- parentPath = pp;
27
+ nodePath = undefined;
28
+ }
29
+ else {
30
+ parent = clone;
31
+ parentPath = pp;
93
32
 
94
- for (let o = 0, j = pp.length; o < j; o++) {
95
- parent = pp[o].call(parent);
96
- }
33
+ for (let i = 0, n = pp.length; i < n; i++) {
34
+ parent = pp[i].call(parent);
97
35
  }
98
36
  }
37
+ }
99
38
 
100
- if (pr !== nodePath) {
101
- node = parent;
102
- nodePath = path.absolute;
39
+ if (pr !== nodePath) {
40
+ node = parent;
41
+ nodePath = path.absolute;
103
42
 
104
- for (let o = 0, j = pr.length; o < j; o++) {
105
- node = pr[o].call(node);
106
- }
43
+ for (let i = 0, n = pr.length; i < n; i++) {
44
+ node = pr[i].call(node);
107
45
  }
108
-
109
- // @ts-ignore
110
- fn(node, values[slot]);
111
46
  }
112
- }
113
47
 
114
- if (elements) {
115
- elements.push([...clone.childNodes] as Elements);
48
+ // @ts-ignore
49
+ fn(node, values[slot]);
116
50
  }
117
51
 
118
- fragment.appendChild(clone)
119
- }
120
-
121
-
122
- export default {
123
- reactive,
124
- static: hydrate
125
- };
52
+ return clone as Fragment;
53
+ }
package/src/html/index.ts CHANGED
@@ -1,17 +1,25 @@
1
1
  import { ReactiveArray } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_REACTIVE, RENDERABLE_TEMPLATE } from '~/constants';
3
- import { RenderableReactive, RenderableTemplate } from '~/types';
2
+ import { RENDERABLE, RENDERABLE_HTML_FRAGMENT, RENDERABLE_HTML_REACTIVE_ARRAY } from '~/constants';
3
+ import { RenderableReactive, RenderableTemplate, RenderableValues } from '~/types';
4
4
  import hydrate from './hydrate';
5
+ import cache from './cache';
5
6
 
6
7
 
7
- const html = <T>(literals: TemplateStringsArray, ...values: RenderableTemplate<T>['values']): RenderableTemplate<T> => {
8
- return { [RENDERABLE]: RENDERABLE_TEMPLATE, literals, template: null, values };
8
+ const html = (literals: TemplateStringsArray, ...values: RenderableValues[]): RenderableTemplate => {
9
+ return {
10
+ [RENDERABLE]: RENDERABLE_HTML_FRAGMENT,
11
+ fragment: hydrate(cache.get(literals), values),
12
+ literals
13
+ };
9
14
  };
10
15
 
11
- html.reactive = <T>(array: ReactiveArray<T>, template: RenderableReactive<T>['template']): RenderableReactive<T> => {
12
- return { [RENDERABLE]: RENDERABLE_REACTIVE, literals: null, template, values: array };
16
+ html.reactive = <T>(array: ReactiveArray<T[]>, template: RenderableReactive['template']) => {
17
+ return {
18
+ [RENDERABLE]: RENDERABLE_HTML_REACTIVE_ARRAY,
19
+ array,
20
+ template
21
+ };
13
22
  };
14
23
 
15
24
 
16
- export default html;
17
- export { hydrate };
25
+ export default html;
package/src/render.ts CHANGED
@@ -1,21 +1,17 @@
1
- import { isInstanceOf } from '@esportsplus/utilities';
2
1
  import { SLOT_HTML } from './constants';
3
- import slot, { Slot } from './slot';
4
2
  import { Renderable } from './types';
5
- import { firstChild, fragment, nodeValue, prepend } from './utilities';
3
+ import { firstChild, fragment, nodeValue } from './utilities';
4
+ import slot from './slot';
6
5
 
7
6
 
8
- let marker = firstChild.call( fragment(SLOT_HTML) ),
9
- node;
7
+ let anchor,
8
+ marker = firstChild.call( fragment(SLOT_HTML) );
10
9
 
11
10
 
12
- export default (renderable: Renderable, parent: HTMLElement | Slot) => {
13
- if (isInstanceOf(parent, Slot)) {
14
- return parent.render(renderable);
15
- }
16
-
11
+ export default (parent: HTMLElement, renderable: Renderable) => {
12
+ // parent.nodeValue = '';
17
13
  nodeValue.call(parent, '');
18
- prepend.call(parent, node = marker.cloneNode());
14
+ parent.append(anchor = marker.cloneNode());
19
15
 
20
- return slot(node, renderable);
16
+ return slot(anchor, renderable);
21
17
  };
@@ -0,0 +1,74 @@
1
+ import queue from '@esportsplus/queue';
2
+ import { CLEANUP } from '~/constants';
3
+ import { microtask, previousSibling } from '~/utilities';
4
+ import { SlotGroup } from '~/types';
5
+
6
+
7
+ let cleanup = queue<VoidFunction[]>(64),
8
+ scheduled = false;
9
+
10
+
11
+ function schedule() {
12
+ if (!cleanup.length || scheduled) {
13
+ return;
14
+ }
15
+
16
+ scheduled = true;
17
+ microtask.add(task);
18
+ }
19
+
20
+ function task() {
21
+ try {
22
+ let fns,
23
+ fn,
24
+ n = cleanup.length;
25
+
26
+ while ((fns = cleanup.next()) && n--) {
27
+ while (fn = fns.pop()) {
28
+ fn();
29
+ }
30
+ }
31
+ }
32
+ catch { }
33
+
34
+ if (cleanup.length) {
35
+ microtask.add(task);
36
+ }
37
+ else {
38
+ scheduled = false;
39
+ }
40
+ }
41
+
42
+
43
+ const ondisconnect = (element: Element, fn: VoidFunction) => {
44
+ ((element as any)[CLEANUP] ??= []).push(fn);
45
+ };
46
+
47
+ const remove = (groups: SlotGroup[]) => {
48
+ let group, head, tail;
49
+
50
+ while (group = groups.pop()) {
51
+ head = group.head;
52
+ tail = group.tail || head;
53
+
54
+ // for (let node: Element | null = tail; node; node = node.previousSibling as Element | null) {
55
+ for (let node = tail; node; node = previousSibling.call(node)) {
56
+ if (CLEANUP in node) {
57
+ cleanup.add( node[CLEANUP] as VoidFunction[] );
58
+ }
59
+
60
+ node.remove();
61
+
62
+ if (head === node) {
63
+ break;
64
+ }
65
+ }
66
+ }
67
+
68
+ if (!scheduled && cleanup.length) {
69
+ schedule();
70
+ }
71
+ };
72
+
73
+
74
+ export { ondisconnect, remove };
@@ -0,0 +1,73 @@
1
+ import { effect } from '@esportsplus/reactivity';
2
+ import { EMPTY_FRAGMENT, STATE_HYDRATING, STATE_NONE } from '~/constants';
3
+ import { Element, Fragment, SlotGroup } from '~/types';
4
+ import { cloneNode, firstChild, lastChild, nodeValue, raf, text } from '~/utilities'
5
+ import { ondisconnect } from '~/slot/cleanup';
6
+ import { remove } from './cleanup';
7
+ import render from './render';
8
+
9
+
10
+ function update(this: { group?: SlotGroup, textnode?: Element }, anchor: Element, fragment: Fragment, value: unknown) {
11
+ let type = typeof value;
12
+
13
+ if (this.group) {
14
+ remove([this.group]);
15
+ this.group = undefined;
16
+ }
17
+
18
+ if (value == null || type !== 'object') {
19
+ let textnode = this.textnode;
20
+
21
+ if (textnode) {
22
+ // textnode.nodeValue = String(value);
23
+ nodeValue.call(textnode, String(value));
24
+ }
25
+ else {
26
+ textnode = this.textnode = text( String(value) );
27
+ }
28
+
29
+ if (!textnode.isConnected) {
30
+ anchor.after(textnode);
31
+ }
32
+ }
33
+ else {
34
+ render(anchor, fragment, value);
35
+
36
+ this.group = {
37
+ // head: fragment.firstChild as Element,
38
+ // tail: fragment.lastChild as Element
39
+ head: firstChild.call(fragment),
40
+ tail: lastChild.call(fragment)
41
+ };
42
+
43
+ anchor.after(fragment);
44
+ }
45
+ }
46
+
47
+
48
+ export default (anchor: Element, fn: Function) => {
49
+ let context = {
50
+ group: undefined as SlotGroup | undefined,
51
+ textnode: undefined as Element | undefined
52
+ },
53
+ // fragment = EMPTY_FRAGMENT.cloneNode() as Fragment,
54
+ fragment = cloneNode.call(EMPTY_FRAGMENT) as Fragment,
55
+ state = STATE_HYDRATING;
56
+
57
+ ondisconnect(
58
+ anchor,
59
+ effect(() => {
60
+ let value = fn();
61
+
62
+ if (state === STATE_HYDRATING) {
63
+ update.call(context, anchor, fragment, value);
64
+ state = STATE_NONE;
65
+ }
66
+ else if (state === STATE_NONE) {
67
+ raf.add(() => {
68
+ update.call(context, anchor, fragment, value);
69
+ });
70
+ }
71
+ })
72
+ );
73
+ };
@@ -0,0 +1,23 @@
1
+ import { EMPTY_FRAGMENT } from '~/constants';
2
+ import { Element, Fragment } from '~/types';
3
+ import { cloneNode } from '~/utilities';
4
+ import effect from './effect';
5
+ import render from './render';
6
+
7
+
8
+ function slot(anchor: Element, input: unknown) {
9
+ let fragment = cloneNode.call(EMPTY_FRAGMENT) as Fragment;
10
+
11
+ render(anchor, fragment, input);
12
+
13
+ anchor.after(fragment);
14
+ }
15
+
16
+
17
+ export default (anchor: Element, value: unknown) => {
18
+ if (typeof value === 'function') {
19
+ return effect(anchor, value as Function);
20
+ }
21
+
22
+ return slot(anchor, value);
23
+ };
@@ -0,0 +1,167 @@
1
+ import { root, ReactiveArray } from '@esportsplus/reactivity';
2
+ import { EMPTY_FRAGMENT } from '~/constants';
3
+ import { Fragment, RenderableReactive, SlotGroup } from '~/types';
4
+ import { append, cloneNode, firstChild, lastChild } from '~/utilities';
5
+ import { remove } from './cleanup';
6
+
7
+
8
+ class ReactiveArraySlot<T> {
9
+ array: ReactiveArray<T[]>;
10
+ fragment = cloneNode.call(EMPTY_FRAGMENT) as Fragment;
11
+ marker: Element;
12
+ nodes: SlotGroup[] = [];
13
+ template: (
14
+ this: ReactiveArray<T[]>,
15
+ ...args: Parameters< Parameters<ReactiveArray<T[]>['map']>[0] >
16
+ ) => SlotGroup;
17
+
18
+
19
+ constructor(anchor: Element, array: ReactiveArray<T[]>, template: RenderableReactive['template']) {
20
+ let fragment = this.fragment;
21
+
22
+ this.array = array;
23
+ this.marker = anchor;
24
+ this.template = function (data, i) {
25
+ let frag = template.call(this, data, i).fragment,
26
+ group = {
27
+ head: firstChild.call(frag),
28
+ tail: lastChild.call(frag)
29
+ };
30
+
31
+ append.call(fragment, frag);
32
+
33
+ return group;
34
+ };
35
+
36
+
37
+ let render = () => {
38
+ root(() => this.render());
39
+ };
40
+
41
+ array.on('clear', () => this.clear());
42
+ array.on('reverse', render);
43
+ array.on('pop', () => this.pop());
44
+ array.on('push', ({ items }) => {
45
+ root(() => this.push(items));
46
+ });
47
+ array.on('shift', () => this.shift());
48
+ array.on('sort', render);
49
+ array.on('splice', ({ deleteCount, items, start }) => {
50
+ root(() => this.splice(start, deleteCount, ...items));
51
+ });
52
+ array.on('unshift', ({ items }) => {
53
+ root(() => this.unshift(items));
54
+ });
55
+
56
+ if (array.length) {
57
+ render();
58
+ }
59
+ }
60
+
61
+
62
+ get length() {
63
+ return this.nodes.length;
64
+ }
65
+
66
+ set length(n: number) {
67
+ if (n >= this.nodes.length) {
68
+ return;
69
+ }
70
+ else if (n === 0) {
71
+ this.clear();
72
+ }
73
+ else {
74
+ this.splice(n);
75
+ }
76
+ }
77
+
78
+
79
+ anchor(index: number = this.nodes.length - 1) {
80
+ let node = this.nodes[index];
81
+
82
+ if (node) {
83
+ return node.tail || node.head;
84
+ }
85
+
86
+ return this.marker;
87
+ }
88
+
89
+ clear() {
90
+ remove(this.nodes);
91
+ }
92
+
93
+ pop() {
94
+ let group = this.nodes.pop();
95
+
96
+ if (group) {
97
+ remove([group]);
98
+ }
99
+ }
100
+
101
+ push(items: T[]) {
102
+ let anchor = this.anchor(),
103
+ array = this.array;
104
+
105
+ for (let i = 0, n = items.length; i < n; i++) {
106
+ this.nodes.push(
107
+ this.template.call(array, items[i], i)
108
+ );
109
+ }
110
+
111
+ anchor.after(this.fragment);
112
+ }
113
+
114
+ render() {
115
+ let nodes = this.nodes;
116
+
117
+ if (nodes.length) {
118
+ remove(nodes);
119
+ }
120
+
121
+ nodes = this.array.map(this.template);
122
+ this.marker.after(this.fragment);
123
+ }
124
+
125
+ shift() {
126
+ let group = this.nodes.shift();
127
+
128
+ if (group) {
129
+ remove([group]);
130
+ }
131
+ }
132
+
133
+ splice(start: number, stop: number = this.nodes.length, ...items: T[]) {
134
+ if (!items.length) {
135
+ return remove(this.nodes.splice(start, stop));
136
+ }
137
+
138
+ let array = this.array,
139
+ n = items.length,
140
+ nodes = new Array(n);
141
+
142
+ for (let i = 0; i < n; i++) {
143
+ nodes[i] = this.template.call(array, items[i], i);
144
+ }
145
+
146
+ remove(this.nodes.splice(start, stop, ...nodes));
147
+ this.anchor(start - 1).after(this.fragment);
148
+ }
149
+
150
+ unshift(items: T[]) {
151
+ let array = this.array,
152
+ n = items.length,
153
+ nodes = new Array(n);
154
+
155
+ for (let i = 0; i < n; i++) {
156
+ nodes[i] = this.template.call(array, items[i], i);
157
+ }
158
+
159
+ this.nodes.unshift(...nodes);
160
+ this.marker.after(this.fragment);
161
+ }
162
+ }
163
+
164
+
165
+ export default (anchor: Element, renderable: RenderableReactive) => {
166
+ new ReactiveArraySlot(anchor, renderable.array, renderable.template);
167
+ };