diff, renderIntoRealDom, dynamic count of elements rendering (without 'key'), useState hook only
This commit is contained in:
parent
110906370b
commit
5ca8f7e1c0
@ -1,24 +1,42 @@
|
|||||||
import {useState} from "../keact/hooks";
|
import {useState} from "../keact/hooks";
|
||||||
import {Toggle} from "./Toggle";
|
import {Toggle} from "./Toggle";
|
||||||
|
import {CheckEmptyFragment} from "./CheckEmptyFragment";
|
||||||
|
import {ChooseBg} from "./ChooseBg";
|
||||||
|
import {TextComponent} from "./TextComponent";
|
||||||
|
import {NamesList} from "./namesList";
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
let [clicked, setClicked] = useState(0);
|
// let [clicked, setClicked] = useState(0);
|
||||||
const text = "Hello world!";
|
// const text = "Hello world!";
|
||||||
function clickHandler() {
|
// function clickHandler() {
|
||||||
const newClicked = +clicked + 1;
|
// const newClicked = +clicked + 1;
|
||||||
setClicked(newClicked);
|
// setClicked(newClicked);
|
||||||
console.log(`clicked ${newClicked}-th time`);
|
// console.log(`clicked ${newClicked}-th time`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<NamesList/>
|
||||||
|
</div>
|
||||||
|
// let [isSwap, setIsSwap] = useState(false);
|
||||||
|
// return <div>
|
||||||
|
// <div>
|
||||||
|
// <span>swapped: </span><span>{isSwap ? 'true' : 'false'}</span>
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <button onClick={() => {setIsSwap(!isSwap)}}>swap</button>
|
||||||
|
// </div>
|
||||||
|
// {isSwap ? [<Toggle/>, <TextComponent/>] : [<TextComponent/>, <Toggle/>]}
|
||||||
|
// </div>
|
||||||
// return <div>
|
// return <div>
|
||||||
// {text}
|
// {text}
|
||||||
// </div>
|
// </div>
|
||||||
return <div>
|
// return <div>
|
||||||
<div className="text_holder"
|
// <div className="text_holder"
|
||||||
onClick={clickHandler}
|
// onClick={clickHandler}
|
||||||
>
|
// >
|
||||||
<span>{text}</span>
|
// <span>{text}</span>
|
||||||
<span>clicked {clicked} times</span>
|
// <span>clicked {clicked} times</span>
|
||||||
</div>
|
// </div>
|
||||||
<Toggle clicked={clicked}/>
|
// <Toggle clicked={clicked}/>
|
||||||
</div>
|
// </div>
|
||||||
}
|
}
|
||||||
15
src/components/CheckEmptyFragment.tsx
Normal file
15
src/components/CheckEmptyFragment.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {useState} from "../keact/hooks";
|
||||||
|
|
||||||
|
export function CheckEmptyFragment(props) {
|
||||||
|
// return <></>
|
||||||
|
// const [isShow, setIsShow] = useState(false)
|
||||||
|
// function toggleIsShow() {
|
||||||
|
// setIsShow(!isShow);
|
||||||
|
// }
|
||||||
|
|
||||||
|
debugger;
|
||||||
|
if (props.isShow) {
|
||||||
|
return <span>component will return false</span>
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@ -1,13 +1,22 @@
|
|||||||
import {useState} from "../keact/hooks";
|
import {useState} from "../keact/hooks";
|
||||||
|
|
||||||
export function ChooseBg() {
|
export function ChooseBg() {
|
||||||
const [bgColor, setBgColor] = useState('#ff0000');
|
const [bgColor, setBgColor] = useState('#ffffff');
|
||||||
|
document.body.style.background = '#ffffff';
|
||||||
|
|
||||||
|
function setNewBgColor(newColor: string) {
|
||||||
|
setBgColor(newColor);
|
||||||
|
document.body.style.background = newColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetBgColor() {
|
||||||
|
setNewBgColor('#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
function changeBgColor(e: any) {
|
function changeBgColor(e: any) {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if ([4, 7].includes(value.length)) {
|
if ([4, 7].includes(value.length)) {
|
||||||
setBgColor(e.target.value);
|
setNewBgColor(value);
|
||||||
document.body.style.background = e.target.value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,8 +25,10 @@ export function ChooseBg() {
|
|||||||
<span>input your own bg color:</span>
|
<span>input your own bg color:</span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
placeholder="#ff0000"
|
placeholder="#ff0000"
|
||||||
|
style={{color: bgColor !== '#ffffff' ? bgColor : '#000000'}}
|
||||||
onInput={changeBgColor}
|
onInput={changeBgColor}
|
||||||
/>
|
/>
|
||||||
|
{bgColor === '#ffffff' ? [<span>now it's </span>, <span style={{background: '#000', color: '#fff'}}>white</span>] : <button onClick={resetBgColor}>reset</button>}
|
||||||
</div>
|
</div>
|
||||||
<span>current bg color is {bgColor}</span>
|
<span>current bg color is {bgColor}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
3
src/components/TextComponent.tsx
Normal file
3
src/components/TextComponent.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function TextComponent() {
|
||||||
|
return "simple text component"
|
||||||
|
}
|
||||||
@ -7,11 +7,6 @@ export const Toggle = (props) => {
|
|||||||
function toggle() {
|
function toggle() {
|
||||||
const newVal = !isOn;
|
const newVal = !isOn;
|
||||||
setIsOn(newVal);
|
setIsOn(newVal);
|
||||||
if (newVal) {
|
|
||||||
document.body.style.background = '#ff0000';
|
|
||||||
} else {
|
|
||||||
document.body.style.background = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return <div>
|
return <div>
|
||||||
{
|
{
|
||||||
|
|||||||
48
src/components/namesList.tsx
Normal file
48
src/components/namesList.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {useState} from "../keact/hooks";
|
||||||
|
|
||||||
|
export function NamesList() {
|
||||||
|
const [namesList, setNamesList] = useState([
|
||||||
|
"Greg",
|
||||||
|
"Elon",
|
||||||
|
"Mark",
|
||||||
|
"Olaph"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [newName, setNewName] = useState("");
|
||||||
|
|
||||||
|
function addNewName() {
|
||||||
|
namesList.push(newName);
|
||||||
|
setNewName("");
|
||||||
|
setNamesList(namesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteName(index: number) {
|
||||||
|
namesList.splice(index, 1);
|
||||||
|
setNamesList(namesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<h1>Names list:</h1>
|
||||||
|
<div>
|
||||||
|
{namesList.map((n, i) =>
|
||||||
|
<div>
|
||||||
|
<span>#{i + 1} - {n}</span><button onClick={deleteName.bind(this, i)}>x</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>add to list:</h2>
|
||||||
|
<input type="text"
|
||||||
|
placeholder="Elon"
|
||||||
|
value={newName}
|
||||||
|
onInput={(e) => {
|
||||||
|
//@ts-ignore
|
||||||
|
setNewName(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={!newName}
|
||||||
|
onClick={addNewName}>add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import {reRenderElement, VirtualElementContext} from "./keact";
|
import {ComponentContext, reRenderComponent} from "./keact-dom";
|
||||||
|
|
||||||
let currentInstance: VirtualElementContext;
|
let currentInstance: ComponentContext;
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
|
|
||||||
export function useState(initial: any): [any, Function] {
|
export function useState(initial: any): [any, Function] {
|
||||||
@ -11,18 +11,18 @@ export function useState(initial: any): [any, Function] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const setFunc = setState.bind(this, currentInstance, cursor);
|
const setFunc = setState.bind(this, currentInstance, cursor);
|
||||||
state.set(cursor, initial)
|
|
||||||
setStateFuncs.set(cursor, setFunc)
|
setStateFuncs.set(cursor, setFunc)
|
||||||
|
state.set(cursor, initial)
|
||||||
cursor++;
|
cursor++;
|
||||||
return [initial, setFunc];
|
return [initial, setFunc];
|
||||||
}
|
}
|
||||||
|
|
||||||
function setState(currentInstance: VirtualElementContext, cursor: number, newValue: any) {
|
function setState(currentInstance: ComponentContext, cursor: number, newValue: any) {
|
||||||
currentInstance.state.set(cursor, newValue);
|
currentInstance.state.set(cursor, newValue);
|
||||||
reRenderElement(currentInstance);
|
reRenderComponent(currentInstance.element);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setContext = (context: VirtualElementContext) => {
|
export const setContext = (context: ComponentContext) => {
|
||||||
currentInstance = context;
|
currentInstance = context;
|
||||||
cursor = 0;
|
cursor = 0;
|
||||||
}
|
}
|
||||||
@ -1,9 +1,285 @@
|
|||||||
import {VirtualElement} from "./keact";
|
import {
|
||||||
|
createElement, FC,
|
||||||
|
Props, resolveChildrenIntoVirtualElements,
|
||||||
|
VirtualElement,
|
||||||
|
VirtualElementComponent,
|
||||||
|
VirtualElementEmpty,
|
||||||
|
VirtualElementFragment,
|
||||||
|
VirtualElementTag,
|
||||||
|
VirtualElementText
|
||||||
|
} from "./keact";
|
||||||
|
import {setContext} from "./hooks";
|
||||||
|
|
||||||
|
const childrenByHtml: Map<HTMLElement, { children: VDOMElement[] }> = new Map()
|
||||||
|
|
||||||
|
export type ComponentContext = {
|
||||||
|
state: Map<number, any>,
|
||||||
|
setStateFuncs: Map<number, Function>
|
||||||
|
element: VDOMComponent,
|
||||||
|
};
|
||||||
|
export type VDOMElement = VDOMComponent | VDOMElementTag | VDOMElementSimple | VDOMElementFragment;
|
||||||
|
export type VDOMComponent = VirtualElementComponent & {
|
||||||
|
//TODO: delete context, use instance as key in map
|
||||||
|
context: ComponentContext;
|
||||||
|
childrenDomElements: VDOMElement[];
|
||||||
|
parent: HTMLElement;
|
||||||
|
};
|
||||||
|
export type VDOMElementTag = VirtualElementTag & {
|
||||||
|
childrenDomElements: VDOMElement[];
|
||||||
|
node: HTMLElement,
|
||||||
|
parent: HTMLElement;
|
||||||
|
funcProps: Record<string, Function>;
|
||||||
|
}
|
||||||
|
export type VDOMElementFragment = VirtualElementFragment & {
|
||||||
|
childrenDomElements: VDOMElement[];
|
||||||
|
parent: HTMLElement;
|
||||||
|
}
|
||||||
|
export type VDOMElementSimple = (VirtualElementText | VirtualElementEmpty) & {
|
||||||
|
node: Node,
|
||||||
|
}
|
||||||
|
|
||||||
|
function runComponentsFunction(generate: FC, props: Props): VirtualElement[] {
|
||||||
|
const result = generate(props);
|
||||||
|
if (typeof result === 'object') {
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
const toReturn = [];
|
||||||
|
for (const c of result) {
|
||||||
|
if (typeof c === 'object') {
|
||||||
|
toReturn.push(c);
|
||||||
|
} else {
|
||||||
|
toReturn.push(...resolveChildrenIntoVirtualElements(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [result];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolveChildrenIntoVirtualElements(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reRenderComponent(component: VDOMComponent) {
|
||||||
|
setContext(component.context)
|
||||||
|
const newChildren = runComponentsFunction(component.generate, component.props);
|
||||||
|
|
||||||
|
diff(component.childrenDomElements, newChildren, component.parent);
|
||||||
|
}
|
||||||
|
|
||||||
function render($element: VirtualElement | undefined, root: HTMLElement) {
|
function render($element: VirtualElement | undefined, root: HTMLElement) {
|
||||||
if (!$element) return;
|
if (!$element) return;
|
||||||
|
|
||||||
root.append($element.node);
|
if (!childrenByHtml.has(root)) {
|
||||||
|
childrenByHtml.set(root, {
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children } = childrenByHtml.get(root);
|
||||||
|
diff(children, [$element], root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function diff(oldChildren: VDOMElement[], newChildren: VirtualElement[], parentNode: HTMLElement) {
|
||||||
|
for (let i = 0, length = Math.max(newChildren.length, oldChildren.length); i < length; i++) {
|
||||||
|
const oldChild = oldChildren[i] as VDOMElement | undefined;
|
||||||
|
const newChild = newChildren[i] as VirtualElement | undefined;
|
||||||
|
if (!newChild && !oldChild) continue;
|
||||||
|
if (newChild && !oldChild) {
|
||||||
|
oldChildren[i] = renderIntoRealDom(newChild, parentNode);
|
||||||
|
continue;
|
||||||
|
} else if (!newChild && oldChild) {
|
||||||
|
removeFromDom(oldChild);
|
||||||
|
oldChildren[i] = undefined;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (oldChild.type === 'empty' && newChild.type === 'empty') {
|
||||||
|
continue;
|
||||||
|
} else if (oldChild.type === 'text' && newChild.type === 'text') {
|
||||||
|
if (oldChild.text !== newChild.text) {
|
||||||
|
console.warn(`updateText in real dom ${oldChild.text} -> ${newChild.text}`);
|
||||||
|
oldChild.node.textContent = newChild.text;
|
||||||
|
oldChild.text = newChild.text;
|
||||||
|
}
|
||||||
|
} else if (oldChild.type === 'component' && newChild.type === 'component') {
|
||||||
|
if (oldChild.generate !== newChild.generate) {
|
||||||
|
let obj: VDOMElement = oldChild;
|
||||||
|
while(obj.type === "fragment" || obj.type === 'component') {
|
||||||
|
obj = obj.childrenDomElements[0];
|
||||||
|
}
|
||||||
|
const insertBefore = obj.node;
|
||||||
|
|
||||||
|
const newComponentDom = renderIntoRealDom(newChild, oldChild.parent, insertBefore)
|
||||||
|
removeFromDom(oldChild)
|
||||||
|
oldChildren[i] = newComponentDom;
|
||||||
|
}
|
||||||
|
else if (JSON.stringify(oldChild.props) !== JSON.stringify(newChild.props)) {
|
||||||
|
setContext(oldChild.context);
|
||||||
|
const newChildren = runComponentsFunction(newChild.generate, newChild.props)
|
||||||
|
|
||||||
|
oldChild.props = newChild.props;
|
||||||
|
diff(oldChild.childrenDomElements, newChildren, oldChild.parent)
|
||||||
|
}
|
||||||
|
} else if (oldChild.type === 'fragment' && newChild.type === 'fragment') {
|
||||||
|
// let obj: VDOMElement = oldChild.childrenDomElements[0];
|
||||||
|
// while(obj.type === "fragment" || obj.type === 'component') {
|
||||||
|
// obj = obj.childrenDomElements[0];
|
||||||
|
// }
|
||||||
|
// const insertBefore = obj.node;
|
||||||
|
diff(oldChild.childrenDomElements, newChild.children, oldChild.parent);
|
||||||
|
} else if (oldChild.type === 'tag' && newChild.type === 'tag') {
|
||||||
|
if (oldChild.tag !== newChild.tag) {
|
||||||
|
const newVDOMTag = renderIntoRealDom(newChild, oldChild.parent, oldChild.node) as VDOMElementTag;
|
||||||
|
removeFromDom(oldChild);
|
||||||
|
oldChildren[i] = newVDOMTag;
|
||||||
|
} else if (JSON.stringify(oldChild.props) !== JSON.stringify(newChild.props)) {
|
||||||
|
clearPropsFromHtmlElement(oldChild.node, oldChild.props)
|
||||||
|
applyPropsToHtmlElement(oldChild.node, newChild.props, oldChild.funcProps);
|
||||||
|
oldChild.props = newChild.props;
|
||||||
|
} else {
|
||||||
|
updateFuncObjFromProps(newChild.props, oldChild.funcProps)
|
||||||
|
diff(oldChild.childrenDomElements, newChild.children, oldChild.node);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let obj: VDOMElement = oldChild;
|
||||||
|
while(obj.type === "fragment" || obj.type === 'component') {
|
||||||
|
obj = obj.childrenDomElements[0];
|
||||||
|
}
|
||||||
|
const insertBefore = obj.node;
|
||||||
|
oldChildren[i] = renderIntoRealDom(newChild, insertBefore.parentElement, insertBefore);
|
||||||
|
removeFromDom(oldChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFuncObjFromProps(props: Props, funcObj: Record<string, Function>) {
|
||||||
|
for (const [propKey, propVal] of Object.entries(props)) {
|
||||||
|
if (typeof propVal === 'function') {
|
||||||
|
funcObj[propKey] = propVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPropsFromHtmlElement(el: HTMLElement, props: Props) {
|
||||||
|
for (const [propKey, propVal] of Object.entries(props)) {
|
||||||
|
if (propKey === 'children') continue;
|
||||||
|
if (typeof propVal === 'function') {
|
||||||
|
el[propKey.toLowerCase()] = undefined;
|
||||||
|
} else {
|
||||||
|
el[propKey] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPropsToHtmlElement(el: HTMLElement, props: Props, funcPropsObj: Record<string, Function>){
|
||||||
|
for (const [propKey, propVal] of Object.entries(props)) {
|
||||||
|
if (propKey === 'children') continue;
|
||||||
|
if (typeof propVal === 'function') {
|
||||||
|
funcPropsObj[propKey] = propVal;
|
||||||
|
el[propKey.toLowerCase()] = (...args: any[]) => {funcPropsObj[propKey](...args)};
|
||||||
|
} else if (typeof propVal === 'object' && el[propKey]) {
|
||||||
|
Object.assign(el[propKey], propVal);
|
||||||
|
} else {
|
||||||
|
el[propKey] = propVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIntoRealDom(el: VirtualElement, parent: HTMLElement, insertBefore?: Node): VDOMElement {
|
||||||
|
if (el.type === 'empty') {
|
||||||
|
const node = document.createTextNode(" ")
|
||||||
|
appendOrReplace(node, parent, insertBefore);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
...el,
|
||||||
|
};
|
||||||
|
} else if (el.type === 'text') {
|
||||||
|
const node = document.createTextNode(el.text)
|
||||||
|
appendOrReplace(node, parent, insertBefore);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
...el,
|
||||||
|
};
|
||||||
|
} else if (el.type === 'tag') {
|
||||||
|
const node = document.createElement(el.tag);
|
||||||
|
const newChildren: VDOMElement[] = [];
|
||||||
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
|
newChildren.push(renderIntoRealDom(el.children[i], node));
|
||||||
|
}
|
||||||
|
|
||||||
|
const funcProps: Record<string, Function> = {};
|
||||||
|
applyPropsToHtmlElement(node, el.props, funcProps)
|
||||||
|
appendOrReplace(node, parent, insertBefore);
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
parent,
|
||||||
|
childrenDomElements: newChildren,
|
||||||
|
funcProps,
|
||||||
|
...el,
|
||||||
|
};
|
||||||
|
} else if (el.type === 'component') {
|
||||||
|
const component: VDOMComponent = {
|
||||||
|
context: {
|
||||||
|
state: new Map(),
|
||||||
|
setStateFuncs: new Map(),
|
||||||
|
element: null,
|
||||||
|
},
|
||||||
|
parent,
|
||||||
|
childrenDomElements: [],
|
||||||
|
// parent,
|
||||||
|
...el
|
||||||
|
};
|
||||||
|
component.context.element = component;
|
||||||
|
setContext(component.context);
|
||||||
|
const children = runComponentsFunction(el.generate, el.props)
|
||||||
|
component.childrenDomElements = [];
|
||||||
|
for (const c of children) {
|
||||||
|
component.childrenDomElements.push(renderIntoRealDom(c, parent));
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
} else if (el.type === 'fragment') {
|
||||||
|
const node = document.createDocumentFragment();
|
||||||
|
const newChildren: VDOMElement[] = [];
|
||||||
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
|
newChildren.push(renderIntoRealDom(el.children[i], node as undefined as HTMLElement));
|
||||||
|
}
|
||||||
|
appendOrReplace(node, parent, insertBefore);
|
||||||
|
newChildren.forEach(child => {
|
||||||
|
if ('parent' in child) {
|
||||||
|
child.parent = parent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
parent,
|
||||||
|
childrenDomElements: newChildren,
|
||||||
|
...el,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromDom(el: VDOMElement) {
|
||||||
|
console.warn('removeFromDom:', el);
|
||||||
|
if (el.type === "fragment" || el.type === 'component') {
|
||||||
|
const stack = [el.childrenDomElements];
|
||||||
|
while(stack.length) {
|
||||||
|
stack.pop().forEach(c => {
|
||||||
|
if (c.type === "fragment" || c.type === 'component') {
|
||||||
|
stack.push(c.childrenDomElements);
|
||||||
|
} else {
|
||||||
|
c.node.parentElement.removeChild(c.node);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
el.node.parentElement.removeChild(el.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOrReplace(node: Node, parent: HTMLElement | DocumentFragment, insertBefore?: Node) {
|
||||||
|
console.warn('appendOrReplace:', node);
|
||||||
|
if (insertBefore) {
|
||||||
|
parent.insertBefore(node, insertBefore);
|
||||||
|
} else {
|
||||||
|
parent.append(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeactDOM = {
|
const KeactDOM = {
|
||||||
|
|||||||
@ -1,238 +1,103 @@
|
|||||||
import {setContext} from "./hooks";
|
|
||||||
import {Fragment} from "./jsx-runtime";
|
import {Fragment} from "./jsx-runtime";
|
||||||
|
|
||||||
export type Props = Record<string, any>;
|
export type Props = Record<string, any>;
|
||||||
export type VirtualElementContext = {
|
|
||||||
state: Map<number, any>,
|
|
||||||
setStateFuncs: Map<number, Function>
|
|
||||||
element: any,
|
|
||||||
}
|
|
||||||
export type VirtualElement = VirtualElementTag | VirtualElementText | VirtualElementComponent | VirtualElementEmpty | VirtualElementFragment;
|
export type VirtualElement = VirtualElementTag | VirtualElementText | VirtualElementComponent | VirtualElementEmpty | VirtualElementFragment;
|
||||||
|
|
||||||
export type VirtualElementComponent = {
|
export type VirtualElementComponent = {
|
||||||
type: 'component',
|
type: 'component',
|
||||||
node: Node,
|
props: Props,
|
||||||
context: VirtualElementContext,
|
|
||||||
generate: FC,
|
generate: FC,
|
||||||
children: VirtualElement[],
|
name: string,
|
||||||
|
instance: {
|
||||||
|
element: VirtualElementComponent;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export type VirtualElementTag = {
|
export type VirtualElementTag = {
|
||||||
node: HTMLElement;
|
|
||||||
type: 'tag';
|
type: 'tag';
|
||||||
tag: string; //html tag
|
tag: string; //html tag
|
||||||
|
props: Props;
|
||||||
children: VirtualElement[];
|
children: VirtualElement[];
|
||||||
otherProps: [string, any][];
|
|
||||||
propsFunctions: [string, Function][];
|
|
||||||
// context: VirtualElementContext
|
// context: VirtualElementContext
|
||||||
};
|
};
|
||||||
export type VirtualElementText = {
|
export type VirtualElementText = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
node: Node,
|
|
||||||
text: string,
|
text: string,
|
||||||
};
|
};
|
||||||
export type VirtualElementEmpty = {
|
export type VirtualElementEmpty = {
|
||||||
type: 'empty',
|
type: 'empty',
|
||||||
node: Node,
|
|
||||||
};
|
};
|
||||||
export type VirtualElementFragment = {
|
export type VirtualElementFragment = {
|
||||||
node: Node,
|
|
||||||
type: 'fragment',
|
type: 'fragment',
|
||||||
children: VirtualElement[];
|
children: VirtualElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VirtualElementValid = VirtualElementComponent | VirtualElementTag | VirtualElementFragment;
|
export type VirtualElementValid = VirtualElementComponent | VirtualElementTag | VirtualElementFragment;
|
||||||
export type FC = (props: Props) => VirtualElement
|
type VirtualElementChildrenSingular = false | string | VirtualElement;
|
||||||
|
type VirtualElementChildren = VirtualElementChildrenSingular | (VirtualElementChildrenSingular)[];
|
||||||
|
export type FC = (props: Props) => VirtualElementChildren;
|
||||||
|
|
||||||
export function reRenderElement(context: VirtualElementContext) {
|
function createElementFragment(props: Props): VirtualElementFragment {
|
||||||
setContext(context);
|
return {
|
||||||
createElement(context.element.generate, {
|
type: 'fragment',
|
||||||
componentToUpdate: context.element,
|
children: resolveChildrenIntoVirtualElements(props.children || [" "]),
|
||||||
...context.element.props
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function createElementComponent(generateFunc: FC, props: Props): VirtualElementComponent {
|
||||||
|
const component: VirtualElementComponent = {
|
||||||
|
type: 'component',
|
||||||
|
generate: generateFunc,
|
||||||
|
props: props,
|
||||||
|
name: generateFunc.name,
|
||||||
|
instance: undefined as {element: VirtualElementComponent},
|
||||||
|
}
|
||||||
|
component.instance = {
|
||||||
|
element: component,
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElementTag(tag: string, props: Props): VirtualElementTag {
|
||||||
|
return {
|
||||||
|
type: 'tag',
|
||||||
|
tag,
|
||||||
|
props: Object.assign({}, props, {children: null}),
|
||||||
|
children: props.children ? resolveChildrenIntoVirtualElements(props.children) : [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createElement(type: string | FC, props: Props): VirtualElementValid {
|
export function createElement(type: string | FC, props: Props): VirtualElementValid {
|
||||||
// type="div"
|
|
||||||
// props = {
|
|
||||||
// children: "Hello world!"
|
|
||||||
// }
|
|
||||||
if (type === Fragment) {
|
if (type === Fragment) {
|
||||||
const element: VirtualElementFragment = {
|
return createElementFragment(props);
|
||||||
type: 'fragment',
|
|
||||||
node: document.createTextNode(" "),
|
|
||||||
children: [],
|
|
||||||
}
|
}
|
||||||
resolveChildren(props.children, element)
|
if (typeof type === 'function') {
|
||||||
return element;
|
return createElementComponent(type, props);
|
||||||
}
|
}
|
||||||
const isComponent = typeof type === 'function';
|
return createElementTag(type, props);
|
||||||
|
|
||||||
//create component
|
|
||||||
if (isComponent) {
|
|
||||||
console.log('createElement() component')
|
|
||||||
const component: VirtualElementComponent = props.componentToUpdate || {
|
|
||||||
type: 'component',
|
|
||||||
node: null,
|
|
||||||
generate: type,
|
|
||||||
children: [],
|
|
||||||
context: {
|
|
||||||
state: new Map(),
|
|
||||||
setStateFuncs: new Map(),
|
|
||||||
element: null,
|
|
||||||
},
|
|
||||||
props: props,
|
|
||||||
}
|
|
||||||
component.context.element = component;
|
|
||||||
|
|
||||||
setContext(component.context);
|
|
||||||
const c= type(props);
|
|
||||||
if (c.type === 'fragment') {
|
|
||||||
throw new Error('first element of component can\'t be fragment');
|
|
||||||
}
|
|
||||||
if (!component.children.length) {
|
|
||||||
component.children.push(c);
|
|
||||||
component.node = c.node;
|
|
||||||
} else {
|
|
||||||
//@ts-ignore
|
|
||||||
replaceChanged(component, {children: [c]});
|
|
||||||
}
|
|
||||||
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (!element.node) {
|
|
||||||
// const node = document.createElement(type);
|
|
||||||
// element.node = node;
|
|
||||||
// } else if (element.tag !== type) {
|
|
||||||
// const node = document.createElement(type);
|
|
||||||
// console.warn(`element.node.parentElement.replaceChild, tag: ${type}`);
|
|
||||||
// element.node.parentElement.replaceChild(node, element.node);
|
|
||||||
// element.node = node;
|
|
||||||
// element.tag = type;
|
|
||||||
// }
|
|
||||||
|
|
||||||
console.log(`createElement() tag, ${type}`);
|
|
||||||
const element: VirtualElementTag = {
|
|
||||||
node: document.createElement(type),
|
|
||||||
// node: null,
|
|
||||||
type: 'tag',
|
|
||||||
tag: type,
|
|
||||||
children: [],
|
|
||||||
otherProps: [],
|
|
||||||
propsFunctions: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [propKey, propVal] of Object.entries(props)) {
|
|
||||||
if (propKey === 'children') {
|
|
||||||
resolveChildren(propVal, element);
|
|
||||||
} else if (typeof propVal === 'function') {
|
|
||||||
element.propsFunctions.push([propKey.toLowerCase(), propVal]);
|
|
||||||
element.node[propKey.toLowerCase()] = propVal;
|
|
||||||
} else {
|
|
||||||
element.otherProps.push([propKey, propVal]);
|
|
||||||
element.node[propKey] = propVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if (className) {
|
|
||||||
// element.node.className = className;
|
|
||||||
// }
|
|
||||||
// if (onClick) {
|
|
||||||
// element.node.onclick = onClick;
|
|
||||||
// }
|
|
||||||
return element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualElementChildren = false | string | VirtualElement | (false | VirtualElement | string)[];
|
export function resolveChildrenIntoVirtualElements(list: VirtualElementChildren): VirtualElement[] {
|
||||||
|
|
||||||
function replaceChanged(oldHead: VirtualElementValid, newHead: VirtualElementValid) {
|
|
||||||
if (oldHead.children.length !== newHead.children.length) {
|
|
||||||
alert('replaceChanged: oldHead.children.length !== newHead.children.length')
|
|
||||||
throw new Error('replaceChanged: oldHead.children.length !== newHead.children.length');
|
|
||||||
}
|
|
||||||
for(let i = 0; i < oldHead.children.length; i++) {
|
|
||||||
const oldChild = oldHead.children[i];
|
|
||||||
const newChild = newHead.children[i];
|
|
||||||
|
|
||||||
if (
|
|
||||||
oldChild.type !== newChild.type ||
|
|
||||||
(
|
|
||||||
oldChild.type === 'tag' &&
|
|
||||||
newChild.type === 'tag' &&
|
|
||||||
(
|
|
||||||
oldChild.tag !== newChild.tag ||
|
|
||||||
JSON.stringify(oldChild.otherProps) !== JSON.stringify(newChild.otherProps)
|
|
||||||
)
|
|
||||||
) ||
|
|
||||||
(
|
|
||||||
oldChild.type === 'text' &&
|
|
||||||
newChild.type === 'text' &&
|
|
||||||
oldChild.text !== newChild.text
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.warn(`replace old node with new. old node:`, oldChild.node, 'new node:', newChild.node)
|
|
||||||
|
|
||||||
if (oldChild.type === 'fragment') {
|
|
||||||
for (const c of oldChild.children) {
|
|
||||||
oldChild.node.parentElement.removeChild(c.node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newChild.type === 'fragment') {
|
|
||||||
for (const c of newChild.children) {
|
|
||||||
oldChild.node.parentElement.insertBefore(c.node, oldChild.node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oldChild.node.parentElement.replaceChild(newChild.node, oldChild.node);
|
|
||||||
oldHead.children[i] = newChild;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldChild.type === 'tag' && newChild.type === 'tag') {
|
|
||||||
for (const [propKey, propVal] of newChild.propsFunctions) {
|
|
||||||
oldChild.node[propKey.toLowerCase()] = propVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
if (oldChild?.children?.length) {
|
|
||||||
//@ts-ignore
|
|
||||||
replaceChanged(oldChild, newChild)
|
|
||||||
}
|
|
||||||
if (oldChild.type === 'component' && newChild.type === 'component') {
|
|
||||||
newChild.node = oldChild.node;
|
|
||||||
newChild.children = oldChild.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveChildren(list: VirtualElementChildren, parent: VirtualElementValid) {
|
|
||||||
const array = Array.isArray(list) ? list : [list];
|
const array = Array.isArray(list) ? list : [list];
|
||||||
|
const result: VirtualElement[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
for (let i = 0; i < array.length; i++) {
|
||||||
const child = array[i];
|
const child = array[i];
|
||||||
if (typeof child === 'boolean') {
|
if (typeof child === 'boolean') {
|
||||||
parent.children[i] = {
|
result.push({
|
||||||
// node: null,
|
|
||||||
node: document.createTextNode(" "),
|
|
||||||
type: 'empty',
|
type: 'empty',
|
||||||
};
|
});
|
||||||
} else if (typeof child !== 'object') {
|
} else if (typeof child !== 'object') {
|
||||||
parent.children[i] = {
|
result.push({
|
||||||
// node: null,
|
|
||||||
node: document.createTextNode(child),
|
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: child,
|
text: `${child}`,
|
||||||
};
|
});
|
||||||
} else {
|
} else {
|
||||||
if (child.type === 'fragment') {
|
if (Array.isArray(child)) {
|
||||||
for (const c of child.children) {
|
result.push(...resolveChildrenIntoVirtualElements(child))
|
||||||
parent.node.appendChild(c.node);
|
} else {
|
||||||
|
result.push(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent.children[i] = child;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent.type !== 'fragment') {
|
|
||||||
parent.node.appendChild(parent.children[i].node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user