@esportsplus/template 0.23.2 → 0.23.4

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,10 +1,8 @@
1
- import { STATE_HYDRATING, STATE_NONE, STATE_WAITING } from './constants.js';
2
1
  import { Attributes, Element } from './types.js';
3
- type State = typeof STATE_HYDRATING | typeof STATE_NONE | typeof STATE_WAITING;
4
- declare const set: (element: Element, name: string, value: unknown, state?: State) => void;
2
+ declare const set: (element: Element, name: string, value: unknown) => void;
5
3
  declare const spread: (element: Element, value: Attributes | Attributes[]) => void;
6
4
  declare const _default: {
7
- set: (element: Element, name: string, value: unknown, state?: State) => void;
5
+ set: (element: Element, name: string, value: unknown) => void;
8
6
  spread: (element: Element, value: Attributes | Attributes[]) => void;
9
7
  };
10
8
  export default _default;
@@ -31,28 +31,51 @@ function list(ctx, element, id, name, state, value) {
31
31
  if (value == null || value === false || value === '') {
32
32
  value = '';
33
33
  }
34
- if (id === null && (!value || typeof value !== 'string')) {
35
- return;
36
- }
37
- let delimiter = delimiters[name], store = (ctx ??= context(element)).store ??= {};
38
- if (store[name] === undefined) {
39
- store[name] = (element.getAttribute(name) || '').trim();
34
+ let base = name + '.static', delimiter = delimiters[name], store = (ctx ??= context(element)).store ??= {}, dynamic = store[name], type = typeof value;
35
+ if (dynamic === undefined) {
36
+ let value = (element.getAttribute(name) || '').trim();
37
+ store[base] = value;
38
+ store[name] = dynamic = new Set();
40
39
  }
41
- let current = value ? (delimiter + value) : '';
42
40
  if (id === null) {
43
- store[name] += current;
41
+ if (value && type === 'string') {
42
+ store[base] += (store[base] ? delimiter : '') + value;
43
+ }
44
44
  }
45
45
  else {
46
- let previous = store[id];
47
- if (!previous) {
48
- store[name] += current;
46
+ let hot = {};
47
+ if (value && type === 'string') {
48
+ let part, parts = value.split(delimiter);
49
+ for (let i = 0, n = parts.length; i < n; i++) {
50
+ part = parts[i].trim();
51
+ if (part === '') {
52
+ continue;
53
+ }
54
+ dynamic.add(part);
55
+ hot[part] = null;
56
+ }
49
57
  }
50
- else if (previous !== current) {
51
- store[name] = store[name].replace(previous, current);
58
+ let cold = store[id];
59
+ if (cold !== undefined) {
60
+ for (let part in cold) {
61
+ if (part in hot) {
62
+ continue;
63
+ }
64
+ dynamic.delete(part);
65
+ }
52
66
  }
53
- store[id] = current;
67
+ store[id] = hot;
68
+ }
69
+ value = store[base];
70
+ for (let key of dynamic) {
71
+ value += (value ? delimiter : '') + key;
72
+ }
73
+ if (state === STATE_HYDRATING) {
74
+ apply(element, name, value);
75
+ }
76
+ else {
77
+ schedule(ctx, element, name, state, value);
54
78
  }
55
- schedule(ctx, element, name, state, store[name]);
56
79
  }
57
80
  function property(ctx, element, id, name, state, value) {
58
81
  if (value == null || value === false || value === '') {
@@ -65,13 +88,14 @@ function property(ctx, element, id, name, state, value) {
65
88
  }
66
89
  ctx[name] = value;
67
90
  }
68
- schedule(ctx, element, name, state, value);
69
- }
70
- function schedule(ctx, element, name, state, value) {
71
91
  if (state === STATE_HYDRATING) {
72
92
  apply(element, name, value);
73
- return;
74
93
  }
94
+ else {
95
+ schedule(ctx, element, name, state, value);
96
+ }
97
+ }
98
+ function schedule(ctx, element, name, state, value) {
75
99
  ctx ??= context(element);
76
100
  (ctx.updates ??= {})[name] = value;
77
101
  if (state === STATE_NONE && !ctx.updating) {
@@ -101,12 +125,11 @@ function task() {
101
125
  scheduled = false;
102
126
  }
103
127
  }
104
- const set = (element, name, value, state = STATE_HYDRATING) => {
105
- let fn = name === 'class' || name === 'style' ? list : property, type = typeof value;
128
+ const set = (element, name, value) => {
129
+ let fn = name === 'class' || name === 'style' ? list : property, state = STATE_HYDRATING, type = typeof value;
106
130
  if (type === 'function') {
107
131
  if (name.startsWith('on')) {
108
- event(element, name, value);
109
- return;
132
+ return event(element, name, value);
110
133
  }
111
134
  let ctx = context(element);
112
135
  ctx.effect ??= 0;
@@ -129,16 +152,14 @@ const set = (element, name, value, state = STATE_HYDRATING) => {
129
152
  return;
130
153
  }
131
154
  if (type !== 'object') {
132
- fn(null, element, null, name, state, value);
133
- return;
134
155
  }
135
- if (isArray(value)) {
156
+ else if (isArray(value)) {
136
157
  for (let i = 0, n = value.length; i < n; i++) {
137
158
  let v = value[i];
138
159
  if (v == null || v === false || v === '') {
139
160
  continue;
140
161
  }
141
- set(element, name, v, state);
162
+ set(element, name, v);
142
163
  }
143
164
  return;
144
165
  }
@@ -146,7 +167,8 @@ const set = (element, name, value, state = STATE_HYDRATING) => {
146
167
  };
147
168
  const spread = function (element, value) {
148
169
  if (isObject(value)) {
149
- for (let name in value) {
170
+ let names = Object.keys(value), name;
171
+ while (name = names.pop()) {
150
172
  let v = value[name];
151
173
  if (v == null || v === false || v === '') {
152
174
  continue;
@@ -66,15 +66,15 @@ export default (element, event, listener) => {
66
66
  case 'ondisconnect':
67
67
  ondisconnect(element, () => listener(element));
68
68
  return;
69
+ case 'onrender':
70
+ root(() => listener(element));
71
+ return;
69
72
  case 'onresize':
70
73
  onresize(element, listener);
71
74
  return;
72
75
  case 'ontick':
73
76
  ontick(element, listener);
74
77
  return;
75
- case 'onrender':
76
- root(() => listener(element));
77
- return;
78
78
  default:
79
79
  element[keys[event] || register(element, event)] = listener;
80
80
  return;
@@ -3,7 +3,7 @@ import { EMPTY_FRAGMENT } from '../constants.js';
3
3
  import { append } from '../utilities/fragment.js';
4
4
  import { cloneNode, firstChild, lastChild } from '../utilities/node.js';
5
5
  import { ondisconnect, remove } from './cleanup.js';
6
- class ReactiveArraySlot {
6
+ class ArraySlot {
7
7
  array;
8
8
  fragment;
9
9
  marker;
@@ -16,7 +16,7 @@ class ReactiveArraySlot {
16
16
  this.template = function (data, i) {
17
17
  let dispose, frag = root((d) => {
18
18
  dispose = d;
19
- return template.call(this, data, i);
19
+ return template(data, i);
20
20
  }), group = {
21
21
  head: firstChild.call(frag),
22
22
  tail: lastChild.call(frag)
@@ -66,58 +66,46 @@ class ReactiveArraySlot {
66
66
  return this.marker;
67
67
  }
68
68
  clear() {
69
- remove(this.nodes);
69
+ remove(...this.nodes.splice(0));
70
70
  }
71
71
  pop() {
72
72
  let group = this.nodes.pop();
73
73
  if (group) {
74
- remove([group]);
74
+ remove(group);
75
75
  }
76
76
  }
77
77
  push(items) {
78
- let anchor = this.anchor(), array = this.array, length = this.nodes.length;
79
- this.nodes.length = length + items.length;
80
- for (let i = 0, n = items.length; i < n; i++) {
81
- this.nodes[i + length] = this.template.call(array, items[i], i);
82
- }
78
+ let anchor = this.anchor();
79
+ this.nodes.push(...items.map(this.template));
83
80
  anchor.after(this.fragment);
84
81
  }
85
82
  render() {
86
- let nodes = this.nodes;
87
- if (nodes.length) {
88
- remove(nodes);
83
+ if (this.nodes.length) {
84
+ remove(...this.nodes.splice(0));
89
85
  }
90
- nodes = this.array.map(this.template);
86
+ this.nodes = this.array.map(this.template);
91
87
  this.marker.after(this.fragment);
92
88
  }
93
89
  shift() {
94
90
  let group = this.nodes.shift();
95
91
  if (group) {
96
- remove([group]);
92
+ remove(group);
97
93
  }
98
94
  }
99
95
  splice(start, stop = this.nodes.length, ...items) {
100
96
  if (!items.length) {
101
- return remove(this.nodes.splice(start, stop));
102
- }
103
- let array = this.array, n = items.length, nodes = new Array(n);
104
- for (let i = 0; i < n; i++) {
105
- nodes[i] = this.template.call(array, items[i], i);
97
+ return remove(...this.nodes.splice(start, stop));
106
98
  }
107
- remove(this.nodes.splice(start, stop, ...nodes));
99
+ remove(...this.nodes.splice(start, stop, ...items.map(this.template)));
108
100
  this.anchor(start - 1).after(this.fragment);
109
101
  }
110
102
  unshift(items) {
111
- let array = this.array, n = items.length, nodes = new Array(n);
112
- for (let i = 0; i < n; i++) {
113
- nodes[i] = this.template.call(array, items[i], i);
114
- }
115
- this.nodes.unshift(...nodes);
103
+ this.nodes.unshift(...items.map(this.template));
116
104
  this.marker.after(this.fragment);
117
105
  }
118
106
  }
119
107
  export default (anchor, renderable) => {
120
- let { array, template } = renderable, slot = new ReactiveArraySlot(anchor, array, template);
108
+ let { array, template } = renderable, slot = new ArraySlot(anchor, array, template);
121
109
  if (array.length) {
122
110
  root(() => {
123
111
  slot.nodes = array.map(slot.template);
@@ -1,4 +1,4 @@
1
1
  import { SlotGroup } from '../types.js';
2
2
  declare const ondisconnect: (element: Element, fn: VoidFunction) => void;
3
- declare const remove: (groups: SlotGroup[]) => void;
3
+ declare const remove: (...groups: SlotGroup[]) => void;
4
4
  export { ondisconnect, remove };
@@ -1,42 +1,17 @@
1
- import queue from '@esportsplus/queue';
2
1
  import { CLEANUP } from '../constants.js';
3
- import { microtask } from '../utilities/queue.js';
4
2
  import { previousSibling } from '../utilities/node.js';
5
- let cleanup = queue(64), scheduled = false;
6
- function schedule() {
7
- if (!cleanup.length || scheduled) {
8
- return;
9
- }
10
- scheduled = true;
11
- microtask.add(task);
12
- }
13
- function task() {
14
- try {
15
- let fns, fn, n = cleanup.length;
16
- while ((fns = cleanup.next()) && n--) {
17
- while (fn = fns.pop()) {
18
- fn();
19
- }
20
- }
21
- }
22
- catch { }
23
- if (cleanup.length) {
24
- microtask.add(task);
25
- }
26
- else {
27
- scheduled = false;
28
- }
29
- }
30
3
  const ondisconnect = (element, fn) => {
31
4
  (element[CLEANUP] ??= []).push(fn);
32
5
  };
33
- const remove = (groups) => {
34
- let group;
35
- while (group = groups.pop()) {
36
- let head = group.head, next, tail = group.tail || head;
6
+ const remove = (...groups) => {
7
+ for (let i = 0, n = groups.length; i < n; i++) {
8
+ let fns, fn, group = groups[i], head = group.head, next, tail = group.tail || head;
37
9
  while (tail) {
38
10
  if (CLEANUP in tail) {
39
- cleanup.add(tail[CLEANUP]);
11
+ fns = tail[CLEANUP];
12
+ while (fn = fns.pop()) {
13
+ fn();
14
+ }
40
15
  }
41
16
  next = previousSibling.call(tail);
42
17
  tail.remove();
@@ -46,8 +21,5 @@ const remove = (groups) => {
46
21
  tail = next;
47
22
  }
48
23
  }
49
- if (!scheduled && cleanup.length) {
50
- schedule();
51
- }
52
24
  };
53
25
  export { ondisconnect, remove };
@@ -5,67 +5,75 @@ import { raf } from '../utilities/queue.js';
5
5
  import { remove } from './cleanup.js';
6
6
  import text from '../utilities/text.js';
7
7
  import render from './render.js';
8
- function update(anchor, value) {
9
- if (this.group) {
10
- remove([this.group]);
11
- this.group = undefined;
12
- }
13
- if (value == null || value === false) {
14
- value = '';
15
- }
16
- let textnode = this.textnode;
17
- if (typeof value !== 'object') {
18
- if (textnode) {
19
- nodeValue.call(textnode, String(value));
20
- if (!textnode.isConnected) {
21
- anchor.after(textnode);
8
+ class EffectSlot {
9
+ anchor;
10
+ disposer;
11
+ group = null;
12
+ textnode = null;
13
+ constructor(anchor, fn) {
14
+ let dispose = fn.length ? () => this.dispose() : undefined, state = STATE_HYDRATING;
15
+ this.anchor = anchor;
16
+ this.disposer = effect(() => {
17
+ let value = fn(dispose);
18
+ if (state === STATE_HYDRATING) {
19
+ state = STATE_NONE;
20
+ this.update(value);
22
21
  }
23
- }
24
- else {
25
- anchor.after(this.textnode = text(String(value)));
26
- }
27
- }
28
- else {
29
- let fragment = render(anchor, value), head = firstChild.call(fragment);
30
- if (textnode && textnode.isConnected) {
31
- remove([{ head: textnode, tail: textnode }]);
32
- }
33
- if (head) {
34
- this.group = {
35
- head,
36
- tail: lastChild.call(fragment)
37
- };
38
- anchor.after(fragment);
39
- }
22
+ else {
23
+ raf.add(() => {
24
+ this.update(value);
25
+ });
26
+ }
27
+ });
40
28
  }
41
- }
42
- export default (anchor, fn) => {
43
- let context = {
44
- group: undefined,
45
- textnode: undefined
46
- }, dispose = fn.length ? () => {
47
- let { group, textnode } = context;
29
+ dispose() {
30
+ let { anchor, group, textnode } = this;
48
31
  if (textnode) {
49
32
  group = { head: anchor, tail: textnode };
50
33
  }
51
34
  else if (group) {
52
35
  group.head = anchor;
53
36
  }
54
- d();
37
+ this.disposer();
55
38
  if (group) {
56
- remove([group]);
39
+ remove(group);
57
40
  }
58
- } : undefined, state = STATE_HYDRATING;
59
- let d = effect(() => {
60
- let value = fn(dispose);
61
- if (state === STATE_HYDRATING) {
62
- update.call(context, anchor, value);
63
- state = STATE_NONE;
41
+ }
42
+ update(value) {
43
+ if (this.group) {
44
+ remove(this.group);
45
+ this.group = null;
46
+ }
47
+ if (value == null || value === false) {
48
+ value = '';
64
49
  }
65
- else if (state === STATE_NONE) {
66
- raf.add(() => {
67
- update.call(context, anchor, value);
68
- });
50
+ let { anchor, textnode } = this;
51
+ if (typeof value !== 'object') {
52
+ if (textnode) {
53
+ nodeValue.call(textnode, String(value));
54
+ if (!textnode.isConnected) {
55
+ anchor.after(textnode);
56
+ }
57
+ }
58
+ else {
59
+ anchor.after(this.textnode = text(String(value)));
60
+ }
69
61
  }
70
- });
62
+ else {
63
+ let fragment = render(anchor, value), head = firstChild.call(fragment);
64
+ if (textnode?.isConnected) {
65
+ remove({ head: textnode, tail: textnode });
66
+ }
67
+ if (head) {
68
+ this.group = {
69
+ head,
70
+ tail: lastChild.call(fragment)
71
+ };
72
+ anchor.after(fragment);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ export default (anchor, fn) => {
78
+ new EffectSlot(anchor, fn);
71
79
  };
@@ -3,7 +3,7 @@ import { EMPTY_FRAGMENT, RENDERABLE } from '../constants.js';
3
3
  import { cloneNode, lastChild } from '../utilities/node.js';
4
4
  import { append } from '../utilities/fragment.js';
5
5
  import text from '../utilities/text.js';
6
- import reactive from './reactive.js';
6
+ import array from './array.js';
7
7
  export default function render(anchor, input) {
8
8
  if (input == null || input === false || input === '') {
9
9
  return EMPTY_FRAGMENT;
@@ -11,12 +11,12 @@ export default function render(anchor, input) {
11
11
  if (typeof input !== 'object') {
12
12
  return text(input);
13
13
  }
14
+ if (RENDERABLE in input) {
15
+ return array(anchor, input);
16
+ }
14
17
  if ('nodeType' in input) {
15
18
  return input;
16
19
  }
17
- if (RENDERABLE in input) {
18
- return reactive(anchor, input);
19
- }
20
20
  if (isArray(input)) {
21
21
  let fragment = cloneNode.call(EMPTY_FRAGMENT);
22
22
  for (let i = 0, n = input.length; i < n; i++) {
package/build/types.d.ts CHANGED
@@ -6,14 +6,14 @@ import slot from './slot/index.js';
6
6
  import html from './html/index.js';
7
7
  type Attribute = Effect<Primitive | Primitive[]> | ((...args: any[]) => void) | Primitive;
8
8
  type Attributes<T extends HTMLElement = Element> = {
9
- [key: `aria-${string}`]: string | number | boolean | undefined;
10
- [key: `data-${string}`]: string | undefined;
11
9
  class?: Attribute | Attribute[];
12
10
  onconnect?: (element: T) => void;
13
11
  ondisconnect?: (element: T) => void;
14
12
  onrender?: (element: T) => void;
15
13
  ontick?: (dispose: VoidFunction, element: T) => void;
16
14
  style?: Attribute | Attribute[];
15
+ [key: `aria-${string}`]: string | number | boolean | undefined;
16
+ [key: `data-${string}`]: string | undefined;
17
17
  } & {
18
18
  [K in keyof GlobalEventHandlersEventMap as `on${string & K}`]?: (this: T, event: GlobalEventHandlersEventMap[K]) => void;
19
19
  } & Record<PropertyKey, unknown>;
@@ -24,7 +24,7 @@ type Renderable<T> = DocumentFragment | Effect<T> | Node | NodeList | Primitive
24
24
  type RenderableReactive<T> = Readonly<{
25
25
  [RENDERABLE]: typeof RENDERABLE_HTML_REACTIVE_ARRAY;
26
26
  array: ReactiveArray<T>;
27
- template: (this: ReactiveArray<T>, value: T, i: number) => ReturnType<typeof html>;
27
+ template: (value: T, i: number) => ReturnType<typeof html>;
28
28
  }>;
29
29
  type SlotGroup = {
30
30
  head: Element;
@@ -1,11 +1,10 @@
1
1
  import { innerHTML } from './element.js';
2
2
  import { cloneNode } from './node.js';
3
- let scratchpad = document.createElement('template');
3
+ let template = document.createElement('template');
4
4
  const append = DocumentFragment.prototype.append;
5
5
  const fragment = (html) => {
6
- innerHTML.call(scratchpad, html);
7
- let content = scratchpad.content;
8
- scratchpad = cloneNode.call(scratchpad);
9
- return content;
6
+ let element = cloneNode.call(template);
7
+ innerHTML.call(element, html);
8
+ return element.content;
10
9
  };
11
10
  export { append, fragment };
@@ -1,3 +1,2 @@
1
- declare const microtask: import("@esportsplus/tasks/build/factory").Scheduler;
2
1
  declare const raf: import("@esportsplus/tasks/build/factory").Scheduler;
3
- export { microtask, raf };
2
+ export { raf };
@@ -1,4 +1,3 @@
1
- import { micro as m, raf as r } from '@esportsplus/tasks';
2
- const microtask = m();
1
+ import { raf as r } from '@esportsplus/tasks';
3
2
  const raf = r();
4
- export { microtask, raf };
3
+ export { raf };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
4
  "@esportsplus/queue": "^0.1.0",
5
- "@esportsplus/reactivity": "^0.16.7",
5
+ "@esportsplus/reactivity": "^0.17.1",
6
6
  "@esportsplus/tasks": "^0.2.1",
7
7
  "@esportsplus/utilities": "^0.22.1"
8
8
  },
@@ -14,7 +14,7 @@
14
14
  "private": false,
15
15
  "type": "module",
16
16
  "types": "./build/index.d.ts",
17
- "version": "0.23.2",
17
+ "version": "0.23.4",
18
18
  "scripts": {
19
19
  "build": "tsc && tsc-alias",
20
20
  "-": "-"
package/src/attributes.ts CHANGED
@@ -61,36 +61,70 @@ function list(
61
61
  value = '';
62
62
  }
63
63
 
64
- if (id === null && (!value || typeof value !== 'string')) {
65
- return;
66
- }
64
+ let base = name + '.static',
65
+ delimiter = delimiters[name],
66
+ store = (ctx ??= context(element)).store ??= {},
67
+ dynamic = store[name] as Set<string> | undefined,
68
+ type = typeof value;
67
69
 
68
- let delimiter = delimiters[name],
69
- store = (ctx ??= context(element)).store ??= {};
70
+ if (dynamic === undefined) {
71
+ let value = (element.getAttribute(name) || '').trim();
70
72
 
71
- if (store[name] === undefined) {
72
- store[name] = (element.getAttribute(name) || '').trim();
73
+ store[base] = value;
74
+ store[name] = dynamic = new Set();
73
75
  }
74
76
 
75
- let current = value ? (delimiter + value) : '';
76
-
77
77
  if (id === null) {
78
- store[name] += current;
78
+ if (value && type === 'string') {
79
+ store[base] += (store[base] ? delimiter : '') + value;
80
+ }
79
81
  }
80
82
  else {
81
- let previous = store[id] as string | undefined;
83
+ let hot: Attributes = {};
82
84
 
83
- if (!previous) {
84
- store[name] += current;
85
+ if (value && type === 'string') {
86
+ let part: string,
87
+ parts = (value as string).split(delimiter);
88
+
89
+ for (let i = 0, n = parts.length; i < n; i++) {
90
+ part = parts[i].trim();
91
+
92
+ if (part === '') {
93
+ continue;
94
+ }
95
+
96
+ dynamic.add(part);
97
+ hot[part] = null;
98
+ }
85
99
  }
86
- else if (previous !== current) {
87
- store[name] = (store[name] as string).replace(previous, current);
100
+
101
+ let cold = store[id] as Attributes | undefined;
102
+
103
+ if (cold !== undefined) {
104
+ for (let part in cold) {
105
+ if (part in hot) {
106
+ continue;
107
+ }
108
+
109
+ dynamic.delete(part);
110
+ }
88
111
  }
89
112
 
90
- store[id] = current;
113
+ store[id] = hot;
91
114
  }
92
115
 
93
- schedule(ctx, element, name, state, store[name]);
116
+ value = store[base];
117
+
118
+ for (let key of dynamic) {
119
+ value += (value ? delimiter : '') + key;
120
+ }
121
+
122
+ if (state === STATE_HYDRATING) {
123
+ apply(element, name, value);
124
+ }
125
+ else {
126
+ schedule(ctx, element, name, state, value);
127
+ }
94
128
  }
95
129
 
96
130
  function property(
@@ -115,15 +149,15 @@ function property(
115
149
  ctx[name] = value as string;
116
150
  }
117
151
 
118
- schedule(ctx, element, name, state, value);
119
- }
120
-
121
- function schedule(ctx: Context | null, element: Element, name: string, state: State, value: unknown) {
122
152
  if (state === STATE_HYDRATING) {
123
153
  apply(element, name, value);
124
- return;
125
154
  }
155
+ else {
156
+ schedule(ctx, element, name, state, value);
157
+ }
158
+ }
126
159
 
160
+ function schedule(ctx: Context | null, element: Element, name: string, state: State, value: unknown) {
127
161
  ctx ??= context(element);
128
162
  (ctx.updates ??= {})[name] = value;
129
163
 
@@ -164,14 +198,14 @@ function task() {
164
198
  }
165
199
 
166
200
 
167
- const set = (element: Element, name: string, value: unknown, state: State = STATE_HYDRATING) => {
201
+ const set = (element: Element, name: string, value: unknown) => {
168
202
  let fn = name === 'class' || name === 'style' ? list : property,
203
+ state: State = STATE_HYDRATING,
169
204
  type = typeof value;
170
205
 
171
206
  if (type === 'function') {
172
207
  if (name.startsWith('on')) {
173
- event(element, name as `on${string}`, value as Function);
174
- return;
208
+ return event(element, name as `on${string}`, value as Function);
175
209
  }
176
210
 
177
211
  let ctx = context(element);
@@ -210,11 +244,9 @@ const set = (element: Element, name: string, value: unknown, state: State = STAT
210
244
  }
211
245
 
212
246
  if (type !== 'object') {
213
- fn(null, element, null, name, state, value);
214
- return;
247
+ // Skip isArray when possible
215
248
  }
216
-
217
- if (isArray(value)) {
249
+ else if (isArray(value)) {
218
250
  for (let i = 0, n = value.length; i < n; i++) {
219
251
  let v = value[i];
220
252
 
@@ -222,7 +254,7 @@ const set = (element: Element, name: string, value: unknown, state: State = STAT
222
254
  continue;
223
255
  }
224
256
 
225
- set(element, name, v, state);
257
+ set(element, name, v);
226
258
  }
227
259
  return;
228
260
  }
@@ -232,7 +264,10 @@ const set = (element: Element, name: string, value: unknown, state: State = STAT
232
264
 
233
265
  const spread = function (element: Element, value: Attributes | Attributes[]) {
234
266
  if (isObject(value)) {
235
- for (let name in value) {
267
+ let names = Object.keys(value),
268
+ name;
269
+
270
+ while (name = names.pop()) {
236
271
  let v = value[name];
237
272
 
238
273
  if (v == null || v === false || v === '') {
@@ -98,6 +98,10 @@ export default (element: Element, event: `on${string}`, listener: Function): voi
98
98
  ondisconnect(element, () => listener(element));
99
99
  return;
100
100
 
101
+ case 'onrender':
102
+ root(() => listener(element));
103
+ return;
104
+
101
105
  case 'onresize':
102
106
  onresize(element, listener);
103
107
  return;
@@ -106,10 +110,6 @@ export default (element: Element, event: `on${string}`, listener: Function): voi
106
110
  ontick(element, listener);
107
111
  return;
108
112
 
109
- case 'onrender':
110
- root(() => listener(element));
111
- return;
112
-
113
113
  default:
114
114
  element[ keys[event] || register(element, event) ] = listener;
115
115
  return;
@@ -6,15 +6,12 @@ import { cloneNode, firstChild, lastChild } from '~/utilities/node';
6
6
  import { ondisconnect, remove } from './cleanup';
7
7
 
8
8
 
9
- class ReactiveArraySlot<T> {
9
+ class ArraySlot<T> {
10
10
  array: ReactiveArray<T>;
11
11
  fragment: Node;
12
12
  marker: Element;
13
13
  nodes: SlotGroup[] = [];
14
- template: (
15
- this: ReactiveArray<T>,
16
- ...args: Parameters< Parameters<ReactiveArray<T>['map']>[0] >
17
- ) => SlotGroup;
14
+ template: (...args: Parameters< RenderableReactive<T>['template'] >) => SlotGroup;
18
15
 
19
16
 
20
17
  constructor(anchor: Element, array: ReactiveArray<T>, template: RenderableReactive<T>['template']) {
@@ -26,7 +23,7 @@ class ReactiveArraySlot<T> {
26
23
  let dispose: VoidFunction,
27
24
  frag = root((d) => {
28
25
  dispose = d;
29
- return template.call(this, data, i);
26
+ return template(data, i);
30
27
  }),
31
28
  group = {
32
29
  head: firstChild.call(frag),
@@ -88,38 +85,31 @@ class ReactiveArraySlot<T> {
88
85
  }
89
86
 
90
87
  clear() {
91
- remove(this.nodes);
88
+ remove(...this.nodes.splice(0));
92
89
  }
93
90
 
94
91
  pop() {
95
92
  let group = this.nodes.pop();
96
93
 
97
94
  if (group) {
98
- remove([group]);
95
+ remove(group);
99
96
  }
100
97
  }
101
98
 
102
99
  push(items: T[]) {
103
- let anchor = this.anchor(),
104
- array = this.array,
105
- length = this.nodes.length;
100
+ let anchor = this.anchor();
106
101
 
107
- this.nodes.length = length + items.length;
102
+ this.nodes.push( ...items.map(this.template) );
108
103
 
109
- for (let i = 0, n = items.length; i < n; i++) {
110
- this.nodes[i + length] = this.template.call(array, items[i], i);
111
- }
112
104
  anchor.after(this.fragment);
113
105
  }
114
106
 
115
107
  render() {
116
- let nodes = this.nodes;
117
-
118
- if (nodes.length) {
119
- remove(nodes);
108
+ if (this.nodes.length) {
109
+ remove(...this.nodes.splice(0));
120
110
  }
121
111
 
122
- nodes = this.array.map(this.template);
112
+ this.nodes = this.array.map(this.template);
123
113
  this.marker.after(this.fragment);
124
114
  }
125
115
 
@@ -127,37 +117,21 @@ class ReactiveArraySlot<T> {
127
117
  let group = this.nodes.shift();
128
118
 
129
119
  if (group) {
130
- remove([group]);
120
+ remove(group);
131
121
  }
132
122
  }
133
123
 
134
124
  splice(start: number, stop: number = this.nodes.length, ...items: T[]) {
135
125
  if (!items.length) {
136
- return remove(this.nodes.splice(start, stop));
137
- }
138
-
139
- let array = this.array,
140
- n = items.length,
141
- nodes = new Array(n);
142
-
143
- for (let i = 0; i < n; i++) {
144
- nodes[i] = this.template.call(array, items[i], i);
126
+ return remove(...this.nodes.splice(start, stop));
145
127
  }
146
128
 
147
- remove(this.nodes.splice(start, stop, ...nodes));
129
+ remove( ...this.nodes.splice(start, stop, ...items.map(this.template)) );
148
130
  this.anchor(start - 1).after(this.fragment);
149
131
  }
150
132
 
151
133
  unshift(items: T[]) {
152
- let array = this.array,
153
- n = items.length,
154
- nodes = new Array(n);
155
-
156
- for (let i = 0; i < n; i++) {
157
- nodes[i] = this.template.call(array, items[i], i);
158
- }
159
-
160
- this.nodes.unshift(...nodes);
134
+ this.nodes.unshift(...items.map(this.template));
161
135
  this.marker.after(this.fragment);
162
136
  }
163
137
  }
@@ -165,7 +139,7 @@ class ReactiveArraySlot<T> {
165
139
 
166
140
  export default <T>(anchor: Element, renderable: RenderableReactive<T>) => {
167
141
  let { array, template } = renderable,
168
- slot = new ReactiveArraySlot(anchor, array, template);
142
+ slot = new ArraySlot(anchor, array, template);
169
143
 
170
144
  if (array.length) {
171
145
  root(() => {
@@ -1,60 +1,27 @@
1
- import queue from '@esportsplus/queue';
2
1
  import { CLEANUP } from '~/constants';
3
- import { microtask } from '~/utilities/queue';
4
2
  import { previousSibling } from '~/utilities/node';
5
3
  import { SlotGroup } from '~/types';
6
4
 
7
5
 
8
- let cleanup = queue<VoidFunction[]>(64),
9
- scheduled = false;
10
-
11
-
12
- function schedule() {
13
- if (!cleanup.length || scheduled) {
14
- return;
15
- }
16
-
17
- scheduled = true;
18
- microtask.add(task);
19
- }
20
-
21
- function task() {
22
- try {
23
- let fns, 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
6
  const ondisconnect = (element: Element, fn: VoidFunction) => {
44
7
  ((element as any)[CLEANUP] ??= []).push(fn);
45
8
  };
46
9
 
47
- const remove = (groups: SlotGroup[]) => {
48
- let group;
49
-
50
- while (group = groups.pop()) {
51
- let head = group.head,
10
+ const remove = (...groups: SlotGroup[]) => {
11
+ for (let i = 0, n = groups.length; i < n; i++) {
12
+ let fns, fn,
13
+ group = groups[i],
14
+ head = group.head,
52
15
  next,
53
16
  tail = group.tail || head;
54
17
 
55
18
  while (tail) {
56
19
  if (CLEANUP in tail) {
57
- cleanup.add( tail[CLEANUP] as VoidFunction[] );
20
+ fns = tail[CLEANUP] as VoidFunction[];
21
+
22
+ while (fn = fns.pop()) {
23
+ fn();
24
+ }
58
25
  }
59
26
 
60
27
  next = previousSibling.call(tail);
@@ -67,10 +34,6 @@ const remove = (groups: SlotGroup[]) => {
67
34
  tail = next;
68
35
  }
69
36
  }
70
-
71
- if (!scheduled && cleanup.length) {
72
- schedule();
73
- }
74
37
  };
75
38
 
76
39
 
@@ -8,84 +8,96 @@ import text from '~/utilities/text';
8
8
  import render from './render';
9
9
 
10
10
 
11
- function update(this: { group?: SlotGroup, textnode?: Node }, anchor: Element, value: unknown) {
12
- if (this.group) {
13
- remove([this.group]);
14
- this.group = undefined;
15
- }
11
+ class EffectSlot {
12
+ anchor: Element;
13
+ disposer: VoidFunction;
14
+ group: SlotGroup | null = null;
15
+ textnode: Node | null = null;
16
16
 
17
- if (value == null || value === false) {
18
- value = '';
19
- }
20
17
 
21
- let textnode = this.textnode;
18
+ constructor(anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) {
19
+ let dispose = fn.length ? () => this.dispose() : undefined,
20
+ state = STATE_HYDRATING;
22
21
 
23
- if (typeof value !== 'object') {
24
- if (textnode) {
25
- nodeValue.call(textnode, String(value));
22
+ this.anchor = anchor;
23
+ this.disposer = effect(() => {
24
+ let value = fn(dispose);
26
25
 
27
- if (!textnode.isConnected) {
28
- anchor.after(textnode);
26
+ if (state === STATE_HYDRATING) {
27
+ state = STATE_NONE;
28
+ this.update(value);
29
29
  }
30
- }
31
- else {
32
- anchor.after( this.textnode = text( String(value) ) );
33
- }
30
+ else {
31
+ raf.add(() => {
32
+ this.update(value);
33
+ });
34
+ }
35
+ });
34
36
  }
35
- else {
36
- let fragment = render(anchor, value),
37
- head = firstChild.call(fragment);
38
37
 
39
- if (textnode && textnode.isConnected) {
40
- remove([{ head: textnode as Element, tail: textnode as Element }]);
38
+
39
+ dispose() {
40
+ let { anchor, group, textnode } = this;
41
+
42
+ if (textnode) {
43
+ group = { head: anchor, tail: textnode as Element };
44
+ }
45
+ else if (group) {
46
+ group.head = anchor;
41
47
  }
42
48
 
43
- if (head) {
44
- this.group = {
45
- head,
46
- tail: lastChild.call(fragment)
47
- };
49
+ this.disposer();
48
50
 
49
- anchor.after(fragment);
51
+ if (group) {
52
+ remove(group);
50
53
  }
51
54
  }
52
- }
53
55
 
56
+ update(value: unknown) {
57
+ if (this.group) {
58
+ remove(this.group);
59
+ this.group = null;
60
+ }
54
61
 
55
- export default (anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) => {
56
- let context = {
57
- group: undefined as SlotGroup | undefined,
58
- textnode: undefined as Node | undefined
59
- },
60
- dispose = fn.length ? () => {
61
- let { group, textnode } = context;
62
+ if (value == null || value === false) {
63
+ value = '';
64
+ }
65
+
66
+ let { anchor, textnode } = this;
62
67
 
68
+ if (typeof value !== 'object') {
63
69
  if (textnode) {
64
- group = { head: anchor, tail: textnode as Element };
70
+ nodeValue.call(textnode, String(value));
71
+
72
+ if (!textnode.isConnected) {
73
+ anchor.after(textnode);
74
+ }
65
75
  }
66
- else if (group) {
67
- group.head = anchor;
76
+ else {
77
+ anchor.after( this.textnode = text( String(value) ) );
68
78
  }
79
+ }
80
+ else {
81
+ let fragment = render(anchor, value),
82
+ head = firstChild.call(fragment);
69
83
 
70
- d();
71
-
72
- if (group) {
73
- remove([group]);
84
+ if (textnode?.isConnected) {
85
+ remove({ head: textnode as Element, tail: textnode as Element });
74
86
  }
75
- } : undefined,
76
- state = STATE_HYDRATING;
77
87
 
78
- let d = effect(() => {
79
- let value = fn(dispose);
88
+ if (head) {
89
+ this.group = {
90
+ head,
91
+ tail: lastChild.call(fragment)
92
+ };
80
93
 
81
- if (state === STATE_HYDRATING) {
82
- update.call(context, anchor, value);
83
- state = STATE_NONE;
94
+ anchor.after(fragment);
84
95
  }
85
- else if (state === STATE_NONE) {
86
- raf.add(() => {
87
- update.call(context, anchor, value);
88
- });
89
- }
90
- });
96
+ }
97
+ }
98
+ }
99
+
100
+
101
+ export default (anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) => {
102
+ new EffectSlot(anchor, fn);
91
103
  };
@@ -4,7 +4,7 @@ import { Element, RenderableReactive } from '~/types';
4
4
  import { cloneNode, lastChild } from '~/utilities/node';
5
5
  import { append } from '~/utilities/fragment';
6
6
  import text from '~/utilities/text';
7
- import reactive from './reactive';
7
+ import array from './array';
8
8
 
9
9
 
10
10
  export default function render(anchor: Element, input: unknown): Node {
@@ -16,12 +16,12 @@ export default function render(anchor: Element, input: unknown): Node {
16
16
  return text(input as any);
17
17
  }
18
18
 
19
- if ('nodeType' in input) {
20
- return input as Node;
19
+ if (RENDERABLE in input) {
20
+ return array(anchor, input as RenderableReactive<unknown>);
21
21
  }
22
22
 
23
- if (RENDERABLE in input) {
24
- return reactive(anchor, input as RenderableReactive<unknown>);
23
+ if ('nodeType' in input) {
24
+ return input as Node;
25
25
  }
26
26
 
27
27
  if (isArray(input)) {
package/src/types.ts CHANGED
@@ -9,14 +9,14 @@ import html from './html';
9
9
  type Attribute = Effect<Primitive | Primitive[]> | ((...args: any[]) => void) | Primitive;
10
10
 
11
11
  type Attributes<T extends HTMLElement = Element> = {
12
- [key: `aria-${string}`]: string | number | boolean | undefined;
13
- [key: `data-${string}`]: string | undefined;
14
12
  class?: Attribute | Attribute[];
15
13
  onconnect?: (element: T) => void;
16
14
  ondisconnect?: (element: T) => void;
17
15
  onrender?: (element: T) => void;
18
16
  ontick?: (dispose: VoidFunction, element: T) => void;
19
17
  style?: Attribute | Attribute[];
18
+ [key: `aria-${string}`]: string | number | boolean | undefined;
19
+ [key: `data-${string}`]: string | undefined;
20
20
  } & {
21
21
  [K in keyof GlobalEventHandlersEventMap as `on${string & K}`]?: (this: T, event: GlobalEventHandlersEventMap[K]) => void;
22
22
  } & Record<PropertyKey, unknown>;
@@ -34,11 +34,7 @@ type Renderable<T> = DocumentFragment | Effect<T> | Node | NodeList | Primitive
34
34
  type RenderableReactive<T> = Readonly<{
35
35
  [RENDERABLE]: typeof RENDERABLE_HTML_REACTIVE_ARRAY;
36
36
  array: ReactiveArray<T>;
37
- template: (
38
- this: ReactiveArray<T>,
39
- value: T,
40
- i: number
41
- ) => ReturnType<typeof html>;
37
+ template: (value: T, i: number) => ReturnType<typeof html>;
42
38
  }>;
43
39
 
44
40
  type SlotGroup = {
@@ -2,19 +2,17 @@ import { innerHTML } from './element';
2
2
  import { cloneNode } from './node';
3
3
 
4
4
 
5
- let scratchpad = document.createElement('template');
5
+ let template = document.createElement('template');
6
6
 
7
7
 
8
8
  const append = DocumentFragment.prototype.append;
9
9
 
10
10
  const fragment = (html: string): DocumentFragment => {
11
- innerHTML.call(scratchpad, html);
11
+ let element = cloneNode.call(template) as HTMLTemplateElement;
12
12
 
13
- let content = scratchpad.content;
13
+ innerHTML.call(element, html);
14
14
 
15
- scratchpad = cloneNode.call(scratchpad) as HTMLTemplateElement;
16
-
17
- return content;
15
+ return element.content;
18
16
  };
19
17
 
20
18
 
@@ -1,9 +1,7 @@
1
- import { micro as m, raf as r } from '@esportsplus/tasks';
1
+ import { raf as r } from '@esportsplus/tasks';
2
2
 
3
3
 
4
- const microtask = m();
5
-
6
4
  const raf = r();
7
5
 
8
6
 
9
- export { microtask, raf };
7
+ export { raf };
File without changes