jaxs 0.2.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/.env +1 -0
- package/.parcel-cache/6f14daf302269614-BundleGraph-0 +0 -0
- package/.parcel-cache/7d3d872b02d671a6-AssetGraph-0 +0 -0
- package/.parcel-cache/8e029bb14f8992df-RequestGraph-0 +0 -0
- package/.parcel-cache/a5394978e4ece10b-AssetGraph-0 +0 -0
- package/.parcel-cache/b5686051ae060930.txt +2 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/README.md +15 -0
- package/bun.lockb +0 -0
- package/bundle.ts +5 -0
- package/bunfig.toml +0 -0
- package/cypress/e2e/add-remove-nested-children.cy.js +39 -0
- package/cypress/e2e/add-remove-root-children.cy.js +37 -0
- package/cypress/jaxs-apps/add-remove-nested-children.html +12 -0
- package/cypress/jaxs-apps/add-remove-nested-children.jsx +84 -0
- package/cypress/jaxs-apps/add-remove-root-children.html +15 -0
- package/cypress/jaxs-apps/add-remove-root-children.jsx +53 -0
- package/cypress/jaxs-apps/dist/add-remove-nested-children.afcab974.js +717 -0
- package/cypress/jaxs-apps/dist/add-remove-nested-children.afcab974.js.map +1 -0
- package/cypress/jaxs-apps/dist/add-remove-nested-children.html +12 -0
- package/cypress/jaxs-apps/dist/add-remove-root-children.3bb9b3f5.js +1682 -0
- package/cypress/jaxs-apps/dist/add-remove-root-children.3bb9b3f5.js.map +1 -0
- package/cypress/jaxs-apps/dist/add-remove-root-children.fbb4ec9b.js +706 -0
- package/cypress/jaxs-apps/dist/add-remove-root-children.fbb4ec9b.js.map +1 -0
- package/cypress/jaxs-apps/dist/add-remove-root-children.html +15 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/e2e.js +20 -0
- package/cypress.config.js +10 -0
- package/dist/jaxs.js +1154 -0
- package/package.json +38 -0
- package/src/app.ts +64 -0
- package/src/debugging.js +5 -0
- package/src/jaxs.ts +8 -0
- package/src/jsx.js +27 -0
- package/src/messageBus.ts +70 -0
- package/src/navigation/findHref.js +10 -0
- package/src/navigation/routeState.js +15 -0
- package/src/navigation/setupHistory.js +38 -0
- package/src/navigation/setupNavigation.js +25 -0
- package/src/navigation.ts +2 -0
- package/src/rendering/change/compile.ts +1 -0
- package/src/rendering/change/instructions/attributes.ts +78 -0
- package/src/rendering/change/instructions/children.ts +128 -0
- package/src/rendering/change/instructions/element.ts +42 -0
- package/src/rendering/change/instructions/events.ts +51 -0
- package/src/rendering/change/instructions/generate.ts +122 -0
- package/src/rendering/change/instructions/idMap.js +55 -0
- package/src/rendering/change/instructions/node.ts +38 -0
- package/src/rendering/change/instructions/text.ts +10 -0
- package/src/rendering/change.ts +131 -0
- package/src/rendering/dom/attributesAndEvents.ts +33 -0
- package/src/rendering/dom/create.ts +68 -0
- package/src/rendering/templates/bound.js +55 -0
- package/src/rendering/templates/children.ts +91 -0
- package/src/rendering/templates/root.ts +55 -0
- package/src/rendering/templates/tag.ts +70 -0
- package/src/rendering/templates/text.ts +17 -0
- package/src/state/equality.js +36 -0
- package/src/state/stores.js +63 -0
- package/src/state/testingTypes.js +6 -0
- package/src/state.js +89 -0
- package/src/types.ts +149 -0
- package/src/views/conditionals.jsx +18 -0
- package/src/views/link.jsx +5 -0
- package/src/views.js +7 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttributeInstructionData,
|
|
3
|
+
ChangeInstructions,
|
|
4
|
+
Dom,
|
|
5
|
+
EventInstructionData,
|
|
6
|
+
ExpandedElement,
|
|
7
|
+
InputElement,
|
|
8
|
+
InsertNodeData,
|
|
9
|
+
Instruction,
|
|
10
|
+
RemoveInstructionData,
|
|
11
|
+
UpdateEventInstructionData,
|
|
12
|
+
} from '../../../types';
|
|
13
|
+
|
|
14
|
+
export const changeText = (source: Text, target: Text): Instruction => ({
|
|
15
|
+
source,
|
|
16
|
+
target,
|
|
17
|
+
type: ChangeInstructions.changeText,
|
|
18
|
+
data: {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const replaceNode = (source: Dom, target: Dom): Instruction => ({
|
|
22
|
+
source,
|
|
23
|
+
target,
|
|
24
|
+
type: ChangeInstructions.replaceNode,
|
|
25
|
+
data: {},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const removeAttribute = (
|
|
29
|
+
source: ExpandedElement,
|
|
30
|
+
target: ExpandedElement,
|
|
31
|
+
data: RemoveInstructionData,
|
|
32
|
+
): Instruction => ({
|
|
33
|
+
source,
|
|
34
|
+
target,
|
|
35
|
+
data,
|
|
36
|
+
type: ChangeInstructions.removeAttribute,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const addAttribute = (
|
|
40
|
+
source: ExpandedElement,
|
|
41
|
+
target: ExpandedElement,
|
|
42
|
+
data: AttributeInstructionData,
|
|
43
|
+
): Instruction => ({
|
|
44
|
+
source,
|
|
45
|
+
target,
|
|
46
|
+
data,
|
|
47
|
+
type: ChangeInstructions.addAttribute,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const updateAttribute = (
|
|
51
|
+
source: ExpandedElement,
|
|
52
|
+
target: ExpandedElement,
|
|
53
|
+
data: AttributeInstructionData,
|
|
54
|
+
): Instruction => ({
|
|
55
|
+
source,
|
|
56
|
+
target,
|
|
57
|
+
data,
|
|
58
|
+
type: ChangeInstructions.updateAttribute,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const removeEvent = (
|
|
62
|
+
source: ExpandedElement,
|
|
63
|
+
target: ExpandedElement,
|
|
64
|
+
data: EventInstructionData,
|
|
65
|
+
): Instruction => ({
|
|
66
|
+
source,
|
|
67
|
+
target,
|
|
68
|
+
data,
|
|
69
|
+
type: ChangeInstructions.removeEvent,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const addEvent = (
|
|
73
|
+
source: ExpandedElement,
|
|
74
|
+
target: ExpandedElement,
|
|
75
|
+
data: EventInstructionData,
|
|
76
|
+
): Instruction => ({
|
|
77
|
+
source,
|
|
78
|
+
target,
|
|
79
|
+
data,
|
|
80
|
+
type: ChangeInstructions.addEvent,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const updateEvent = (
|
|
84
|
+
source: ExpandedElement,
|
|
85
|
+
target: ExpandedElement,
|
|
86
|
+
data: UpdateEventInstructionData,
|
|
87
|
+
): Instruction => ({
|
|
88
|
+
source,
|
|
89
|
+
target,
|
|
90
|
+
data,
|
|
91
|
+
type: ChangeInstructions.updateEvent,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const removeNode = (
|
|
95
|
+
source: ExpandedElement,
|
|
96
|
+
) => ({
|
|
97
|
+
source,
|
|
98
|
+
target: source, // for type crap only
|
|
99
|
+
type: ChangeInstructions.removeNode,
|
|
100
|
+
data: {},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const insertNode = (
|
|
104
|
+
target: ExpandedElement,
|
|
105
|
+
data: InsertNodeData,
|
|
106
|
+
) => ({
|
|
107
|
+
target,
|
|
108
|
+
source: target, // for type crap only
|
|
109
|
+
type: ChangeInstructions.insertNode,
|
|
110
|
+
data,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const changeValue = (
|
|
114
|
+
source: InputElement,
|
|
115
|
+
target: InputElement,
|
|
116
|
+
data: AttributeInstructionData,
|
|
117
|
+
) => ({
|
|
118
|
+
source,
|
|
119
|
+
target,
|
|
120
|
+
type: ChangeInstructions.changeValue,
|
|
121
|
+
data,
|
|
122
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const nullMatch = { index: -1 }
|
|
2
|
+
|
|
3
|
+
export class IdMap {
|
|
4
|
+
constructor () {
|
|
5
|
+
this.map = {}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
populate (list) {
|
|
9
|
+
list.forEach((element, i) => {
|
|
10
|
+
const id = element.__jsx
|
|
11
|
+
if (id) {
|
|
12
|
+
this.map[id] = this.map[id] || []
|
|
13
|
+
this.map[id].push({
|
|
14
|
+
element,
|
|
15
|
+
index: i
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pullMatch (element) {
|
|
22
|
+
const id = element && element.__jsx
|
|
23
|
+
if (!id) return nullMatch
|
|
24
|
+
if (!(this.map[id] && this.map[id].length)) return nullMatch
|
|
25
|
+
|
|
26
|
+
return this.map[id].shift()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
clear (element) {
|
|
30
|
+
const id = element && element.__jsx
|
|
31
|
+
if (!(id && this.map[id] && this.map[id].length)) return
|
|
32
|
+
|
|
33
|
+
const matches = this.map[id]
|
|
34
|
+
this.map[id] = matches.reduce((collection, possibleMatch) => {
|
|
35
|
+
if (possibleMatch.element !== element) collection.push(possibleMatch)
|
|
36
|
+
return collection
|
|
37
|
+
}, [])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
check (element) {
|
|
41
|
+
const id = element && element.__jsx
|
|
42
|
+
if (!(id && this.map[id])) return false
|
|
43
|
+
return this.map[id].length > 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
remaining () {
|
|
47
|
+
return Object.values(this.map).flat()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const createIdMap = (list) => {
|
|
52
|
+
const map = new IdMap()
|
|
53
|
+
map.populate(list)
|
|
54
|
+
return map
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Dom, ExpandedElement, Instructions } from '../../../types';
|
|
2
|
+
import { compileForElement } from './element';
|
|
3
|
+
import { compileForText } from './text';
|
|
4
|
+
|
|
5
|
+
enum NodeTypes {
|
|
6
|
+
ElementNode = 1,
|
|
7
|
+
TextNode = 3,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const compileForNodeGenerator =
|
|
11
|
+
(compileForCollection: any) => (source: Dom, target: Dom) => {
|
|
12
|
+
let instructions = [] as Instructions;
|
|
13
|
+
|
|
14
|
+
if (source.nodeType === NodeTypes.ElementNode) {
|
|
15
|
+
const sourceElement = source as ExpandedElement;
|
|
16
|
+
const targetElement = target as ExpandedElement;
|
|
17
|
+
|
|
18
|
+
const baseInstructions = compileForElement(
|
|
19
|
+
sourceElement,
|
|
20
|
+
targetElement,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const childrenInstructions = compileForCollection(
|
|
24
|
+
sourceElement.childNodes,
|
|
25
|
+
targetElement.childNodes,
|
|
26
|
+
sourceElement,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
instructions = baseInstructions.concat(childrenInstructions);
|
|
30
|
+
} else if (source.nodeType === NodeTypes.TextNode) {
|
|
31
|
+
instructions = compileForText(
|
|
32
|
+
source as Text,
|
|
33
|
+
target as Text,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return instructions;
|
|
38
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Instructions } from '../../../types';
|
|
2
|
+
import { changeText } from './generate';
|
|
3
|
+
|
|
4
|
+
export const compileForText = (source: Text, target: Text) => {
|
|
5
|
+
if (source.textContent !== target.textContent) {
|
|
6
|
+
return [changeText(source, target)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return [] as Instructions;
|
|
10
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AttributeInstructionData,
|
|
3
|
+
EventInstructionData,
|
|
4
|
+
ExpandedElement,
|
|
5
|
+
HtmlChildren,
|
|
6
|
+
InputElement,
|
|
7
|
+
InsertNodeData,
|
|
8
|
+
Instruction,
|
|
9
|
+
RemoveInstructionData,
|
|
10
|
+
UpdateEventInstructionData,
|
|
11
|
+
Updater,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import { ChangeInstructions } from '../types'
|
|
14
|
+
import { compileChange } from './change/compile';
|
|
15
|
+
import { debug } from '../debugging';
|
|
16
|
+
|
|
17
|
+
export const change = (
|
|
18
|
+
source: HtmlChildren,
|
|
19
|
+
target: HtmlChildren,
|
|
20
|
+
parent: ExpandedElement,
|
|
21
|
+
) => {
|
|
22
|
+
const instructions = compileChange(source, target, parent);
|
|
23
|
+
|
|
24
|
+
debug('instructions', instructions.map((instruction) => instruction.type))
|
|
25
|
+
|
|
26
|
+
instructions.forEach((instruction) => {
|
|
27
|
+
performInstruction(instruction);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const performInstruction = (instruction: Instruction) => {
|
|
32
|
+
const performer = performers[instruction.type] || noop;
|
|
33
|
+
performer(instruction);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const noop: Updater = (_instruction: Instruction) => {};
|
|
37
|
+
|
|
38
|
+
const changeText: Updater = (instruction: Instruction) => {
|
|
39
|
+
const { source, target } = instruction;
|
|
40
|
+
source.nodeValue = target.textContent;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const removeNode: Updater = (instruction: Instruction) => {
|
|
44
|
+
const { source } = instruction;
|
|
45
|
+
source.remove();
|
|
46
|
+
debug('removeNode called on', source.nodeName)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const insertNode: Updater = (instruction: Instruction) => {
|
|
50
|
+
const { target, data } = instruction;
|
|
51
|
+
const { parent, index } = data as InsertNodeData;
|
|
52
|
+
const sibling = parent.childNodes[index];
|
|
53
|
+
if (!sibling) {
|
|
54
|
+
parent.appendChild(target);
|
|
55
|
+
} else if (sibling && sibling !== target) {
|
|
56
|
+
parent.insertBefore(target, sibling);
|
|
57
|
+
}
|
|
58
|
+
// else case, sibling is target and so it is moving to the same place: no-op.
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const replaceNode: Updater = (instruction: Instruction) => {
|
|
62
|
+
const { source, target } = instruction;
|
|
63
|
+
source.replaceWith(target);
|
|
64
|
+
debug('replaceNode called on', source.nodeName, 'with', target.nodeName)
|
|
65
|
+
debug('parent', source.parentElement)
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const removeAttribute: Updater = (instruction: Instruction) => {
|
|
69
|
+
const { source, data } = instruction;
|
|
70
|
+
const { name } = data as RemoveInstructionData;
|
|
71
|
+
|
|
72
|
+
(source as ExpandedElement).removeAttribute(name);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const addAttribute: Updater = (instruction: Instruction) => {
|
|
76
|
+
const { source, data } = instruction;
|
|
77
|
+
const { name, value } = data as AttributeInstructionData;
|
|
78
|
+
|
|
79
|
+
(source as ExpandedElement).setAttribute(name, value);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const updateAttribute: Updater = (instruction: Instruction) => {
|
|
83
|
+
addAttribute(instruction);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const removeEvent: Updater = (instruction: Instruction) => {
|
|
87
|
+
const data = instruction.data as EventInstructionData;
|
|
88
|
+
const source = instruction.source as ExpandedElement;
|
|
89
|
+
const { name, value } = data;
|
|
90
|
+
(source as ExpandedElement).removeEventListener(name, value);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const addEvent: Updater = (instruction: Instruction) => {
|
|
94
|
+
const data = instruction.data as EventInstructionData;
|
|
95
|
+
const source = instruction.source as ExpandedElement;
|
|
96
|
+
const { name, value } = data;
|
|
97
|
+
(source as ExpandedElement).addEventListener(name, value);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const updateEvent: Updater = (instruction: Instruction) => {
|
|
101
|
+
const data = instruction.data as UpdateEventInstructionData;
|
|
102
|
+
const source = instruction.source as ExpandedElement;
|
|
103
|
+
|
|
104
|
+
const { name, sourceValue, targetValue } = data;
|
|
105
|
+
|
|
106
|
+
source.removeEventListener(name, sourceValue);
|
|
107
|
+
source.addEventListener(name, targetValue);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const changeValue: Updater = (instruction: Instruction) => {
|
|
111
|
+
const data = instruction.data as AttributeInstructionData;
|
|
112
|
+
const source = instruction.source as InputElement;
|
|
113
|
+
|
|
114
|
+
const { value } = data;
|
|
115
|
+
|
|
116
|
+
source.value = value;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const performers = {
|
|
120
|
+
[ChangeInstructions.changeText]: changeText,
|
|
121
|
+
[ChangeInstructions.removeNode]: removeNode,
|
|
122
|
+
[ChangeInstructions.insertNode]: insertNode,
|
|
123
|
+
[ChangeInstructions.replaceNode]: replaceNode,
|
|
124
|
+
[ChangeInstructions.removeAttribute]: removeAttribute,
|
|
125
|
+
[ChangeInstructions.addAttribute]: addAttribute,
|
|
126
|
+
[ChangeInstructions.updateAttribute]: updateAttribute,
|
|
127
|
+
[ChangeInstructions.removeEvent]: removeEvent,
|
|
128
|
+
[ChangeInstructions.addEvent]: addEvent,
|
|
129
|
+
[ChangeInstructions.updateEvent]: updateEvent,
|
|
130
|
+
[ChangeInstructions.changeValue]: changeValue,
|
|
131
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Attributes, AttributesAndEvents } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const separateAttrsAndEvents = (
|
|
4
|
+
combined: Attributes,
|
|
5
|
+
defaultValue = '',
|
|
6
|
+
): AttributesAndEvents => {
|
|
7
|
+
const attributes: Attributes = {};
|
|
8
|
+
const events: Attributes = {};
|
|
9
|
+
|
|
10
|
+
for (const key in combined) {
|
|
11
|
+
const value = combined[key];
|
|
12
|
+
if (key.match(/^on.+/i)) {
|
|
13
|
+
const eventKey = key.slice(2).toLowerCase();
|
|
14
|
+
events[eventKey] = value;
|
|
15
|
+
} else {
|
|
16
|
+
attributes[key] = normalizeValueForKey(combined, key, defaultValue);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
attributes,
|
|
22
|
+
events,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const normalizeValueForKey = (
|
|
27
|
+
object: Attributes,
|
|
28
|
+
key: string,
|
|
29
|
+
defaultValue = '',
|
|
30
|
+
) => {
|
|
31
|
+
if (object[key] === undefined) return defaultValue;
|
|
32
|
+
return object[key];
|
|
33
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attributes,
|
|
3
|
+
DomEventPublisher,
|
|
4
|
+
EventAttributes,
|
|
5
|
+
EventMaps,
|
|
6
|
+
ExpandedElement,
|
|
7
|
+
InputElement,
|
|
8
|
+
RenderKit,
|
|
9
|
+
} from '../../types';
|
|
10
|
+
|
|
11
|
+
export const setAttributesOnElement = (
|
|
12
|
+
element: Element,
|
|
13
|
+
attributes: Attributes,
|
|
14
|
+
) => {
|
|
15
|
+
for (const key in attributes) {
|
|
16
|
+
if (key === '__self') continue;
|
|
17
|
+
if (key === 'value') {
|
|
18
|
+
(element as InputElement).value = attributes[key];
|
|
19
|
+
} else {
|
|
20
|
+
element.setAttribute(key, attributes[key]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const setEventsOnElement = (
|
|
26
|
+
element: ExpandedElement,
|
|
27
|
+
events: EventAttributes,
|
|
28
|
+
publish: DomEventPublisher,
|
|
29
|
+
) => {
|
|
30
|
+
const eventMaps = {} as EventMaps;
|
|
31
|
+
|
|
32
|
+
for (const domEvent in events) {
|
|
33
|
+
const eventName = events[domEvent];
|
|
34
|
+
const listener = (event: Event) => publish(eventName, event);
|
|
35
|
+
element.addEventListener(domEvent, listener);
|
|
36
|
+
|
|
37
|
+
eventMaps[domEvent] = {
|
|
38
|
+
domEvent: domEvent,
|
|
39
|
+
busEvent: eventName,
|
|
40
|
+
listener: listener,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
element.eventMaps = eventMaps;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const createNode = (type: string, document: Document) => {
|
|
48
|
+
document = document || window.document;
|
|
49
|
+
return document.createElement(type);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const createTextNode = (value: string, document: Document) => {
|
|
53
|
+
document = document || window.document;
|
|
54
|
+
return document.createTextNode(value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const createDecoratedNode = (
|
|
58
|
+
type: string,
|
|
59
|
+
attributes: Attributes,
|
|
60
|
+
events: EventAttributes,
|
|
61
|
+
renderKit: RenderKit,
|
|
62
|
+
) => {
|
|
63
|
+
// deno-lint-ignore no-explicit-any
|
|
64
|
+
const dom = createNode(type, renderKit.document) as any as ExpandedElement;
|
|
65
|
+
setAttributesOnElement(dom, attributes);
|
|
66
|
+
setEventsOnElement(dom, events, renderKit.publish);
|
|
67
|
+
return dom;
|
|
68
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { eventName } from '../../state'
|
|
2
|
+
import { change } from '../change'
|
|
3
|
+
|
|
4
|
+
export class Bound {
|
|
5
|
+
constructor (TemplateClass, viewModel, subscriptions, attributes) {
|
|
6
|
+
this.TemplateClass = TemplateClass
|
|
7
|
+
this.viewModel = viewModel
|
|
8
|
+
this.attributes = attributes || {}
|
|
9
|
+
this.subscriptions = subscriptions
|
|
10
|
+
this.dom = []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
render (renderKit) {
|
|
14
|
+
this.parentElement = renderKit.parent
|
|
15
|
+
this.renderKit = renderKit
|
|
16
|
+
this.subscribeForRerender()
|
|
17
|
+
this.dom = this._render(renderKit)
|
|
18
|
+
return this.dom
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_render (renderKit) {
|
|
22
|
+
const props = {
|
|
23
|
+
...this.attributes,
|
|
24
|
+
...this.viewModel(renderKit.state.value())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const template = this.TemplateClass(props)
|
|
28
|
+
|
|
29
|
+
const dom = !template ? [] : template.render(renderKit)
|
|
30
|
+
return dom
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
rerender () {
|
|
34
|
+
if (!this.parentElement) {
|
|
35
|
+
this.parentElement = this.dom[0] && this.dom[0].parentElement
|
|
36
|
+
}
|
|
37
|
+
const newDom = this._render(this.renderKit)
|
|
38
|
+
change(this.dom, newDom, this.parentElement)
|
|
39
|
+
|
|
40
|
+
if (this.parentElement) {
|
|
41
|
+
this.dom = Array.from(this.parentElement.childNodes)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
subscribeForRerender () {
|
|
46
|
+
this.subscriptions.forEach((storeName) => {
|
|
47
|
+
this.renderKit.subscribe(eventName(storeName), () => this.rerender())
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const bind = ({ Template, viewModel, subscriptions }) => {
|
|
53
|
+
subscriptions = subscriptions || []
|
|
54
|
+
return (attributes) => new Bound(Template, viewModel, subscriptions, attributes)
|
|
55
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Dom,
|
|
3
|
+
DomCollection,
|
|
4
|
+
RenderKit,
|
|
5
|
+
Template,
|
|
6
|
+
TextValue,
|
|
7
|
+
} from '../../types';
|
|
8
|
+
import { TextTemplate } from './text';
|
|
9
|
+
|
|
10
|
+
export const ensureArray = (children: Template | Template[]): Template[] => {
|
|
11
|
+
if (Array.isArray(children)) {
|
|
12
|
+
return children.flat();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!children) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return [children];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/* three options for children
|
|
23
|
+
1. there is no view
|
|
24
|
+
2. view is an array, recurse
|
|
25
|
+
3. view is a renderable thing
|
|
26
|
+
*/
|
|
27
|
+
const recursiveRender = (
|
|
28
|
+
children: Template[],
|
|
29
|
+
renderKit: RenderKit,
|
|
30
|
+
rendered = [] as DomCollection,
|
|
31
|
+
): DomCollection => children.reduce(renderReducer(renderKit), rendered).flat();
|
|
32
|
+
|
|
33
|
+
const renderReducer =
|
|
34
|
+
(renderKit: RenderKit) =>
|
|
35
|
+
(aggregate: DomCollection, view: Template): DomCollection => {
|
|
36
|
+
if (!view) return aggregate;
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(view)) {
|
|
39
|
+
const dom = recursiveRender(view, renderKit, aggregate) as DomCollection;
|
|
40
|
+
return dom;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
view.render(renderKit).forEach((template) => aggregate.push(template));
|
|
44
|
+
|
|
45
|
+
return aggregate;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const replaceTextNodes = (child: TextValue | Template) => {
|
|
49
|
+
if (isTextNode(child)) {
|
|
50
|
+
return textNode(child as TextValue);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return child;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const isTextNode = (child: TextValue | Template) => {
|
|
57
|
+
return typeof child === 'string' || typeof child === 'number';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const textNode = (content: TextValue) => {
|
|
61
|
+
return new TextTemplate(content);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export class Children implements Template {
|
|
65
|
+
collection: Template[];
|
|
66
|
+
parentElement: Element | undefined;
|
|
67
|
+
|
|
68
|
+
constructor(jsxChildren: Template[]) {
|
|
69
|
+
this.collection = ensureArray(jsxChildren);
|
|
70
|
+
this.collection = this.collection.map(replaceTextNodes) as Template[];
|
|
71
|
+
this.collection = this.collection.flat() as Template[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render(renderKit: RenderKit, parentElement: Element | undefined) {
|
|
75
|
+
this.parentElement = parentElement;
|
|
76
|
+
const dom = this.generateDom(renderKit);
|
|
77
|
+
this.attachToParent(dom);
|
|
78
|
+
return dom;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
generateDom(renderKit: RenderKit) {
|
|
82
|
+
return recursiveRender(this.collection, renderKit);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
attachToParent(dom: DomCollection) {
|
|
86
|
+
if (this.parentElement === undefined) return;
|
|
87
|
+
|
|
88
|
+
const parent = this.parentElement as Element;
|
|
89
|
+
dom.forEach((node: Dom) => parent.appendChild(node));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DomCollection,
|
|
3
|
+
RenderKit,
|
|
4
|
+
Template,
|
|
5
|
+
} from '../../types';
|
|
6
|
+
|
|
7
|
+
export class Root {
|
|
8
|
+
template: Template;
|
|
9
|
+
selector: string;
|
|
10
|
+
renderKit: RenderKit;
|
|
11
|
+
dom: DomCollection;
|
|
12
|
+
parentElement: Element | null;
|
|
13
|
+
|
|
14
|
+
constructor(template: Template, selector: string, renderKit: RenderKit) {
|
|
15
|
+
this.template = template;
|
|
16
|
+
this.selector = selector;
|
|
17
|
+
this.renderKit = renderKit;
|
|
18
|
+
this.dom = [];
|
|
19
|
+
this.parentElement = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
renderAndAttach(renderKit: RenderKit) {
|
|
23
|
+
this.parentElement = this.getParentElement();
|
|
24
|
+
this.dom = this.render({...renderKit, parent: this.parentElement});
|
|
25
|
+
|
|
26
|
+
if (this.parentElement) {
|
|
27
|
+
this.attach();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render(renderKit: RenderKit) {
|
|
32
|
+
return this.template.render(renderKit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attach() {
|
|
36
|
+
this.parentElement && (this.parentElement.innerHTML = '');
|
|
37
|
+
this.dom.forEach((element) => {
|
|
38
|
+
this.parentElement && this.parentElement.appendChild(element);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getParentElement() {
|
|
43
|
+
return this.renderKit.document.querySelector(this.selector);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const render = (
|
|
48
|
+
template: Template,
|
|
49
|
+
selector: string,
|
|
50
|
+
renderKit: RenderKit,
|
|
51
|
+
) => {
|
|
52
|
+
const root = new Root(template, selector, renderKit);
|
|
53
|
+
root.renderAndAttach(renderKit);
|
|
54
|
+
return root;
|
|
55
|
+
};
|