@oscarpalmer/abydon 0.8.0 → 0.10.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.
@@ -1,5 +1,6 @@
1
- import {isReactive} from '@oscarpalmer/sentinel';
2
- import type {FragmentData, FragmentElement} from '../../models';
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {computed, isReactive} from '@oscarpalmer/sentinel';
3
+ import type {FragmentData, ProperElement} from '../../models';
3
4
  import {mapEvent} from '../event';
4
5
  import {setAttribute} from './value';
5
6
 
@@ -11,17 +12,9 @@ function getValue(data: FragmentData, original: string): unknown {
11
12
  return matches == null ? original : data.values[+matches[1]];
12
13
  }
13
14
 
14
- export function isBadAttribute(name: string, value: string): boolean {
15
- return (
16
- /^on/i.test(name) ||
17
- (/^(href|src|xlink:href)$/i.test(name) &&
18
- /(data:text\/html|javascript:)/i.test(value))
19
- );
20
- }
21
-
22
15
  export function mapAttributes(
23
16
  data: FragmentData,
24
- element: FragmentElement,
17
+ element: ProperElement,
25
18
  ): void {
26
19
  const attributes = [...element.attributes];
27
20
  const {length} = attributes;
@@ -29,31 +22,25 @@ export function mapAttributes(
29
22
  for (let index = 0; index < length; index += 1) {
30
23
  const {name, value} = attributes[index];
31
24
 
32
- if (isBadAttribute(name, value)) {
33
- element.removeAttribute(name);
34
-
35
- continue;
36
- }
37
-
38
25
  const actual = getValue(data, value);
39
26
 
40
27
  if (name.startsWith('@')) {
41
28
  mapEvent(element, name, actual);
42
- } else if (name.includes('.') || isReactive(actual)) {
29
+ } else if (
30
+ name.includes('.') ||
31
+ typeof value === 'function' ||
32
+ isReactive(actual)
33
+ ) {
43
34
  mapValue(element, name, actual);
44
35
  }
45
36
  }
46
37
  }
47
38
 
48
- function mapValue(
49
- element: FragmentElement,
50
- name: string,
51
- value: unknown,
52
- ): void {
39
+ function mapValue(element: ProperElement, name: string, value: unknown): void {
53
40
  switch (true) {
54
41
  case typeof value === 'function':
55
- mapValue(element, name, value());
56
- return;
42
+ setAttribute(element, name, computed(value as GenericCallback));
43
+ break;
57
44
 
58
45
  default:
59
46
  setAttribute(element, name, value);
@@ -1,37 +1,29 @@
1
- import {closest} from '@oscarpalmer/atoms/element';
2
1
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
2
  import {getString} from '@oscarpalmer/atoms/string';
4
3
  import {effect, isReactive} from '@oscarpalmer/sentinel';
5
- import type {FragmentElement} from '../../models';
6
-
7
- const booleanAttributes = new Set([
8
- 'checked',
9
- 'disabled',
10
- 'hidden',
11
- 'inert',
12
- 'multiple',
13
- 'open',
14
- 'readOnly',
15
- 'required',
16
- 'selected',
17
- ]);
18
-
19
- const classPattern = /^class\./;
20
- const stylePattern = /^style\./;
4
+ import {
5
+ isBooleanAttribute,
6
+ setAttribute as setAttr,
7
+ } from '@oscarpalmer/toretto/attribute';
8
+ import type {ProperElement} from '../../models';
9
+
10
+ const classExpression = /^class\./;
11
+ const styleFullExpression = /^style\.([\w-]+)(?:\.([\w-]+))?$/;
12
+ const stylePrefixExpression = /^style\./;
21
13
 
22
14
  export function setAttribute(
23
- element: FragmentElement,
15
+ element: ProperElement,
24
16
  name: string,
25
17
  value: unknown,
26
18
  ): void {
27
19
  element.removeAttribute(name);
28
20
 
29
21
  switch (true) {
30
- case classPattern.test(name):
22
+ case classExpression.test(name):
31
23
  setClasses(element, name, value);
32
24
  return;
33
25
 
34
- case stylePattern.test(name):
26
+ case stylePrefixExpression.test(name):
35
27
  setStyle(element, name, value);
36
28
  return;
37
29
 
@@ -42,12 +34,12 @@ export function setAttribute(
42
34
  }
43
35
 
44
36
  function setClasses(
45
- element: FragmentElement,
37
+ element: ProperElement,
46
38
  name: string,
47
39
  value: unknown,
48
40
  ): void {
49
41
  function update(value: unknown): void {
50
- if (/^(|true)$/.test(getString(value))) {
42
+ if (value === true) {
51
43
  element.classList.add(...classes);
52
44
  } else {
53
45
  element.classList.remove(...classes);
@@ -65,11 +57,7 @@ function setClasses(
65
57
  }
66
58
  }
67
59
 
68
- function setStyle(
69
- element: FragmentElement,
70
- name: string,
71
- value: unknown,
72
- ): void {
60
+ function setStyle(element: ProperElement, name: string, value: unknown): void {
73
61
  function update(value: unknown): void {
74
62
  if (value == null || value === false || (value === true && unit == null)) {
75
63
  element.style.removeProperty(property);
@@ -81,32 +69,26 @@ function setStyle(
81
69
  }
82
70
  }
83
71
 
84
- const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
72
+ const [, property, unit] = styleFullExpression.exec(name) ?? [];
85
73
 
86
- if (property == null) {
87
- return;
88
- }
89
-
90
- if (isReactive(value)) {
91
- effect(() => {
92
- update(value.get());
93
- });
94
- } else {
95
- update(value);
74
+ if (property != null) {
75
+ if (isReactive(value)) {
76
+ effect(() => {
77
+ update(value.get());
78
+ });
79
+ } else {
80
+ update(value);
81
+ }
96
82
  }
97
83
  }
98
84
 
99
- function setValue(
100
- element: FragmentElement,
101
- name: string,
102
- value: unknown,
103
- ): void {
85
+ function setValue(element: ProperElement, name: string, value: unknown): void {
104
86
  const callback =
105
- booleanAttributes.has(name) && name in element
87
+ isBooleanAttribute(name) && name in element
106
88
  ? name === 'selected'
107
- ? updateSelected(element)
89
+ ? updateSelected
108
90
  : updateProperty
109
- : updateAttribute;
91
+ : setAttr;
110
92
 
111
93
  if (isReactive(value)) {
112
94
  effect(() => {
@@ -123,37 +105,25 @@ function triggerSelectChange(select: HTMLSelectElement): void {
123
105
  }
124
106
  }
125
107
 
126
- function updateAttribute(
127
- element: FragmentElement,
108
+ function updateProperty(
109
+ element: ProperElement,
128
110
  name: string,
129
111
  value: unknown,
130
112
  ): void {
131
- if (value == null) {
132
- element.removeAttribute(name);
133
- } else {
134
- element.setAttribute(name, getString(value));
135
- }
113
+ (element as unknown as PlainObject)[name] = value === true;
136
114
  }
137
115
 
138
- function updateProperty(
139
- element: FragmentElement,
116
+ function updateSelected(
117
+ element: ProperElement,
140
118
  name: string,
141
119
  value: unknown,
142
120
  ): void {
143
- (element as unknown as PlainObject)[name] = /^(|true)$/.test(
144
- getString(value),
145
- );
146
- }
147
-
148
- function updateSelected(element: FragmentElement) {
149
- const select = closest(element, 'select')[0] as HTMLSelectElement;
121
+ const select = element.closest('select') as HTMLSelectElement;
150
122
  const options = [...(select?.options ?? [])];
151
123
 
152
- return (element: FragmentElement, name: string, value: unknown): void => {
153
- if (select != null && options.includes(element as HTMLOptionElement)) {
154
- triggerSelectChange(select);
155
- }
124
+ if (select != null && options.includes(element as HTMLOptionElement)) {
125
+ triggerSelectChange(select);
126
+ }
156
127
 
157
- updateProperty(element, name, value);
158
- };
128
+ updateProperty(element, name, value);
159
129
  }
package/src/node/event.ts CHANGED
@@ -1,4 +1,20 @@
1
- import type {FragmentElement} from '../models';
1
+ import type {ProperElement} from '../models';
2
+
3
+ const controllers = new WeakMap<ProperElement, AbortController>();
4
+
5
+ const eventNamePattern = /^@([\w-]+)(?::([a-z:]+))?$/i;
6
+
7
+ function getController(element: ProperElement): AbortController {
8
+ let controller = controllers.get(element);
9
+
10
+ if (controller == null) {
11
+ controller = new AbortController();
12
+
13
+ controllers.set(element, controller);
14
+ }
15
+
16
+ return controller;
17
+ }
2
18
 
3
19
  function getOptions(options: string): AddEventListenerOptions {
4
20
  const parts = options.split(':');
@@ -11,19 +27,25 @@ function getOptions(options: string): AddEventListenerOptions {
11
27
  }
12
28
 
13
29
  export function mapEvent(
14
- element: FragmentElement,
30
+ element: ProperElement,
15
31
  name: string,
16
32
  value: unknown,
17
33
  ): void {
18
34
  element.removeAttribute(name);
19
35
 
20
- const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
36
+ const [, type, options] = eventNamePattern.exec(name) ?? [];
21
37
 
22
38
  if (typeof value === 'function' && type != null) {
23
- element.addEventListener(
24
- type,
25
- value as EventListener,
26
- getOptions(options ?? ''),
27
- );
39
+ element.addEventListener(type, value as EventListener, {
40
+ ...getOptions(options ?? ''),
41
+ signal: getController(element).signal,
42
+ });
43
+ }
44
+ }
45
+
46
+ export function removeEvents(element: ProperElement): void {
47
+ if (controllers.has(element)) {
48
+ controllers.get(element)?.abort();
49
+ controllers.delete(element);
28
50
  }
29
51
  }
package/src/node/index.ts CHANGED
@@ -1,7 +1,8 @@
1
- import {isReactive} from '@oscarpalmer/sentinel';
2
- import {isFragmentElement} from '../helpers';
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {computed, isReactive} from '@oscarpalmer/sentinel';
3
+ import {isFragment, isProperElement} from '../helpers';
3
4
  import {createNodes} from '../helpers/dom';
4
- import type {FragmentData} from '../models';
5
+ import type {FragmentData, ProperNode} from '../models';
5
6
  import {mapAttributes} from './attribute';
6
7
  import {setReactiveNode} from './value';
7
8
 
@@ -12,11 +13,11 @@ function mapNode(data: FragmentData, comment: Comment): void {
12
13
  const value = matches == null ? null : data.values[+matches[1]];
13
14
 
14
15
  if (value != null) {
15
- mapValue(comment, value);
16
+ mapValue(data, comment, value);
16
17
  }
17
18
  }
18
19
 
19
- export function mapNodes(data: FragmentData, nodes: Node[]): void {
20
+ export function mapNodes(data: FragmentData, nodes: ProperNode[]): void {
20
21
  const {length} = nodes;
21
22
 
22
23
  for (let index = 0; index < length; index += 1) {
@@ -28,28 +29,44 @@ export function mapNodes(data: FragmentData, nodes: Node[]): void {
28
29
  continue;
29
30
  }
30
31
 
31
- if (isFragmentElement(node)) {
32
+ if (isProperElement(node)) {
32
33
  mapAttributes(data, node);
33
34
  }
34
35
 
35
36
  if (node.hasChildNodes()) {
36
- mapNodes(data, [...node.childNodes]);
37
+ mapNodes(data, [...node.childNodes] as ProperNode[]);
37
38
  }
38
39
  }
39
40
  }
40
41
 
41
- function mapValue(comment: Comment, value: unknown): void {
42
+ function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
42
43
  switch (true) {
43
44
  case typeof value === 'function':
44
- mapValue(comment, value());
45
- return;
45
+ setReactiveNode(data, comment, computed(value as GenericCallback));
46
+ break;
46
47
 
47
48
  case isReactive(value):
48
- setReactiveNode(comment, value);
49
+ setReactiveNode(data, comment, value);
49
50
  break;
50
51
 
51
52
  default:
52
- comment.replaceWith(...createNodes(value));
53
+ replaceComment(data, comment, value);
53
54
  break;
54
55
  }
55
56
  }
57
+
58
+ function replaceComment(
59
+ data: FragmentData,
60
+ comment: Comment,
61
+ value: unknown,
62
+ ): void {
63
+ const item = data.items.find(item => item.nodes.includes(comment));
64
+ const nodes = createNodes(value);
65
+
66
+ if (item != null) {
67
+ item.fragment = isFragment(value) ? value : undefined;
68
+ item.nodes = nodes;
69
+ }
70
+
71
+ comment.replaceWith(...nodes);
72
+ }
package/src/node/value.ts CHANGED
@@ -1,48 +1,87 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
2
  import {getString} from '@oscarpalmer/atoms/string';
3
3
  import {type Reactive, effect} from '@oscarpalmer/sentinel';
4
- import {isFragment, isFragmentNode} from '../helpers';
4
+ import type {Fragment} from '../fragment';
5
+ import {isFragment, isProperNode} from '../helpers';
5
6
  import {createNodes, replaceNodes} from '../helpers/dom';
6
- import type {FragmentNode} from '../models';
7
+ import type {FragmentData, ProperNode} from '../models';
7
8
 
8
- export function setReactiveNode(comment: Comment, reactive: Reactive): void {
9
+ function setNodes(
10
+ nodes: ProperNode[] | undefined,
11
+ comment: Comment,
12
+ text: Text,
13
+ value: Fragment | ProperNode,
14
+ ): ProperNode[] {
15
+ const next = createNodes(value);
16
+
17
+ if (nodes == null) {
18
+ if (comment.parentNode != null) {
19
+ replaceNodes([comment], next);
20
+ } else if (text.parentNode != null) {
21
+ replaceNodes([text], next);
22
+ }
23
+ } else {
24
+ replaceNodes(nodes, next);
25
+ }
26
+
27
+ return next;
28
+ }
29
+
30
+ export function setReactiveNode(
31
+ data: FragmentData,
32
+ comment: Comment,
33
+ reactive: Reactive,
34
+ ): void {
35
+ const item = data.items.find(item => item.nodes.includes(comment));
9
36
  const text = new Text();
10
37
 
11
- let nodes: FragmentNode[] | undefined;
38
+ let nodes: ProperNode[] | undefined;
12
39
 
13
40
  effect(() => {
14
41
  const value = reactive.get();
15
42
 
16
- if (isFragment(value) || isFragmentNode(value)) {
17
- const next = createNodes(value);
18
-
19
- if (nodes == null) {
20
- if (comment.parentNode != null) {
21
- replaceNodes([comment], next);
22
- } else if (text.parentNode != null) {
23
- replaceNodes([text], next);
24
- }
25
- } else {
26
- replaceNodes(nodes, next);
27
- }
43
+ const valueIsFragment = isFragment(value);
28
44
 
29
- nodes = next;
45
+ if (valueIsFragment || isProperNode(value)) {
46
+ nodes = setNodes(nodes, comment, text, value);
47
+ } else {
48
+ nodes = setText(nodes, comment, text, value) ? [text] : undefined;
49
+ }
30
50
 
31
- return;
51
+ if (item != null) {
52
+ item.fragment = valueIsFragment ? value : undefined;
53
+ item.nodes = [...(nodes ?? [comment])];
32
54
  }
55
+ });
56
+ }
33
57
 
34
- const isNullable = isNullableOrWhitespace(value);
58
+ function setText(
59
+ nodes: ProperNode[] | undefined,
60
+ comment: Comment,
61
+ text: Text,
62
+ value: unknown,
63
+ ): boolean {
64
+ const isNullable = isNullableOrWhitespace(value);
35
65
 
36
- text.textContent = getString(value);
66
+ text.textContent = isNullable ? '' : getString(value);
37
67
 
38
- if (nodes != null) {
39
- replaceNodes(nodes, [isNullable ? comment : text]);
40
- } else if (isNullable && comment.parentNode == null) {
41
- text.replaceWith(comment);
42
- } else if (!isNullable && text.parentNode == null) {
43
- comment.replaceWith(text);
44
- }
68
+ if (nodes != null) {
69
+ replaceNodes(nodes, [isNullable ? comment : text]);
45
70
 
46
- nodes = undefined;
47
- });
71
+ return !isNullable;
72
+ }
73
+
74
+ if (isNullable && comment.parentNode == null) {
75
+ text.replaceWith(comment);
76
+
77
+ return false;
78
+ }
79
+
80
+ if (!isNullable && text.parentNode == null) {
81
+ comment.replaceWith(text);
82
+
83
+ return true;
84
+ }
85
+
86
+ return false;
48
87
  }
@@ -1,3 +1,4 @@
1
+ import type { ProperNode } from './models';
1
2
  export declare class Fragment {
2
3
  private readonly $fragment;
3
4
  private readonly data;
@@ -9,7 +10,7 @@ export declare class Fragment {
9
10
  /**
10
11
  * Gets a list of the fragment's nodes
11
12
  */
12
- get(): Node[];
13
+ get(): ProperNode[];
13
14
  /**
14
15
  * Removes the fragment from the DOM
15
16
  */
@@ -1,5 +1,4 @@
1
- import type { FragmentNode } from '../models';
2
- export declare function createNodes(value: unknown): FragmentNode[];
3
- export declare function createTemplate(html: string): Node;
4
- export declare function getNodes(node: Node): Node[];
5
- export declare function replaceNodes(from: FragmentNode[], to: FragmentNode[]): void;
1
+ import type { ProperNode } from '../models';
2
+ export declare function createNodes(value: unknown): ProperNode[];
3
+ export declare function removeNodes(nodes: ProperNode[]): void;
4
+ export declare function replaceNodes(from: ProperNode[], to: ProperNode[]): void;
@@ -1,5 +1,5 @@
1
1
  import type { Fragment } from '../fragment';
2
- import type { FragmentElement, FragmentNode } from '../models';
2
+ import type { ProperElement, ProperNode } from '../models';
3
3
  export declare function isFragment(value: unknown): value is Fragment;
4
- export declare function isFragmentElement(value: unknown): value is FragmentElement;
5
- export declare function isFragmentNode(value: unknown): value is FragmentNode;
4
+ export declare function isProperElement(value: unknown): value is ProperElement;
5
+ export declare function isProperNode(value: unknown): value is ProperNode;
package/types/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export type ProperNode = CharacterData | Element;
3
4
  export declare class Fragment {
4
5
  private readonly $fragment;
5
6
  private readonly data;
@@ -11,7 +12,7 @@ export declare class Fragment {
11
12
  /**
12
13
  * Gets a list of the fragment's nodes
13
14
  */
14
- get(): Node[];
15
+ get(): ProperNode[];
15
16
  /**
16
17
  * Removes the fragment from the DOM
17
18
  */
package/types/models.d.ts CHANGED
@@ -1,8 +1,13 @@
1
+ import type { Fragment } from './fragment';
1
2
  export type FragmentData = {
2
3
  expressions: unknown[];
3
- nodes: Node[];
4
+ items: FragmentItem[];
4
5
  strings: TemplateStringsArray;
5
6
  values: unknown[];
6
7
  };
7
- export type FragmentElement = HTMLElement | SVGElement;
8
- export type FragmentNode = Comment | Element | Text;
8
+ export type FragmentItem = {
9
+ fragment?: Fragment;
10
+ nodes: ProperNode[];
11
+ };
12
+ export type ProperElement = HTMLElement | SVGElement;
13
+ export type ProperNode = CharacterData | Element;
@@ -1,3 +1,2 @@
1
- import type { FragmentData, FragmentElement } from '../../models';
2
- export declare function isBadAttribute(name: string, value: string): boolean;
3
- export declare function mapAttributes(data: FragmentData, element: FragmentElement): void;
1
+ import type { FragmentData, ProperElement } from '../../models';
2
+ export declare function mapAttributes(data: FragmentData, element: ProperElement): void;
@@ -1,2 +1,2 @@
1
- import type { FragmentElement } from '../../models';
2
- export declare function setAttribute(element: FragmentElement, name: string, value: unknown): void;
1
+ import type { ProperElement } from '../../models';
2
+ export declare function setAttribute(element: ProperElement, name: string, value: unknown): void;
@@ -1,2 +1,3 @@
1
- import type { FragmentElement } from '../models';
2
- export declare function mapEvent(element: FragmentElement, name: string, value: unknown): void;
1
+ import type { ProperElement } from '../models';
2
+ export declare function mapEvent(element: ProperElement, name: string, value: unknown): void;
3
+ export declare function removeEvents(element: ProperElement): void;
@@ -1,2 +1,2 @@
1
- import type { FragmentData } from '../models';
2
- export declare function mapNodes(data: FragmentData, nodes: Node[]): void;
1
+ import type { FragmentData, ProperNode } from '../models';
2
+ export declare function mapNodes(data: FragmentData, nodes: ProperNode[]): void;
@@ -1,2 +1,3 @@
1
1
  import { type Reactive } from '@oscarpalmer/sentinel';
2
- export declare function setReactiveNode(comment: Comment, reactive: Reactive): void;
2
+ import type { FragmentData } from '../models';
3
+ export declare function setReactiveNode(data: FragmentData, comment: Comment, reactive: Reactive): void;