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
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jaxs",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Modular J/TSX application framework",
|
|
5
|
+
"module": "src/jaxs.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
"@types/jsdom": "^21.1.6",
|
|
10
|
+
"concurrently": "^8.2.2",
|
|
11
|
+
"cypress": "^13.6.2",
|
|
12
|
+
"jsdom": "^23.2.0",
|
|
13
|
+
"npm": "^10.3.0",
|
|
14
|
+
"parcel": "^2.11.0",
|
|
15
|
+
"standard": "^17.1.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "standard --fix",
|
|
22
|
+
"build": "bun run bundle.ts",
|
|
23
|
+
"serve": "bun run build && parcel serve --dist-dir=cypress/jaxs-apps/dist",
|
|
24
|
+
"integration": "concurrently \"npm run serve\" \"npx cypress open\"",
|
|
25
|
+
"integration:ci": "concurrently \"npm run serve\" \"npx cypress run\""
|
|
26
|
+
},
|
|
27
|
+
"targets": {
|
|
28
|
+
"html": {
|
|
29
|
+
"source": [
|
|
30
|
+
"./cypress/jaxs-apps/add-remove-root-children.html",
|
|
31
|
+
"./cypress/jaxs-apps/add-remove-nested-children.html"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"standard": {
|
|
36
|
+
"globals": [ "cy", "describe", "it", "beforeEach" ]
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { App, BusPublish, BusSubscribe } from './types';
|
|
2
|
+
import { createBus } from './messageBus';
|
|
3
|
+
import { State } from './state'
|
|
4
|
+
import {
|
|
5
|
+
locationChangeEvent,
|
|
6
|
+
setupHistory,
|
|
7
|
+
} from './navigation/setupHistory';
|
|
8
|
+
import { setupNavigation } from './navigation/setupNavigation';
|
|
9
|
+
import { render } from './rendering/templates/root';
|
|
10
|
+
|
|
11
|
+
const setupBus = (app: App) => {
|
|
12
|
+
const { publish, subscribe, bus } = createBus();
|
|
13
|
+
|
|
14
|
+
app.publish = publish;
|
|
15
|
+
app.subscribe = subscribe;
|
|
16
|
+
app.bus = bus;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const setupState = (app: App) => {
|
|
20
|
+
const state = new State(app.publish as BusPublish);
|
|
21
|
+
app.state = state;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const connectBusToState = (app: App) => {
|
|
25
|
+
const { bus } = app;
|
|
26
|
+
bus.addListenerOptions({ state: app.state });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const setupRenderKit = (app: App, document: Document) => {
|
|
30
|
+
app.renderKit = {
|
|
31
|
+
publish: app.publish as BusPublish,
|
|
32
|
+
subscribe: app.subscribe as BusSubscribe,
|
|
33
|
+
state: app.state as State,
|
|
34
|
+
document,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const triggerRoute = (app: App) => {
|
|
39
|
+
const publish = app.publish as BusPublish;
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
publish(locationChangeEvent, null);
|
|
42
|
+
}, 0);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const addRender = (app: App) => {
|
|
46
|
+
app.render = (template, selector) => {
|
|
47
|
+
return render(template, selector, app.renderKit);
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const createApp = (document = window.document) => {
|
|
52
|
+
const app = {} as App;
|
|
53
|
+
|
|
54
|
+
setupBus(app);
|
|
55
|
+
setupState(app);
|
|
56
|
+
connectBusToState(app);
|
|
57
|
+
setupRenderKit(app, document);
|
|
58
|
+
setupHistory(app);
|
|
59
|
+
setupNavigation(app);
|
|
60
|
+
triggerRoute(app);
|
|
61
|
+
addRender(app);
|
|
62
|
+
|
|
63
|
+
return app;
|
|
64
|
+
};
|
package/src/debugging.js
ADDED
package/src/jaxs.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as jsx } from './jsx'
|
|
2
|
+
export { render } from './rendering/templates/root'
|
|
3
|
+
export { bind } from './rendering/templates/bound'
|
|
4
|
+
export { createBus } from './messageBus'
|
|
5
|
+
export { createApp } from './app'
|
|
6
|
+
export { State } from './state'
|
|
7
|
+
export * as navigation from './navigation'
|
|
8
|
+
export { views } from './views'
|
package/src/jsx.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Tag } from './rendering/templates/tag'
|
|
2
|
+
import { Children } from './rendering/templates/children'
|
|
3
|
+
|
|
4
|
+
const ensureChildrenArray = (maybeChildren, attributes) =>
|
|
5
|
+
maybeChildren || attributes.children || []
|
|
6
|
+
|
|
7
|
+
const packageAttributes = (maybeAttributes, maybeChildren) => {
|
|
8
|
+
const attributes = maybeAttributes || {}
|
|
9
|
+
const children = ensureChildrenArray(maybeChildren, attributes)
|
|
10
|
+
attributes.children = attributes.children || children
|
|
11
|
+
return attributes
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const jsx = (type, attributes, ...children) => {
|
|
15
|
+
if (typeof type === 'string') {
|
|
16
|
+
return new Tag(type, attributes, children)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return type(packageAttributes(attributes, children))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
jsx.fragment = (attributes, maybeChildren) => {
|
|
23
|
+
const children = ensureChildrenArray(maybeChildren, attributes)
|
|
24
|
+
return new Children(children)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default jsx
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BusEventName,
|
|
3
|
+
BusListener,
|
|
4
|
+
BusListenersMap,
|
|
5
|
+
BusOptions,
|
|
6
|
+
BusPayload,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
export class MessageBus {
|
|
10
|
+
listeners: BusListenersMap;
|
|
11
|
+
options: BusOptions;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.options = {};
|
|
15
|
+
this.listeners = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
subscribe(eventName: BusEventName, listener: BusListener) {
|
|
19
|
+
this.ensureListenerCollection(eventName);
|
|
20
|
+
|
|
21
|
+
this.listeners[eventName].push(listener);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
publish(eventName: BusEventName, payload: BusPayload) {
|
|
25
|
+
const listeners = this.listeners[eventName];
|
|
26
|
+
if (!listeners) return false;
|
|
27
|
+
|
|
28
|
+
listeners.forEach((listener: BusListener) => {
|
|
29
|
+
listener(
|
|
30
|
+
payload,
|
|
31
|
+
this.buildListenerKit(eventName),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ensureListenerCollection(eventName: BusEventName) {
|
|
39
|
+
if (this.listeners[eventName]) return;
|
|
40
|
+
|
|
41
|
+
this.listeners[eventName] = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
buildListenerKit(eventName: BusEventName) {
|
|
45
|
+
return {
|
|
46
|
+
eventName,
|
|
47
|
+
publish: this.publish.bind(this),
|
|
48
|
+
...this.options,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
addListenerOptions(options: BusOptions) {
|
|
53
|
+
this.options = {
|
|
54
|
+
...this.options,
|
|
55
|
+
...options,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const createBus = () => {
|
|
61
|
+
const bus = new MessageBus();
|
|
62
|
+
const publish = bus.publish.bind(bus);
|
|
63
|
+
const subscribe = bus.subscribe.bind(bus);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
bus,
|
|
67
|
+
publish,
|
|
68
|
+
subscribe,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createRouteState } from './routeState'
|
|
2
|
+
export const locationChangeEvent = 'locationChange'
|
|
3
|
+
export const routeChangeEvent = 'routeChange'
|
|
4
|
+
|
|
5
|
+
export const extractQueryParams = (queryString) => {
|
|
6
|
+
return queryString
|
|
7
|
+
.replace(/^\?/, '')
|
|
8
|
+
.split('&')
|
|
9
|
+
.reduce((aggregate, pairString) => {
|
|
10
|
+
if (!pairString) return aggregate
|
|
11
|
+
|
|
12
|
+
const pair = pairString.split('=')
|
|
13
|
+
aggregate[pair[0]] = pair[1]
|
|
14
|
+
return aggregate
|
|
15
|
+
}, {})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const onLocationChange = (_payload, { publish, state }) => {
|
|
19
|
+
const { host, pathname, search } = window.location
|
|
20
|
+
const path = pathname
|
|
21
|
+
const query = extractQueryParams(search)
|
|
22
|
+
|
|
23
|
+
state.route.update({
|
|
24
|
+
host,
|
|
25
|
+
path,
|
|
26
|
+
query
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// subscribe for fetching data and other things
|
|
30
|
+
publish(routeChangeEvent, { host, path, query })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const setupHistory = (app) => {
|
|
34
|
+
const { publish, subscribe, state } = app
|
|
35
|
+
createRouteState(state)
|
|
36
|
+
window.addEventListener('popstate', () => publish(locationChangeEvent))
|
|
37
|
+
subscribe(locationChangeEvent, onLocationChange)
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { findHref } from './findHref'
|
|
2
|
+
import { locationChangeEvent } from './setupHistory'
|
|
3
|
+
|
|
4
|
+
export const linkNavigationEvent = 'goToHref'
|
|
5
|
+
export const programmaticNavigationEvent = 'navigate'
|
|
6
|
+
|
|
7
|
+
export const navigate = (path, { publish }) => {
|
|
8
|
+
window.history.pushState(null, '', path)
|
|
9
|
+
publish(locationChangeEvent)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const onLinkClick = (domEvent, { publish }) => {
|
|
13
|
+
if (!domEvent || !domEvent.target) return
|
|
14
|
+
domEvent.preventDefault()
|
|
15
|
+
|
|
16
|
+
const href = findHref(domEvent.target)
|
|
17
|
+
navigate(href, { publish })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const setupNavigation = (app) => {
|
|
21
|
+
const { subscribe } = app
|
|
22
|
+
|
|
23
|
+
subscribe(linkNavigationEvent, onLinkClick)
|
|
24
|
+
subscribe(programmaticNavigationEvent, navigate)
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { compileChildren as compileChange } from './instructions/children';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ExpandedElement, Instructions } from '../../../types';
|
|
2
|
+
import { addAttribute, removeAttribute, updateAttribute } from './generate';
|
|
3
|
+
|
|
4
|
+
export const compileForAttributes = (
|
|
5
|
+
source: ExpandedElement,
|
|
6
|
+
target: ExpandedElement,
|
|
7
|
+
) => {
|
|
8
|
+
const instructions = [] as Instructions;
|
|
9
|
+
const sourceAttributes = source.attributes;
|
|
10
|
+
const sourceLength = sourceAttributes.length;
|
|
11
|
+
const targetAttributes = target.attributes;
|
|
12
|
+
const targetLength = targetAttributes.length;
|
|
13
|
+
|
|
14
|
+
// NOTE: this implementation sucks, but JSDOM doesn't properly implement
|
|
15
|
+
// .getNamedItem and the deno dom doesn't implement the .length attribute.
|
|
16
|
+
// It's a total test cluster fuck. When there are integration-y tests that
|
|
17
|
+
// I can use for rerender via the DOM, I will get rid of unit tests and
|
|
18
|
+
// improve the implementation. Sucks all around.
|
|
19
|
+
let index;
|
|
20
|
+
let innerIndex;
|
|
21
|
+
let matchingAttribute;
|
|
22
|
+
|
|
23
|
+
// iterate through the source attributes to find removals and updates
|
|
24
|
+
for (index = 0; index < sourceLength; index++) {
|
|
25
|
+
matchingAttribute = null;
|
|
26
|
+
const sourceAttribute = sourceAttributes.item(index);
|
|
27
|
+
if (!sourceAttribute) continue;
|
|
28
|
+
|
|
29
|
+
for (innerIndex = 0; innerIndex < targetLength; innerIndex++) {
|
|
30
|
+
const targetAttribute = targetAttributes.item(innerIndex);
|
|
31
|
+
if (!targetAttribute) continue;
|
|
32
|
+
if (sourceAttribute.name == targetAttribute.name) {
|
|
33
|
+
matchingAttribute = targetAttribute;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!matchingAttribute) {
|
|
39
|
+
instructions.push(
|
|
40
|
+
removeAttribute(source, target, { name: sourceAttribute.name }),
|
|
41
|
+
);
|
|
42
|
+
} else if (sourceAttribute.value !== matchingAttribute.value) {
|
|
43
|
+
instructions.push(
|
|
44
|
+
updateAttribute(source, target, {
|
|
45
|
+
name: sourceAttribute.name,
|
|
46
|
+
value: matchingAttribute.value,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// iterate through the target to find additions
|
|
53
|
+
for (index = 0; index < targetLength; index++) {
|
|
54
|
+
matchingAttribute = null;
|
|
55
|
+
const targetAttribute = targetAttributes.item(index);
|
|
56
|
+
if (!targetAttribute) continue;
|
|
57
|
+
|
|
58
|
+
for (innerIndex = 0; innerIndex < sourceLength; innerIndex++) {
|
|
59
|
+
const sourceAttribute = sourceAttributes.item(innerIndex);
|
|
60
|
+
if (!sourceAttribute) continue;
|
|
61
|
+
if (sourceAttribute.name == targetAttribute.name) {
|
|
62
|
+
matchingAttribute = sourceAttribute;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!matchingAttribute) {
|
|
68
|
+
instructions.push(
|
|
69
|
+
addAttribute(source, target, {
|
|
70
|
+
name: targetAttribute.name,
|
|
71
|
+
value: targetAttribute.value,
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return instructions;
|
|
78
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Dom,
|
|
3
|
+
ExpandedElement,
|
|
4
|
+
HtmlChildren,
|
|
5
|
+
Instruction,
|
|
6
|
+
Instructions,
|
|
7
|
+
} from '../../../types';
|
|
8
|
+
import { insertNode, removeNode, replaceNode } from './generate';
|
|
9
|
+
import { createIdMap } from './idMap';
|
|
10
|
+
import { compileForNodeGenerator } from './node';
|
|
11
|
+
import { debug } from '../../../debugging';
|
|
12
|
+
|
|
13
|
+
type DiffPair = {
|
|
14
|
+
source: Dom;
|
|
15
|
+
target: Dom;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const compileChildren = (
|
|
19
|
+
sourceList: HtmlChildren,
|
|
20
|
+
targetList: HtmlChildren,
|
|
21
|
+
parent: ExpandedElement,
|
|
22
|
+
) => {
|
|
23
|
+
const baseInstructions = [] as Instructions;
|
|
24
|
+
const length = largerLength(sourceList, targetList);
|
|
25
|
+
const sourceMap = createIdMap(sourceList);
|
|
26
|
+
const targetMap = createIdMap(targetList);
|
|
27
|
+
const nodesPairsToDiff = [] as DiffPair[];
|
|
28
|
+
|
|
29
|
+
let index = 0;
|
|
30
|
+
for (; index < length; index++) {
|
|
31
|
+
const source = sourceList[index] as ExpandedElement;
|
|
32
|
+
const target = targetList[index] as ExpandedElement;
|
|
33
|
+
debug(
|
|
34
|
+
'\n',
|
|
35
|
+
'loop index',
|
|
36
|
+
index,
|
|
37
|
+
source && source.__jsx,
|
|
38
|
+
target && target.__jsx,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// This algorithm uses the target as the source of truth, iterating
|
|
42
|
+
// through it first figuring out what to do. The length could be larger than
|
|
43
|
+
// the target length, which means that there are unresolved sources to remove.
|
|
44
|
+
// Part of the goal of this flow is to ensure that insertions happen in
|
|
45
|
+
// accending order.
|
|
46
|
+
if (target && targetMap.check(target)) {
|
|
47
|
+
debug('target', target.__jsx, 'index', index);
|
|
48
|
+
const matchingSource = sourceMap.pullMatch(target);
|
|
49
|
+
targetMap.clear(target); // mark target as resolved
|
|
50
|
+
|
|
51
|
+
if (matchingSource.element) {
|
|
52
|
+
debug('matching source found for target');
|
|
53
|
+
if (matchingSource.index !== index) {
|
|
54
|
+
// move source to index
|
|
55
|
+
debug('moving source', matchingSource.element.__jsx, index);
|
|
56
|
+
baseInstructions.push(
|
|
57
|
+
insertNode(matchingSource.element, { parent, index }),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
// update element for attribute, event and child changes
|
|
61
|
+
debug('updating to match target',
|
|
62
|
+
matchingSource.element.__jsx,
|
|
63
|
+
matchingSource.element.classList,
|
|
64
|
+
target.__jsx,
|
|
65
|
+
target.classList
|
|
66
|
+
);
|
|
67
|
+
nodesPairsToDiff.push({
|
|
68
|
+
source: matchingSource.element,
|
|
69
|
+
target,
|
|
70
|
+
});
|
|
71
|
+
} else if (source) {
|
|
72
|
+
debug('NO matching source for target but source in slot', source.__jsx);
|
|
73
|
+
|
|
74
|
+
if (targetMap.check(source)) {
|
|
75
|
+
// the source is somewhere else in the target, so just add this
|
|
76
|
+
// target element and assume the source will get resolved later.
|
|
77
|
+
debug('adding', target.__jsx, 'at', index);
|
|
78
|
+
baseInstructions.push(insertNode(target, { parent, index }));
|
|
79
|
+
} else {
|
|
80
|
+
// no matching target, but something is in the index/slot ... so swap
|
|
81
|
+
debug('replacing', source.__jsx, target.__jsx, 'at', index);
|
|
82
|
+
sourceMap.clear(source); // resolve source
|
|
83
|
+
baseInstructions.push(replaceNode(source as Dom, target as Dom));
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// extra targets, add these to the end of the parent in order received
|
|
87
|
+
debug('adding target to end', target.__jsx);
|
|
88
|
+
baseInstructions.push(insertNode(target, { parent, index }));
|
|
89
|
+
}
|
|
90
|
+
} else if (source) {
|
|
91
|
+
// stuff has been remove from the target
|
|
92
|
+
// check to see if source has been resolved in map
|
|
93
|
+
// if not remove from dom
|
|
94
|
+
const matchingSource = sourceMap.pullMatch(source);
|
|
95
|
+
if (matchingSource.element) {
|
|
96
|
+
debug('removing', source.__jsx);
|
|
97
|
+
baseInstructions.push(removeNode(source));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// deal with unresolved sources
|
|
103
|
+
sourceMap.remaining().forEach(({ element }) => {
|
|
104
|
+
debug('removing', element.__jsx);
|
|
105
|
+
baseInstructions.push(removeNode(element));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const nodeInstructions = nodesPairsToDiff
|
|
109
|
+
.reduce((collection, { source, target }) => {
|
|
110
|
+
return collection.concat(compileForNode(source, target));
|
|
111
|
+
}, [] as Instructions);
|
|
112
|
+
|
|
113
|
+
return baseInstructions.concat(nodeInstructions).sort(instructionsSorter);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const instructionsSorter = (left: Instruction, right: Instruction) => {
|
|
117
|
+
if (left.type > right.type) return 1;
|
|
118
|
+
if (left.type < right.type) return -1;
|
|
119
|
+
return 0;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const largerLength = (sourceList: HtmlChildren, targetList: HtmlChildren) => {
|
|
123
|
+
const sourceLength = Array.from(sourceList).length;
|
|
124
|
+
const targetLength = Array.from(targetList).length;
|
|
125
|
+
return sourceLength > targetLength ? sourceLength : targetLength;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const compileForNode = compileForNodeGenerator(compileChildren);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ExpandedElement, InputElement, Instructions } from '../../../types';
|
|
2
|
+
import { changeValue } from './generate';
|
|
3
|
+
import { compileForAttributes } from './attributes';
|
|
4
|
+
import { compileForEvents } from './events';
|
|
5
|
+
|
|
6
|
+
export const compileForElement = (
|
|
7
|
+
source: ExpandedElement,
|
|
8
|
+
target: ExpandedElement,
|
|
9
|
+
) => {
|
|
10
|
+
const attributeInstructions = compileForAttributes(source, target);
|
|
11
|
+
const eventInstructions = compileForEvents(source, target);
|
|
12
|
+
const valueInstructions = compileForInputValue(source, target);
|
|
13
|
+
|
|
14
|
+
return attributeInstructions
|
|
15
|
+
.concat(eventInstructions)
|
|
16
|
+
.concat(valueInstructions);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const compileForInputValue = (
|
|
20
|
+
sourceElement: ExpandedElement,
|
|
21
|
+
targetElement: ExpandedElement,
|
|
22
|
+
) => {
|
|
23
|
+
const instructions = [] as Instructions;
|
|
24
|
+
if (sourceElement.tagName !== 'INPUT') {
|
|
25
|
+
return instructions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const source = sourceElement as InputElement;
|
|
29
|
+
const target = targetElement as InputElement;
|
|
30
|
+
|
|
31
|
+
if (source.value !== target.value) {
|
|
32
|
+
instructions.push(
|
|
33
|
+
changeValue(
|
|
34
|
+
source,
|
|
35
|
+
target,
|
|
36
|
+
{ name: 'value', value: target.value },
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return instructions;
|
|
42
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ExpandedElement, Instructions } from '../../../types';
|
|
2
|
+
import { addEvent, removeEvent, updateEvent } from './generate';
|
|
3
|
+
|
|
4
|
+
export const compileForEvents = (
|
|
5
|
+
source: ExpandedElement,
|
|
6
|
+
target: ExpandedElement,
|
|
7
|
+
) => {
|
|
8
|
+
const instructions = [] as Instructions;
|
|
9
|
+
const sourceEventMaps = source.eventMaps;
|
|
10
|
+
const targetEventMaps = target.eventMaps;
|
|
11
|
+
const sourceDomEvents = Object.keys(sourceEventMaps);
|
|
12
|
+
const targetDomEvents = Object.keys(targetEventMaps);
|
|
13
|
+
|
|
14
|
+
sourceDomEvents.forEach((domEvent) => {
|
|
15
|
+
const sourceEventMap = sourceEventMaps[domEvent];
|
|
16
|
+
const targetEventMap = targetEventMaps[domEvent];
|
|
17
|
+
|
|
18
|
+
if (!targetEventMap) {
|
|
19
|
+
instructions.push(
|
|
20
|
+
removeEvent(source, target, {
|
|
21
|
+
name: sourceEventMap.domEvent,
|
|
22
|
+
value: sourceEventMap.listener,
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
} else if (targetEventMap.busEvent !== sourceEventMap.busEvent) {
|
|
26
|
+
instructions.push(
|
|
27
|
+
updateEvent(source, target, {
|
|
28
|
+
name: domEvent,
|
|
29
|
+
targetValue: targetEventMap.listener,
|
|
30
|
+
sourceValue: sourceEventMap.listener,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
} // else events the same
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
targetDomEvents.forEach((domEvent) => {
|
|
37
|
+
const sourceEventMap = sourceEventMaps[domEvent];
|
|
38
|
+
const targetEventMap = targetEventMaps[domEvent];
|
|
39
|
+
|
|
40
|
+
if (!sourceEventMap) {
|
|
41
|
+
instructions.push(
|
|
42
|
+
addEvent(source, target, {
|
|
43
|
+
name: targetEventMap.domEvent,
|
|
44
|
+
value: targetEventMap.listener,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return instructions;
|
|
51
|
+
};
|