@potok-web-framework/core 0.1.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/CHANGELOG.md +7 -0
- package/bun.lock +25 -0
- package/package.json +39 -0
- package/src/block.ts +102 -0
- package/src/bootstrap-app.ts +115 -0
- package/src/client-only.ts +17 -0
- package/src/constants.ts +27 -0
- package/src/context.ts +85 -0
- package/src/detect-child.ts +21 -0
- package/src/error-boundary.ts +51 -0
- package/src/exports/client.ts +1 -0
- package/src/exports/hmr.ts +1 -0
- package/src/exports/index.ts +21 -0
- package/src/exports/jsx-runtime.ts +4 -0
- package/src/exports/server.ts +1 -0
- package/src/fragment.ts +28 -0
- package/src/global.dev.d.ts +12 -0
- package/src/hmr/hmr-dev.ts +10 -0
- package/src/hmr/register-component.ts +109 -0
- package/src/hmr/registered-component.ts +59 -0
- package/src/hmr/registry.ts +78 -0
- package/src/hmr/types.ts +6 -0
- package/src/hmr/utils.ts +20 -0
- package/src/html-element.ts +95 -0
- package/src/jsx-types.ts +13 -0
- package/src/lazy.ts +44 -0
- package/src/lib-context-reader.ts +33 -0
- package/src/lib-scripts.ts +8 -0
- package/src/lifecycle.ts +44 -0
- package/src/list.ts +175 -0
- package/src/portal.ts +101 -0
- package/src/prop-types.ts +1165 -0
- package/src/ref.ts +11 -0
- package/src/render-to-dom.ts +325 -0
- package/src/render-to-string.ts +65 -0
- package/src/server-node.ts +98 -0
- package/src/show.ts +46 -0
- package/src/signals.ts +323 -0
- package/src/store.ts +68 -0
- package/src/text.ts +35 -0
- package/src/types.ts +69 -0
- package/src/utils.ts +118 -0
- package/tests/signals.test.ts +403 -0
- package/tsconfig.json +17 -0
- package/vite.config.ts +21 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { fragment } from '../fragment'
|
|
2
|
+
import { Lifecycle } from '../lifecycle'
|
|
3
|
+
import { LibContextReader } from '../lib-context-reader'
|
|
4
|
+
import { Show } from '../show'
|
|
5
|
+
import { createSignal } from '../signals'
|
|
6
|
+
import { Component, ComponentProps, LibContext, PotokElement } from '../types'
|
|
7
|
+
import { getComponentInstanceId, isWindowDefined } from './utils'
|
|
8
|
+
import { HMR_DEV } from './hmr-dev'
|
|
9
|
+
|
|
10
|
+
export function registerComponent<Props extends ComponentProps>(
|
|
11
|
+
component: (props: Props) => PotokElement,
|
|
12
|
+
moduleId: string,
|
|
13
|
+
componentId: string,
|
|
14
|
+
): Component<Props> {
|
|
15
|
+
if (!isWindowDefined()) {
|
|
16
|
+
return component
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const registeredComponent = HMR_DEV.registry.addComponent(
|
|
20
|
+
moduleId,
|
|
21
|
+
componentId,
|
|
22
|
+
component,
|
|
23
|
+
)
|
|
24
|
+
HMR_DEV.registry.notifyComponentUpdate(moduleId, componentId)
|
|
25
|
+
|
|
26
|
+
function renderComponent(
|
|
27
|
+
props: Props,
|
|
28
|
+
context: LibContext,
|
|
29
|
+
ignoreCache = false,
|
|
30
|
+
) {
|
|
31
|
+
const instanceId = getComponentInstanceId(context)
|
|
32
|
+
const cachedInstance = registeredComponent.getCachedInstance(instanceId)
|
|
33
|
+
|
|
34
|
+
if (!ignoreCache && cachedInstance) {
|
|
35
|
+
return cachedInstance.element
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
HMR_DEV.currentInstance = {
|
|
39
|
+
states: cachedInstance ? cachedInstance.states : [],
|
|
40
|
+
stateIndex: 0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const element = registeredComponent.component(props)
|
|
44
|
+
registeredComponent.addCachedInstance(
|
|
45
|
+
instanceId,
|
|
46
|
+
element,
|
|
47
|
+
HMR_DEV.currentInstance.states,
|
|
48
|
+
)
|
|
49
|
+
HMR_DEV.currentInstance = null
|
|
50
|
+
|
|
51
|
+
return element
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (props: Props) => {
|
|
55
|
+
return LibContextReader({
|
|
56
|
+
children(context) {
|
|
57
|
+
const value = createSignal({
|
|
58
|
+
element: renderComponent(props, context) as PotokElement | null,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const instanceId = getComponentInstanceId(context)
|
|
62
|
+
|
|
63
|
+
return Lifecycle({
|
|
64
|
+
onMounted() {
|
|
65
|
+
const unsubscribe = HMR_DEV.registry.subscribeToComponent(
|
|
66
|
+
moduleId,
|
|
67
|
+
componentId,
|
|
68
|
+
() => {
|
|
69
|
+
value.element = null
|
|
70
|
+
value.element = renderComponent(props, context, true)
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
registeredComponent.unmarkInstanceAsRemovable(instanceId)
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
unsubscribe()
|
|
78
|
+
|
|
79
|
+
registeredComponent.markInstanceAsRemovable(instanceId)
|
|
80
|
+
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
const cachedInstance =
|
|
83
|
+
registeredComponent.getCachedInstance(instanceId)
|
|
84
|
+
|
|
85
|
+
if (cachedInstance?.isRemovable) {
|
|
86
|
+
registeredComponent.removeCachedInstance(instanceId)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
children: [
|
|
92
|
+
Show({
|
|
93
|
+
get when() {
|
|
94
|
+
return value.element !== null
|
|
95
|
+
},
|
|
96
|
+
children: [
|
|
97
|
+
fragment({
|
|
98
|
+
get children() {
|
|
99
|
+
return [value.element!]
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Component, PotokElement } from '../types'
|
|
2
|
+
import { HMRSubscriber } from './types'
|
|
3
|
+
|
|
4
|
+
export type CachedInstanceState = {
|
|
5
|
+
current: unknown
|
|
6
|
+
initial: unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type CachedInstance = {
|
|
10
|
+
isRemovable: boolean
|
|
11
|
+
element: PotokElement
|
|
12
|
+
states: CachedInstanceState[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class RegisteredComponent {
|
|
16
|
+
component: Component<any>
|
|
17
|
+
subscribers: Set<HMRSubscriber>
|
|
18
|
+
private cachedInstances: Map<string, CachedInstance>
|
|
19
|
+
|
|
20
|
+
constructor(component: Component<any>) {
|
|
21
|
+
this.component = component
|
|
22
|
+
this.subscribers = new Set()
|
|
23
|
+
this.cachedInstances = new Map()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getCachedInstance(instanceId: string): CachedInstance | undefined {
|
|
27
|
+
return this.cachedInstances.get(instanceId)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addCachedInstance(
|
|
31
|
+
instanceId: string,
|
|
32
|
+
element: PotokElement,
|
|
33
|
+
states: CachedInstanceState[],
|
|
34
|
+
) {
|
|
35
|
+
this.cachedInstances.set(instanceId, {
|
|
36
|
+
isRemovable: false,
|
|
37
|
+
element,
|
|
38
|
+
states,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
markInstanceAsRemovable(instanceId: string) {
|
|
43
|
+
const cachedInstance = this.cachedInstances.get(instanceId)
|
|
44
|
+
if (cachedInstance) {
|
|
45
|
+
cachedInstance.isRemovable = true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
unmarkInstanceAsRemovable(instanceId: string) {
|
|
50
|
+
const cachedInstance = this.cachedInstances.get(instanceId)
|
|
51
|
+
if (cachedInstance) {
|
|
52
|
+
cachedInstance.isRemovable = false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
removeCachedInstance(instanceId: string) {
|
|
57
|
+
this.cachedInstances.delete(instanceId)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Component } from '../types'
|
|
2
|
+
import { ComponentId, HMRSubscriber, ModudeId } from './types'
|
|
3
|
+
import { RegisteredComponent } from './registered-component'
|
|
4
|
+
|
|
5
|
+
export class HMRRegistry {
|
|
6
|
+
private components: Map<ModudeId, Map<ComponentId, RegisteredComponent>> =
|
|
7
|
+
new Map()
|
|
8
|
+
|
|
9
|
+
private getModuleComponentsMap(moduleId: ModudeId) {
|
|
10
|
+
if (!this.components.has(moduleId)) {
|
|
11
|
+
this.components.set(moduleId, new Map())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return this.components.get(moduleId)!
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
addComponent(
|
|
18
|
+
moduleId: ModudeId,
|
|
19
|
+
componentId: ComponentId,
|
|
20
|
+
component: Component<any>,
|
|
21
|
+
): RegisteredComponent {
|
|
22
|
+
const moduleComponentsMap = this.getModuleComponentsMap(moduleId)
|
|
23
|
+
|
|
24
|
+
if (!moduleComponentsMap.has(componentId)) {
|
|
25
|
+
moduleComponentsMap.set(componentId, new RegisteredComponent(component))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const registeredComponent = moduleComponentsMap.get(componentId)!
|
|
29
|
+
|
|
30
|
+
registeredComponent.component = component
|
|
31
|
+
|
|
32
|
+
return registeredComponent
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getRegisteredComponent(
|
|
36
|
+
moduleId: ModudeId,
|
|
37
|
+
componentId: ComponentId,
|
|
38
|
+
): RegisteredComponent | undefined {
|
|
39
|
+
return this.getModuleComponentsMap(moduleId).get(componentId)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
subscribeToComponent(
|
|
43
|
+
moduleId: ModudeId,
|
|
44
|
+
componentId: ComponentId,
|
|
45
|
+
subscriber: HMRSubscriber,
|
|
46
|
+
) {
|
|
47
|
+
this.getRegisteredComponent(moduleId, componentId)?.subscribers.add(
|
|
48
|
+
subscriber,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
this.unsubscribeFromComponent(moduleId, componentId, subscriber)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
unsubscribeFromComponent(
|
|
57
|
+
moduleId: ModudeId,
|
|
58
|
+
componentId: ComponentId,
|
|
59
|
+
subscriber: HMRSubscriber,
|
|
60
|
+
) {
|
|
61
|
+
this.getRegisteredComponent(moduleId, componentId)?.subscribers.delete(
|
|
62
|
+
subscriber,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
notifyComponentUpdate(moduleId: ModudeId, componentId: ComponentId) {
|
|
67
|
+
const registeredComponent = this.getRegisteredComponent(
|
|
68
|
+
moduleId,
|
|
69
|
+
componentId,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (!registeredComponent) return
|
|
73
|
+
|
|
74
|
+
for (const subscriber of registeredComponent.subscribers) {
|
|
75
|
+
subscriber(registeredComponent)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/hmr/types.ts
ADDED
package/src/hmr/utils.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LibContext } from '../types'
|
|
2
|
+
|
|
3
|
+
export function isWindowDefined(): boolean {
|
|
4
|
+
return typeof window !== 'undefined'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getComponentInstanceId(context: LibContext): string {
|
|
8
|
+
const indexes: number[] = []
|
|
9
|
+
|
|
10
|
+
let currentContext: LibContext | null = context
|
|
11
|
+
|
|
12
|
+
while (currentContext) {
|
|
13
|
+
indexes.push(currentContext.index)
|
|
14
|
+
currentContext = currentContext.parentBlock
|
|
15
|
+
? currentContext.parentBlock.context
|
|
16
|
+
: null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return indexes.reverse().join('_')
|
|
20
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LibBlock } from './block'
|
|
2
|
+
import type { LibHTMLElementTagNameMap } from './prop-types'
|
|
3
|
+
import { createEffect } from './signals'
|
|
4
|
+
import type { LibContext, LibHtmlElementNode, PotokElement } from './types'
|
|
5
|
+
import {
|
|
6
|
+
extractListenersFromProps,
|
|
7
|
+
mergeContext,
|
|
8
|
+
normalizeChildren,
|
|
9
|
+
objectKeys,
|
|
10
|
+
} from './utils'
|
|
11
|
+
|
|
12
|
+
export type LibHTMLElementProps<Tag extends keyof LibHTMLElementTagNameMap> = {
|
|
13
|
+
tag: Tag
|
|
14
|
+
} & LibHTMLElementTagNameMap[Tag]
|
|
15
|
+
|
|
16
|
+
class LibHTMLElement<
|
|
17
|
+
Tag extends keyof LibHTMLElementTagNameMap
|
|
18
|
+
> extends LibBlock {
|
|
19
|
+
constructor(
|
|
20
|
+
readonly props: LibHTMLElementProps<Tag>,
|
|
21
|
+
readonly context: LibContext
|
|
22
|
+
) {
|
|
23
|
+
super()
|
|
24
|
+
|
|
25
|
+
const element: LibHtmlElementNode<Tag> = {
|
|
26
|
+
type: 'html-element',
|
|
27
|
+
context,
|
|
28
|
+
props,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.node = element
|
|
32
|
+
|
|
33
|
+
const descriptors = Object.getOwnPropertyDescriptors(props)
|
|
34
|
+
|
|
35
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
36
|
+
if (['tag', 'children', 'ref'].includes(key)) {
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (descriptor.value) {
|
|
41
|
+
context.updateHtmlElementNodeProp(element, key, descriptor.value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getter = descriptor.get
|
|
45
|
+
if (getter) {
|
|
46
|
+
this.addEffect(
|
|
47
|
+
createEffect(() => {
|
|
48
|
+
context.updateHtmlElementNodeProp(element, key, getter())
|
|
49
|
+
}, true)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (context.isHydrating) {
|
|
55
|
+
this.insertNode()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
normalizeChildren(props.children).forEach((childCreator, index) => {
|
|
59
|
+
this.children.push(
|
|
60
|
+
childCreator(
|
|
61
|
+
mergeContext(context, {
|
|
62
|
+
parentBlock: this,
|
|
63
|
+
index,
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (!context.isHydrating) {
|
|
70
|
+
this.insertNode()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override unmount() {
|
|
75
|
+
if (this.node) {
|
|
76
|
+
objectKeys(extractListenersFromProps(this.props)).forEach((key) => {
|
|
77
|
+
this.context.listeners[key]?.delete(this.node as unknown as HTMLElement)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.props.ref) {
|
|
82
|
+
this.props.ref.element = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
super.unmount()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function htmlElement<Tag extends keyof LibHTMLElementTagNameMap>(
|
|
90
|
+
props: LibHTMLElementProps<Tag>
|
|
91
|
+
): PotokElement {
|
|
92
|
+
return function (context: LibContext) {
|
|
93
|
+
return new LibHTMLElement(props, context)
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/jsx-types.ts
ADDED
package/src/lazy.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fragment } from './fragment'
|
|
2
|
+
import { LibContextReader } from './lib-context-reader'
|
|
3
|
+
import { Show } from './show'
|
|
4
|
+
import { createSignal } from './signals'
|
|
5
|
+
import { PotokElement } from './types'
|
|
6
|
+
|
|
7
|
+
export type LazyProps<Module> = {
|
|
8
|
+
import: () => Promise<Module>
|
|
9
|
+
resolve: (module: Module) => PotokElement
|
|
10
|
+
fallback?: PotokElement
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Lazy<Module>(props: LazyProps<Module>): PotokElement {
|
|
14
|
+
return LibContextReader({
|
|
15
|
+
children: (context) => {
|
|
16
|
+
const signal = createSignal({
|
|
17
|
+
block: null as PotokElement | null,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
context.promises.push(
|
|
21
|
+
props.import().then((module) => {
|
|
22
|
+
signal.block = props.resolve(module)
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return fragment({
|
|
27
|
+
children: [
|
|
28
|
+
Show({
|
|
29
|
+
get when() {
|
|
30
|
+
return signal.block !== null
|
|
31
|
+
},
|
|
32
|
+
children: [signal.block!],
|
|
33
|
+
}),
|
|
34
|
+
Show({
|
|
35
|
+
get when() {
|
|
36
|
+
return signal.block === null
|
|
37
|
+
},
|
|
38
|
+
children: [props.fallback],
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LibBlock } from './block'
|
|
2
|
+
import type { LibContext, MaybeArray, PotokElement } from './types'
|
|
3
|
+
import { mergeContext, normalizeArray } from './utils'
|
|
4
|
+
|
|
5
|
+
export type LibContextReaderProps = {
|
|
6
|
+
children: MaybeArray<(context: LibContext) => PotokElement>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class LibContextReaderClass extends LibBlock {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly props: LibContextReaderProps,
|
|
12
|
+
readonly context: LibContext
|
|
13
|
+
) {
|
|
14
|
+
super()
|
|
15
|
+
|
|
16
|
+
normalizeArray(props.children).forEach((childCreator, index) => {
|
|
17
|
+
this.children.push(
|
|
18
|
+
childCreator(context)(
|
|
19
|
+
mergeContext(context, {
|
|
20
|
+
parentBlock: this,
|
|
21
|
+
index,
|
|
22
|
+
})
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function LibContextReader(props: LibContextReaderProps): PotokElement {
|
|
30
|
+
return function (context: LibContext) {
|
|
31
|
+
return new LibContextReaderClass(props, context)
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LibBlock } from './block'
|
|
2
|
+
import type { LibContext, PotokElement, WithChildren } from './types'
|
|
3
|
+
import { mergeContext, normalizeChildren } from './utils'
|
|
4
|
+
|
|
5
|
+
type OnMoutedReturnCallback = () => void
|
|
6
|
+
|
|
7
|
+
export type LifecycleProps = WithChildren<{
|
|
8
|
+
onMounted?: () => OnMoutedReturnCallback | void
|
|
9
|
+
onUnmounted?: () => void
|
|
10
|
+
}>
|
|
11
|
+
|
|
12
|
+
class LifecycleClass extends LibBlock {
|
|
13
|
+
private onMountedReturn?: OnMoutedReturnCallback | void
|
|
14
|
+
|
|
15
|
+
constructor(readonly props: LifecycleProps, readonly context: LibContext) {
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
normalizeChildren(props.children).forEach((childCreator, index) => {
|
|
19
|
+
this.children.push(
|
|
20
|
+
childCreator(
|
|
21
|
+
mergeContext(context, {
|
|
22
|
+
parentBlock: this,
|
|
23
|
+
index,
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
this.onMountedReturn = props.onMounted?.()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override unmount() {
|
|
33
|
+
super.unmount()
|
|
34
|
+
|
|
35
|
+
this.onMountedReturn?.()
|
|
36
|
+
this.props.onUnmounted?.()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Lifecycle(props: LifecycleProps): PotokElement {
|
|
41
|
+
return function (context: LibContext) {
|
|
42
|
+
return new LifecycleClass(props, context)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/list.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { LibBlock } from './block'
|
|
2
|
+
import {
|
|
3
|
+
createEffect,
|
|
4
|
+
createSignal,
|
|
5
|
+
deepTrack,
|
|
6
|
+
getSignal,
|
|
7
|
+
isSignal,
|
|
8
|
+
untrack,
|
|
9
|
+
type Signal,
|
|
10
|
+
} from './signals'
|
|
11
|
+
import type { LibContext, PotokElement } from './types'
|
|
12
|
+
import { mergeContext } from './utils'
|
|
13
|
+
|
|
14
|
+
export type ListProps<Item> = {
|
|
15
|
+
items: Item[]
|
|
16
|
+
render: (each: { item: Item; index: number }) => PotokElement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ArrayDiff<Item> =
|
|
20
|
+
| {
|
|
21
|
+
type: 'delete'
|
|
22
|
+
index: number
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: 'add'
|
|
26
|
+
index: number
|
|
27
|
+
item: Item
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: 'replace'
|
|
31
|
+
index: number
|
|
32
|
+
oldItem: Item
|
|
33
|
+
newItem: Item
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ListClass<Item> extends LibBlock {
|
|
37
|
+
signals: Signal<{
|
|
38
|
+
item: Item
|
|
39
|
+
index: number
|
|
40
|
+
}>[] = []
|
|
41
|
+
|
|
42
|
+
constructor(readonly props: ListProps<Item>, readonly context: LibContext) {
|
|
43
|
+
super()
|
|
44
|
+
|
|
45
|
+
let prevItems: Item[] = []
|
|
46
|
+
|
|
47
|
+
this.addEffect(
|
|
48
|
+
createEffect(() => {
|
|
49
|
+
const items = props.items
|
|
50
|
+
|
|
51
|
+
if (isSignal(items)) {
|
|
52
|
+
deepTrack(items, 1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
untrack(() => {
|
|
56
|
+
const changes = this.diff(prevItems, items)
|
|
57
|
+
|
|
58
|
+
changes.forEach((change) => {
|
|
59
|
+
if (change.type === 'add') {
|
|
60
|
+
const signal = createSignal({
|
|
61
|
+
item: change.item,
|
|
62
|
+
index: change.index,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
this.children.splice(change.index, 0, null)
|
|
66
|
+
this.signals.splice(change.index, 0, getSignal(signal)!)
|
|
67
|
+
|
|
68
|
+
const creator = props.render(signal)
|
|
69
|
+
|
|
70
|
+
const block = creator(
|
|
71
|
+
mergeContext(context, {
|
|
72
|
+
parentBlock: this,
|
|
73
|
+
get index() {
|
|
74
|
+
return signal.index
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
this.children[change.index] = block
|
|
80
|
+
} else if (change.type === 'replace') {
|
|
81
|
+
this.signals[change.index]!.proxy.item = change.newItem
|
|
82
|
+
} else if (change.type === 'delete') {
|
|
83
|
+
const child = this.children[change.index]
|
|
84
|
+
this.children.splice(change.index, 1)
|
|
85
|
+
this.signals.splice(change.index, 1)
|
|
86
|
+
child?.unmount()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
this.signals.forEach((signal, index) => {
|
|
91
|
+
if (signal.value.index !== index) {
|
|
92
|
+
signal.proxy.index = index
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
prevItems = [...items]
|
|
97
|
+
})
|
|
98
|
+
}, true)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
diff(firstArray: Item[], secondArray: Item[]): ArrayDiff<Item>[] {
|
|
103
|
+
const paths: number[][] = []
|
|
104
|
+
|
|
105
|
+
for (let y = 0; y <= firstArray.length; y++) {
|
|
106
|
+
paths.push([y])
|
|
107
|
+
|
|
108
|
+
for (let x = 1; x <= secondArray.length; x++) {
|
|
109
|
+
if (y === 0) {
|
|
110
|
+
paths[y]!.push(x)
|
|
111
|
+
} else {
|
|
112
|
+
const top = paths[y - 1]![x]! + 1
|
|
113
|
+
const left = paths[y]![x - 1]! + 1
|
|
114
|
+
const topLeft =
|
|
115
|
+
paths[y - 1]![x - 1]! +
|
|
116
|
+
(firstArray[y - 1] !== secondArray[x - 1] ? 1 : 0)
|
|
117
|
+
|
|
118
|
+
paths[y]!.push(Math.min(top, left, topLeft))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const diffs: ArrayDiff<Item>[] = []
|
|
124
|
+
|
|
125
|
+
let x = secondArray.length
|
|
126
|
+
let y = firstArray.length
|
|
127
|
+
|
|
128
|
+
while (x > 0 || y > 0) {
|
|
129
|
+
const top = y > 0 ? paths[y - 1]![x]! : Infinity
|
|
130
|
+
const left = x > 0 ? paths[y]![x - 1]! : Infinity
|
|
131
|
+
const topLeft = y > 0 && x > 0 ? paths[y - 1]![x - 1]! : Infinity
|
|
132
|
+
|
|
133
|
+
const min = Math.min(top, left, topLeft)
|
|
134
|
+
|
|
135
|
+
if (min === topLeft) {
|
|
136
|
+
if (firstArray[y - 1] !== secondArray[x - 1]) {
|
|
137
|
+
diffs.push({
|
|
138
|
+
type: 'replace',
|
|
139
|
+
index: y - 1,
|
|
140
|
+
oldItem: firstArray[y - 1]!,
|
|
141
|
+
newItem: secondArray[x - 1]!,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
x--
|
|
145
|
+
y--
|
|
146
|
+
} else if (min === top) {
|
|
147
|
+
diffs.push({
|
|
148
|
+
type: 'delete',
|
|
149
|
+
index: y - 1,
|
|
150
|
+
})
|
|
151
|
+
y--
|
|
152
|
+
} else {
|
|
153
|
+
diffs.push({
|
|
154
|
+
type: 'add',
|
|
155
|
+
index: y,
|
|
156
|
+
item: secondArray[x - 1]!,
|
|
157
|
+
})
|
|
158
|
+
x--
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return diffs
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override unmount() {
|
|
166
|
+
super.unmount()
|
|
167
|
+
this.signals = []
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function List<Item>(props: ListProps<Item>): PotokElement {
|
|
172
|
+
return function (context: LibContext) {
|
|
173
|
+
return new ListClass(props, context)
|
|
174
|
+
}
|
|
175
|
+
}
|