Last active
January 23, 2025 18:45
-
-
Save bigmistqke/1bafd4bde945ca168409b598fe37b8b1 to your computer and use it in GitHub Desktop.
solid-vue
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// v-dom driven by signals | |
import { | |
createSignal, | |
createMemo, | |
} from "solid-js"; | |
interface TextNode { | |
text: string; | |
} | |
interface ElementNode { | |
tag: string; | |
props: Record<string, string>; | |
children: Array<ValidNode>; | |
} | |
interface ComponentNode { | |
tag: Component; | |
props: Record<string, string>; | |
children: Array<ValidNode>; | |
} | |
type VNode = ElementNode | ComponentNode; | |
type Component = (props: Record<string, any>) => () => VNode; | |
type ValidChild = VNode | string | number | Component; | |
type ValidNode = VNode | TextNode; | |
function isTextNode(node: any): node is TextNode { | |
return typeof node === "object" && "text" in node; | |
} | |
function isElementNode(node: any): node is ElementNode { | |
return typeof node === "object" && "tag" in node; | |
} | |
function isComponentNode(node: any): node is ComponentNode { | |
return ( | |
typeof node === "object" && "tag" in node && typeof node.tag === "function" | |
); | |
} | |
function h( | |
tag: string | Component, | |
props: Record<string, any>, | |
...children: Array<ValidChild> | |
) { | |
return { | |
tag, | |
props: Object.fromEntries( | |
Object.entries(props).map(([key, value]) => [ | |
key, | |
typeof value === "object" | |
? Object.entries(value) | |
.map(([key, value]) => `${key}: ${value}; `) | |
.join("") | |
: value, | |
]), | |
), | |
children: children.map((child) => | |
typeof child === "object" ? child : { text: child.toString() }, | |
), | |
} as VNode; | |
} | |
function vdom( | |
cb: (render: (node: VNode) => void) => Promise<void> | void, | |
parent = document.body, | |
) { | |
let previous: ElementNode; | |
const map = new WeakMap<ValidNode, HTMLElement | Text>(); | |
function traverse({ | |
node, | |
previous, | |
sibling, | |
parent, | |
}: { | |
node: ValidNode; | |
previous?: ValidNode; | |
sibling?: HTMLElement | Text; | |
parent: HTMLElement; | |
}): HTMLElement | Text { | |
if (node === previous) { | |
const element = map.get(previous)!; | |
map.set(node, element); | |
return element; | |
} | |
const shouldUnmount = | |
previous && | |
("text" in node || "text" in previous || node.tag !== previous.tag); | |
const shouldMount = !previous || shouldUnmount; | |
if (shouldUnmount) { | |
parent.removeChild(map.get(previous)!); | |
} | |
if (isComponentNode(node)) { | |
let previousNode: VNode; | |
const currentTemplate = node.tag?.({ | |
...node.props, | |
children: node.children, | |
}); | |
const draw = createMemo(() => { | |
const node = currentTemplate(); | |
const element = traverse({ | |
node, | |
previous: previousNode, | |
parent, | |
sibling, | |
}); | |
previousNode = node; | |
return element; | |
}); | |
return draw(); | |
} | |
const element = shouldMount | |
? isTextNode(node) | |
? document.createTextNode(node.text.toString()) | |
: document.createElement(node.tag) | |
: map.get(previous)!; | |
map.set(node, element); | |
if (shouldMount) { | |
if (sibling) { | |
sibling.after(element); | |
} else { | |
parent.prepend(element); | |
} | |
} | |
if (isElementNode(node)) { | |
if (!(element instanceof HTMLElement)) { | |
throw `Element of ElementNode should be instance of HTMLElement`; | |
} | |
Object.entries(node.props).forEach(([key, value]) => { | |
if ( | |
!shouldUnmount && | |
isElementNode(previous) && | |
value === previous.props[key] | |
) { | |
return; | |
} | |
element.setAttribute(key, value); | |
}); | |
let sibling: HTMLElement | Text; | |
node.children.forEach((child, index) => { | |
sibling = traverse({ | |
node: child, | |
previous: | |
!shouldUnmount && isElementNode(previous) | |
? previous.children[index] | |
: undefined, | |
parent: element, | |
sibling, | |
}); | |
}); | |
if ( | |
isElementNode(previous) && | |
previous.children.length > node.children.length | |
) { | |
previous.children.slice(node.children.length).forEach((child) => { | |
element.removeChild(map.get(child)!); | |
}); | |
} | |
} | |
return element; | |
} | |
cb((node) => { | |
traverse({ node, previous, parent }); | |
previous = node; | |
}); | |
} | |
// USAGE | |
const Button: Component = (props) => { | |
const [title, set] = createSignal(props.title); | |
setTimeout(() => set((title) => title + title[title.length - 1]), 1000); | |
return () => h("button", {}, title() ?? props.title, ...props.children); | |
}; | |
document.body.innerHTML = ""; | |
vdom(async (render) => { | |
render( | |
h( | |
"div", | |
{}, | |
"whatever", | |
"forever", | |
h( | |
Button, | |
{ title: "yolo", style: { border: "1px solid red" } }, | |
h("h1", { style: { color: "red" } }, "hallo"), | |
), | |
"ok", | |
h( | |
Button, | |
{ title: "yeeehaaa", style: { border: "1px solid red" } }, | |
h("h1", { style: { color: "red" } }, "hallo"), | |
), | |
), | |
); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment