@oscarpalmer/abydon 0.21.0 → 0.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/abydon",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "keywords": [
5
5
  "components",
6
6
  "dom",
@@ -42,15 +42,16 @@
42
42
  "test:leak": "npx vp test run --detect-async-leaks --coverage"
43
43
  },
44
44
  "dependencies": {
45
- "@oscarpalmer/atoms": "^0.183",
46
- "@oscarpalmer/mora": "^0.28",
47
- "@oscarpalmer/toretto": "^0.42"
45
+ "@oscarpalmer/atoms": "^0.185",
46
+ "@oscarpalmer/mora": "^0.29",
47
+ "@oscarpalmer/toretto": "^0.43"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@oscarpalmer/oui": "^0.20",
51
+ "@oxlint/plugins": "^1.62",
51
52
  "@types/node": "^25.6",
52
53
  "@vitest/coverage-istanbul": "^4.1",
53
- "jsdom": "^29",
54
+ "jsdom": "^29.1",
54
55
  "tsdown": "^0.21",
55
56
  "typescript": "^5.9",
56
57
  "vite": "npm:@voidzero-dev/vite-plus-core@latest",
@@ -1,29 +1,31 @@
1
1
  import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
- import {parse} from '@oscarpalmer/atoms/string';
3
- import {computed, isReactive, isSignal} from '@oscarpalmer/mora';
2
+ import {computed, isSignal} from '@oscarpalmer/mora';
4
3
  import {
5
- EVENT_ON_PREFIXED,
4
+ EVENT_ON_VALUE,
6
5
  EXPRESSION_ABYDON_ATTRIBUTE_PREFIX,
7
6
  EXPRESSION_ABYDON_CONTENT,
8
7
  EXPRESSION_EVENT_PREFIX,
9
- EXPRESSION_PERIOD,
10
8
  INPUT_TYPE_CHECKBOX,
9
+ INPUT_TYPE_RADIO,
11
10
  PROPERTY_CHECKED,
12
11
  PROPERTY_VALUE,
13
- } from '../../constants';
14
- import {isInputElement} from '../../helpers/dom';
15
- import type {FragmentData} from '../../models';
16
- import {mapEvent} from '../event';
12
+ } from '../constants';
13
+ import {isInputElement} from '../helpers/dom';
14
+ import type {FragmentData} from '../models';
15
+ import {mapEvent} from '../node/event';
17
16
  import {setAttribute} from './value';
18
17
 
19
18
  function getValue(data: FragmentData, original: string): unknown {
20
- const matches = EXPRESSION_ABYDON_CONTENT.exec(original ?? '');
19
+ const matches = EXPRESSION_ABYDON_CONTENT.exec(original);
21
20
 
22
21
  return matches == null ? original : data.values[+matches[1]];
23
22
  }
24
23
 
25
24
  export function mapAttributes(data: FragmentData, element: HTMLElement | SVGElement): void {
26
- const attributes = [...element.attributes];
25
+ const attributes = [...element.attributes].sort((first, second) =>
26
+ first.name.localeCompare(second.name),
27
+ );
28
+
27
29
  const {length} = attributes;
28
30
 
29
31
  for (let index = 0; index < length; index += 1) {
@@ -41,13 +43,8 @@ export function mapAttributes(data: FragmentData, element: HTMLElement | SVGElem
41
43
  mapEvent(element, actualName, actualValue);
42
44
  break;
43
45
 
44
- case EXPRESSION_PERIOD.test(actualName):
45
- case typeof actualValue === 'function':
46
- case isReactive(actualValue):
47
- mapValue(data, element, actualName, actualValue);
48
- break;
49
-
50
46
  default:
47
+ mapValue(data, element, actualName, actualValue);
51
48
  break;
52
49
  }
53
50
  }
@@ -65,23 +62,9 @@ function mapValue(
65
62
  setAttribute(data, element, name, value);
66
63
  }
67
64
 
68
- if (!isInputElement(element) || !isSignal(value)) {
69
- return;
70
- }
71
-
72
- let property: string | undefined;
73
-
74
- if (name === PROPERTY_CHECKED && element.type === INPUT_TYPE_CHECKBOX) {
75
- property = PROPERTY_CHECKED;
76
- } else if (name === PROPERTY_VALUE) {
77
- property = PROPERTY_VALUE;
78
- }
79
-
80
- if (property != null) {
81
- mapEvent(element, EVENT_ON_PREFIXED, () => {
82
- const next = element[property as keyof typeof element];
83
-
84
- value.set(parse(String(next)) ?? next);
65
+ if (isInputElement(element) && isSignal(value) && name in element) {
66
+ mapEvent(element, EVENT_ON_VALUE, () => {
67
+ value.set(element[name as keyof typeof element]);
85
68
  });
86
69
  }
87
70
  }
@@ -0,0 +1,140 @@
1
+ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
+ import {getString} from '@oscarpalmer/atoms/string';
3
+ import {camelCase} from '@oscarpalmer/atoms/string/case';
4
+ import {isReactive} from '@oscarpalmer/mora';
5
+ import {setAttribute as setTorettoAttribute} from '@oscarpalmer/toretto/attribute';
6
+ import {setStyle} from '@oscarpalmer/toretto/style';
7
+ import {
8
+ ATTRIBUTE_CLASS_PREFIX_LENGTH,
9
+ ATTRIBUTE_NAME_DELIMITER,
10
+ EXPRESSION_ATTRIBUTE_CLASS,
11
+ EXPRESSION_ATTRIBUTE_STYLE_FULL,
12
+ EXPRESSION_ATTRIBUTE_STYLE_PREFIX,
13
+ EXPRESSION_ATTRIBUTE_STYLE_PROPERTY,
14
+ PROPERTY_CHECKED,
15
+ PROPERTY_VALUE,
16
+ VALUE_TRUE,
17
+ } from '../constants';
18
+ import type {FragmentData} from '../models';
19
+
20
+ function getStyleValue(value: unknown, unit: string, isProperty: boolean): string | undefined {
21
+ if (removeStyleValue(value, unit, isProperty)) {
22
+ return;
23
+ }
24
+
25
+ return value === true || value === VALUE_TRUE ? unit : `${getString(value)}${unit ?? ''}`;
26
+ }
27
+
28
+ function removeStyleValue(value: unknown, unit: unknown, isProperty: boolean): boolean {
29
+ if (value == null || value === false || (isProperty && isNullableOrWhitespace(value))) {
30
+ return true;
31
+ }
32
+
33
+ return unit == null && (value === true || value === VALUE_TRUE);
34
+ }
35
+
36
+ export function setAttribute(
37
+ data: FragmentData,
38
+ element: HTMLElement | SVGElement,
39
+ name: string,
40
+ value: unknown,
41
+ ): void {
42
+ switch (true) {
43
+ case EXPRESSION_ATTRIBUTE_CLASS.test(name):
44
+ setClassValues(data, element, name, value);
45
+ return;
46
+
47
+ case EXPRESSION_ATTRIBUTE_STYLE_PREFIX.test(name):
48
+ setStyleValues(data, element, name, value);
49
+ return;
50
+
51
+ default:
52
+ setValue(data, element, name, value);
53
+ break;
54
+ }
55
+ }
56
+
57
+ function setClassValues(
58
+ data: FragmentData,
59
+ element: HTMLElement | SVGElement,
60
+ name: string,
61
+ value: unknown,
62
+ ): void {
63
+ function update(value: unknown): void {
64
+ if (value === true || value === VALUE_TRUE) {
65
+ element.classList.add(...classes);
66
+ } else {
67
+ element.classList.remove(...classes);
68
+ }
69
+ }
70
+
71
+ const classes = name.slice(ATTRIBUTE_CLASS_PREFIX_LENGTH).split(ATTRIBUTE_NAME_DELIMITER);
72
+
73
+ if (isReactive(value)) {
74
+ data.mora.subscribers.add(value.subscribe(update));
75
+ } else {
76
+ update(value);
77
+ }
78
+ }
79
+
80
+ function setStyleValues(
81
+ data: FragmentData,
82
+ element: HTMLElement | SVGElement,
83
+ name: string,
84
+ value: unknown,
85
+ ): void {
86
+ let [, property, unit] = EXPRESSION_ATTRIBUTE_STYLE_FULL.exec(name)!;
87
+
88
+ const isProperty = EXPRESSION_ATTRIBUTE_STYLE_PROPERTY.test(name);
89
+
90
+ property = camelCase(property);
91
+
92
+ if (isProperty) {
93
+ property = `--${property}`;
94
+ }
95
+
96
+ function update(value: unknown): void {
97
+ setStyle(
98
+ element,
99
+ property as keyof CSSStyleDeclaration,
100
+ getStyleValue(value, unit, isProperty),
101
+ );
102
+ }
103
+
104
+ if (isReactive(value)) {
105
+ data.mora.subscribers.add(value.subscribe(update));
106
+ } else {
107
+ update(value);
108
+ }
109
+ }
110
+
111
+ function setValue(
112
+ data: FragmentData,
113
+ element: HTMLElement | SVGElement,
114
+ name: string,
115
+ value: unknown,
116
+ ): void {
117
+ const isReactiveValue = isReactive(value);
118
+
119
+ unsetValue(element, name, isReactiveValue ? value.peek() : value);
120
+
121
+ if (isReactiveValue) {
122
+ data.mora.subscribers.add(
123
+ value.subscribe(next => {
124
+ setTorettoAttribute(element, name, next);
125
+ }),
126
+ );
127
+ } else {
128
+ setTorettoAttribute(element, name, value);
129
+ }
130
+ }
131
+
132
+ function unsetValue(element: HTMLElement | SVGElement, name: string, value: unknown): void {
133
+ if (name === PROPERTY_CHECKED) {
134
+ (element as HTMLInputElement).checked = !(value === true || value === VALUE_TRUE);
135
+ }
136
+
137
+ if (name === PROPERTY_VALUE) {
138
+ (element as HTMLInputElement).value = (element as HTMLInputElement).value === '' ? '_' : '';
139
+ }
140
+ }
package/src/constants.ts CHANGED
@@ -8,6 +8,8 @@ export const ATTRIBUTE_CLASS_PREFIX_LENGTH = 6;
8
8
 
9
9
  export const ATTRIBUTE_NAME_DELIMITER = '.';
10
10
 
11
+ export const CHANGE_INPUTS = new Set(['checkbox', 'radio']);
12
+
11
13
  export const ERROR_FRAGMENT = 'Fragment function must return a Fragment instance';
12
14
 
13
15
  export const ERROR_IDENTIFIER_DUPLICATE = "Duplicate identifier found: '<id>'";
@@ -29,17 +31,15 @@ export const EVENT_DEFAULTS: Record<string, string> = {
29
31
  TEXTAREA: EVENT_INPUT,
30
32
  };
31
33
 
32
- export const EVENT_ON_PREFIXED = '@on';
33
-
34
34
  export const EVENT_ON_VALUE = 'on';
35
35
 
36
36
  export const EVENT_OPTIONS_DELIMITER = ':';
37
37
 
38
- export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-]+)="<!--abydon.(\d+)-->"/g;
38
+ export const EXPRESSION_ABYDON_ATTRIBUTE_FULL = /(@?[\w-.]+(?::[a-z:]+)?)="<!--abydon.(\d+)-->"/g;
39
39
 
40
- export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^_/;
40
+ export const EXPRESSION_ABYDON_ATTRIBUTE_PREFIX = /^abydon-/;
41
41
 
42
- export const EXPRESSION_ABYDON_CONTENT = /^@?abydon\.(\d+)@?$/;
42
+ export const EXPRESSION_ABYDON_CONTENT = /^abydon\.(\d+)$/;
43
43
 
44
44
  export const EXPRESSION_ATTRIBUTE_CLASS = /^class\./;
45
45
 
@@ -47,9 +47,11 @@ export const EXPRESSION_ATTRIBUTE_STYLE_FULL = /^style\.([\w-]+)(?:\.([\w-]+))?$
47
47
 
48
48
  export const EXPRESSION_ATTRIBUTE_STYLE_PREFIX = /^style\./;
49
49
 
50
- export const EXPRESSION_EVENT_CHANGE_TYPES = /^(checkbox|radio)$/;
50
+ export const EXPRESSION_ATTRIBUTE_STYLE_PROPERTY = /^style\.--/;
51
+
52
+ export const EXPRESSION_EVENT_ATTRIBUTE = /^@([\w-]+)(?::([a-z:]+))?="$/i;
51
53
 
52
- export const EXPRESSION_EVENT_NAME = /^@([\w-]+)(?::([a-z:]+))?$/i;
54
+ export const EXPRESSION_EVENT_NAME = /^@?([\w-]+)(?::([a-z:]+))?$/i;
53
55
 
54
56
  export const EXPRESSION_EVENT_OPTIONS_ACTIVE = /^a(ctive)$/i;
55
57
 
@@ -61,10 +63,12 @@ export const EXPRESSION_EVENT_PREFIX = /^@/;
61
63
 
62
64
  export const EXPRESSION_PERIOD = /\./;
63
65
 
64
- export const EXPRESSION_TEXTAREA_VALUE = /<!--abydon\.(\d+)-->/;
66
+ export const EXPRESSION_TEXTAREA_VALUE = /(?:<|&lt;)!--abydon\.(\d+)--(?:>|&gt;)/;
65
67
 
66
68
  export const INPUT_TYPE_CHECKBOX = 'checkbox';
67
69
 
70
+ export const INPUT_TYPE_RADIO = 'radio';
71
+
68
72
  export const NAME_FRAGMENT = '$fragment';
69
73
 
70
74
  export const NAME_FRAGMENTS = '$fragments';
@@ -76,3 +80,7 @@ export const PROPERTY_IDENTIFIER = 'identifier';
76
80
  export const PROPERTY_VALUE = 'value';
77
81
 
78
82
  export const TEMPLATE_ITEM = '<>';
83
+
84
+ export const VALUE_TRUE = 'true';
85
+
86
+ export const WHITESPACE = /\s+/g;
package/src/fragment.ts CHANGED
@@ -16,6 +16,13 @@ export class Fragment {
16
16
  cache: true,
17
17
  };
18
18
 
19
+ /**
20
+ * Is template caching enabled?
21
+ */
22
+ get caching(): boolean {
23
+ return this.#configuration.cache;
24
+ }
25
+
19
26
  /**
20
27
  * Fragment identifier
21
28
  */
@@ -91,14 +98,12 @@ export class Fragment {
91
98
 
92
99
  mapNodes(
93
100
  data,
94
- data.items.flatMap(
95
- item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
96
- ),
101
+ data.items.flatMap(item => item.nodes!),
97
102
  );
98
103
  }
99
104
 
100
105
  return data.items.flatMap(
101
- item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes ?? [],
106
+ item => item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes!,
102
107
  );
103
108
  }
104
109
 
@@ -137,7 +142,7 @@ function removeFragment(data: FragmentData): void {
137
142
  fragments?.[fragmentIndex]?.remove();
138
143
  }
139
144
 
140
- removeNodes(nodes ?? []);
145
+ removeNodes(nodes!);
141
146
  }
142
147
 
143
148
  data.items.length = 0;
package/src/fragments.ts CHANGED
@@ -14,13 +14,6 @@ import type {FragmentsState} from './models';
14
14
  export class Fragments {
15
15
  readonly #state: FragmentsState;
16
16
 
17
- /**
18
- * Fragment items
19
- */
20
- get items(): ReactiveArray<Fragment> {
21
- return this.#state.mapped;
22
- }
23
-
24
17
  constructor(
25
18
  items: ReactiveArray<unknown>,
26
19
  identify: (item: unknown) => unknown,
@@ -39,18 +32,16 @@ export class Fragments {
39
32
  subscriber: undefined,
40
33
  };
41
34
 
42
- states.set(this, this.#state);
35
+ fragmentsStates.set(this, this.#state);
43
36
 
44
37
  initializeFragments(this.#state);
45
38
  }
46
39
  }
47
40
 
48
41
  export function handleFragments(item: Fragments | FragmentsState, remove: boolean): void {
49
- const state = isFragments(item) ? states.get(item) : item;
42
+ const state = isFragments(item) ? fragmentsStates.get(item) : item;
50
43
 
51
- if (state != null) {
52
- (remove ? removeFragments : initializeFragments)(state);
53
- }
44
+ (remove ? removeFragments : initializeFragments)(state!);
54
45
  }
55
46
 
56
47
  function handleItems(state: FragmentsState, items: unknown[]): void {
@@ -135,4 +126,4 @@ function updateFragments(state: FragmentsState, active?: Set<string>): void {
135
126
 
136
127
  //
137
128
 
138
- const states: WeakMap<Fragments, FragmentsState> = new WeakMap();
129
+ export const fragmentsStates: WeakMap<Fragments, FragmentsState> = new WeakMap();
@@ -27,10 +27,20 @@ export function compareArrays(
27
27
  return firstIsLarger ? ARRAY_COMPARISON_REMOVED : ARRAY_COMPARISON_ADDED;
28
28
  }
29
29
 
30
+ /**
31
+ * Is the value a Fragment?
32
+ * @param value Value to check
33
+ * @returns `true` if the value is a Fragment, otherwise `false`
34
+ */
30
35
  export function isFragment(value: unknown): value is Fragment {
31
36
  return isNamed(value, NAME_FRAGMENT);
32
37
  }
33
38
 
39
+ /**
40
+ * Is the value a Fragments?
41
+ * @param value Value to check
42
+ * @returns `true` if the value is a Fragments, otherwise `false`
43
+ */
34
44
  export function isFragments(value: unknown): value is Fragments {
35
45
  return isNamed(value, NAME_FRAGMENTS);
36
46
  }
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import '@oscarpalmer/mora';
2
- import type {ReactiveArray} from '@oscarpalmer/mora';
2
+ import {isArray, type ReactiveArray} from '@oscarpalmer/mora';
3
3
  import {Fragment} from './fragment';
4
4
  import {Fragments} from './fragments';
5
5
 
6
6
  /**
7
- * Create Fragments from a reactive array
7
+ * Create a Fragments from a reactive array
8
8
  * @param array Reactive array
9
9
  * @param identify Function to identify item
10
10
  * @param fragment Function to create fragment from item
@@ -15,11 +15,23 @@ export function fragments<Item>(
15
15
  identify: (item: Item) => unknown,
16
16
  fragment: (item: Item) => Fragment,
17
17
  ): Fragments {
18
+ if (!isArray(array)) {
19
+ throw new TypeError('Fragments array must be a reactive array');
20
+ }
21
+
22
+ if (typeof identify !== 'function') {
23
+ throw new TypeError('Fragments identify must be a function');
24
+ }
25
+
26
+ if (typeof fragment !== 'function') {
27
+ throw new TypeError('Fragments fragment must be a function');
28
+ }
29
+
18
30
  return new Fragments(array as ReactiveArray<unknown>, identify as never, fragment as never);
19
31
  }
20
32
 
21
33
  /**
22
- * Create Fragment from a template
34
+ * Create a Fragment from a template
23
35
  * @returns Fragment
24
36
  */
25
37
  export function html(template: TemplateStringsArray, ...values: unknown[]): Fragment {
@@ -29,3 +41,4 @@ export function html(template: TemplateStringsArray, ...values: unknown[]): Frag
29
41
  export * from '@oscarpalmer/mora';
30
42
  export type {Fragment} from './fragment';
31
43
  export type {Fragments} from './fragments';
44
+ export {isFragment, isFragments} from './helpers';
package/src/node/event.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import {on} from '@oscarpalmer/toretto/event';
2
2
  import {
3
+ CHANGE_INPUTS,
3
4
  EVENT_CHANGE,
4
5
  EVENT_DEFAULTS,
5
6
  EVENT_INPUT,
6
7
  EVENT_ON_VALUE,
7
8
  EVENT_OPTIONS_DELIMITER,
8
9
  EVENT_SUBMIT,
9
- EXPRESSION_EVENT_CHANGE_TYPES,
10
10
  EXPRESSION_EVENT_NAME,
11
11
  EXPRESSION_EVENT_OPTIONS_ACTIVE,
12
12
  EXPRESSION_EVENT_OPTIONS_CAPTURE,
@@ -29,7 +29,7 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
29
29
  }
30
30
 
31
31
  if (element instanceof HTMLInputElement) {
32
- if (EXPRESSION_EVENT_CHANGE_TYPES.test(element.type)) {
32
+ if (CHANGE_INPUTS.has(element.type)) {
33
33
  return EVENT_CHANGE;
34
34
  }
35
35
 
@@ -40,9 +40,16 @@ function getType(element: HTMLElement | SVGElement, type: string): string {
40
40
  }
41
41
 
42
42
  export function mapEvent(element: HTMLElement | SVGElement, name: string, value: unknown): void {
43
- const [, type, options] = EXPRESSION_EVENT_NAME.exec(name) ?? [];
43
+ const [, type, options] = EXPRESSION_EVENT_NAME.exec(name)!;
44
44
 
45
- if (type != null && typeof value === 'function') {
46
- on(element, getType(element, type), value as EventListener, getOptions(options ?? ''));
45
+ const values = Array.isArray(value) ? value : [value];
46
+ const {length} = values;
47
+
48
+ for (let index = 0; index < length; index += 1) {
49
+ const item = values[index];
50
+
51
+ if (typeof item === 'function') {
52
+ on(element, getType(element, type), item as EventListener, getOptions(options ?? ''));
53
+ }
47
54
  }
48
55
  }
package/src/node/index.ts CHANGED
@@ -1,24 +1,25 @@
1
1
  import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {getString} from '@oscarpalmer/atoms/string';
2
3
  import {
3
4
  computed,
4
5
  isComputed,
5
6
  isReactive,
6
7
  isSignal,
7
8
  type Computed,
8
- type Reactive,
9
+ type ReactiveArray,
9
10
  } from '@oscarpalmer/mora';
10
11
  import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
11
- import {EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
12
- import {handleFragments} from '../fragments';
12
+ import {mapAttributes} from '../attribute/index';
13
+ import {EVENT_ON_VALUE, EXPRESSION_ABYDON_CONTENT, EXPRESSION_TEXTAREA_VALUE} from '../constants';
14
+ import {Fragments, fragmentsStates, handleFragments} from '../fragments';
13
15
  import {isFragment, isFragments} from '../helpers';
14
16
  import {createNodes} from '../helpers/dom';
15
17
  import type {FragmentData} from '../models';
16
- import {mapAttributes} from './attribute';
17
- import {setReactiveValue} from './value';
18
18
  import {mapEvent} from './event';
19
+ import {setReactiveValue} from './value';
19
20
 
20
21
  function mapNode(data: FragmentData, comment: Comment): void {
21
- const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent ?? '');
22
+ const matches = EXPRESSION_ABYDON_CONTENT.exec(comment.textContent);
22
23
  const value = matches == null ? null : data.values[+matches[1]];
23
24
 
24
25
  if (value != null) {
@@ -53,23 +54,33 @@ export function mapNodes(data: FragmentData, nodes: ChildNode[]): void {
53
54
  }
54
55
 
55
56
  function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
56
- const [, index] = EXPRESSION_TEXTAREA_VALUE.exec(element.value) ?? [];
57
+ const [, index] =
58
+ EXPRESSION_TEXTAREA_VALUE.exec(element.textContent) ??
59
+ EXPRESSION_TEXTAREA_VALUE.exec(element.value) ??
60
+ [];
57
61
 
58
62
  if (index == null) {
59
63
  return;
60
64
  }
61
65
 
66
+ element.textContent = '';
62
67
  element.value = '';
63
68
 
64
69
  const value = data.values[Number.parseInt(index, 10)];
65
70
 
66
71
  if (isSignal(value)) {
67
- element.value = String(value.peek());
72
+ element.value = getString(value.peek());
68
73
 
69
- mapEvent(element, '@on', () => {
74
+ mapEvent(element, EVENT_ON_VALUE, () => {
70
75
  value.set(element.value);
71
76
  });
72
77
 
78
+ data.mora.subscribers.add(
79
+ value.subscribe(value => {
80
+ element.value = getString(value);
81
+ }),
82
+ );
83
+
73
84
  return;
74
85
  }
75
86
 
@@ -82,11 +93,11 @@ function mapTextarea(data: FragmentData, element: HTMLTextAreaElement): void {
82
93
  }
83
94
 
84
95
  if (reactive == null) {
85
- element.value = String(value);
96
+ element.value = '';
86
97
  } else {
87
98
  data.mora.subscribers.add(
88
99
  reactive.subscribe(value => {
89
- element.value = String(value);
100
+ element.value = getString(value);
90
101
  }),
91
102
  );
92
103
  }
@@ -99,8 +110,7 @@ function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
99
110
  break;
100
111
 
101
112
  case isFragments(value):
102
- handleFragments(value, false);
103
- setReactiveValue(data, comment, value.items as Reactive<unknown[], unknown>);
113
+ setFragmentsValue(data, comment, value);
104
114
  break;
105
115
 
106
116
  case isReactive(value):
@@ -132,3 +142,11 @@ function setComputedValue(data: FragmentData, comment: Comment, callback: Generi
132
142
 
133
143
  setReactiveValue(data, comment, value);
134
144
  }
145
+
146
+ function setFragmentsValue(data: FragmentData, comment: Comment, fragments: Fragments): void {
147
+ const state = fragmentsStates.get(fragments)!;
148
+
149
+ handleFragments(state, false);
150
+
151
+ setReactiveValue(data, comment, state.mapped as ReactiveArray<unknown>);
152
+ }
package/src/node/value.ts CHANGED
@@ -113,11 +113,7 @@ function replaceText(item: FragmentItem, comment: Comment, isNullable: boolean):
113
113
  replaceNodes(item.nodes ?? [], to);
114
114
  }
115
115
 
116
- function setArray(
117
- item: FragmentItem,
118
- comment: Comment,
119
- value: unknown[],
120
- ): Partial<FragmentItem> | boolean {
116
+ function setArray(item: FragmentItem, comment: Comment, value: unknown[]): Partial<FragmentItem> {
121
117
  if (value.length === 0) {
122
118
  return {
123
119
  nodes: setText(item, comment, value),
@@ -129,7 +125,9 @@ function setArray(
129
125
  const next = templates.map(fragment => fragment.identifier) as unknown[];
130
126
  const previous = item.fragments?.map(fragment => fragment.identifier) ?? [];
131
127
 
132
- if (new Set(next).size !== templates.length) {
128
+ const nextSet = new Set(next);
129
+
130
+ if (nextSet.size !== templates.length) {
133
131
  templates = [];
134
132
  }
135
133
 
@@ -150,7 +148,7 @@ function setArray(
150
148
 
151
149
  return handleArray(
152
150
  {
153
- next: new Set(next),
151
+ next: nextSet,
154
152
  previous: new Set(previous),
155
153
  },
156
154
  {
@@ -163,11 +161,7 @@ function setArray(
163
161
 
164
162
  function setNodes(item: FragmentItem, comment: Comment, next: ChildNode[]): ChildNode[] {
165
163
  if (item.nodes == null) {
166
- if (comment.parentNode != null) {
167
- replaceNodes([comment], next);
168
- } else if (item.text?.parentNode != null) {
169
- replaceNodes([item.text], next);
170
- }
164
+ replaceNodes([comment], next);
171
165
  } else {
172
166
  replaceNodes(item.nodes, next);
173
167
  }
@@ -196,25 +190,16 @@ export function setReactiveValue(
196
190
  setReactiveValueForSingle(item, comment, value);
197
191
  }
198
192
 
199
- item.nodes = [...(item.nodes ?? [comment])];
193
+ item.nodes ??= [comment];
200
194
  }),
201
195
  );
202
196
  }
203
197
 
204
198
  function setReactiveValueForArray(item: FragmentItem, comment: Comment, value: unknown[]): void {
205
- const result = setArray(item, comment, value);
206
-
207
- item.fragments = typeof result === 'boolean' ? undefined : result?.fragments;
199
+ const {fragments, nodes} = setArray(item, comment, value);
208
200
 
209
- if (typeof result === 'boolean') {
210
- if (result) {
211
- item.nodes = item.text == null ? [] : [item.text];
212
- } else {
213
- item.nodes = undefined;
214
- }
215
- } else {
216
- item.nodes = result?.nodes;
217
- }
201
+ item.fragments = fragments;
202
+ item.nodes = nodes;
218
203
  }
219
204
 
220
205
  function setReactiveValueForSingle(item: FragmentItem, comment: Comment, value: unknown): void {