diff --git a/src/components/App.tsx b/src/components/App.tsx
index c521adf..4bf3c70 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,24 +1,42 @@
import {useState} from "../keact/hooks";
import {Toggle} from "./Toggle";
+import {CheckEmptyFragment} from "./CheckEmptyFragment";
+import {ChooseBg} from "./ChooseBg";
+import {TextComponent} from "./TextComponent";
+import {NamesList} from "./namesList";
export const App = () => {
- let [clicked, setClicked] = useState(0);
- const text = "Hello world!";
- function clickHandler() {
- const newClicked = +clicked + 1;
- setClicked(newClicked);
- console.log(`clicked ${newClicked}-th time`);
- }
+ // let [clicked, setClicked] = useState(0);
+ // const text = "Hello world!";
+ // function clickHandler() {
+ // const newClicked = +clicked + 1;
+ // setClicked(newClicked);
+ // console.log(`clicked ${newClicked}-th time`);
+ // }
+
+ return
{
diff --git a/src/components/namesList.tsx b/src/components/namesList.tsx
new file mode 100644
index 0000000..a764597
--- /dev/null
+++ b/src/components/namesList.tsx
@@ -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
+
Names list:
+
+ {namesList.map((n, i) =>
+
+ #{i + 1} - {n}
+
+ )}
+
+
+
add to list:
+ {
+ //@ts-ignore
+ setNewName(e.target.value)
+ }}
+ />
+
+
+
+}
\ No newline at end of file
diff --git a/src/keact/hooks.ts b/src/keact/hooks.ts
index 2d9a463..ba73392 100644
--- a/src/keact/hooks.ts
+++ b/src/keact/hooks.ts
@@ -1,6 +1,6 @@
-import {reRenderElement, VirtualElementContext} from "./keact";
+import {ComponentContext, reRenderComponent} from "./keact-dom";
-let currentInstance: VirtualElementContext;
+let currentInstance: ComponentContext;
let cursor = 0;
export function useState(initial: any): [any, Function] {
@@ -11,18 +11,18 @@ export function useState(initial: any): [any, Function] {
return result;
}
const setFunc = setState.bind(this, currentInstance, cursor);
- state.set(cursor, initial)
setStateFuncs.set(cursor, setFunc)
+ state.set(cursor, initial)
cursor++;
return [initial, setFunc];
}
-function setState(currentInstance: VirtualElementContext, cursor: number, newValue: any) {
+function setState(currentInstance: ComponentContext, cursor: number, newValue: any) {
currentInstance.state.set(cursor, newValue);
- reRenderElement(currentInstance);
+ reRenderComponent(currentInstance.element);
}
-export const setContext = (context: VirtualElementContext) => {
+export const setContext = (context: ComponentContext) => {
currentInstance = context;
cursor = 0;
}
\ No newline at end of file
diff --git a/src/keact/keact-dom.ts b/src/keact/keact-dom.ts
index 8b46bfc..8d7eefd 100644
--- a/src/keact/keact-dom.ts
+++ b/src/keact/keact-dom.ts
@@ -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
= new Map()
+
+export type ComponentContext = {
+ state: Map,
+ setStateFuncs: Map
+ 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;
+}
+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) {
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) {
+ 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){
+ 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 = {};
+ 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 = {
diff --git a/src/keact/keact.ts b/src/keact/keact.ts
index 78b4152..20e8cc5 100644
--- a/src/keact/keact.ts
+++ b/src/keact/keact.ts
@@ -1,238 +1,103 @@
-import {setContext} from "./hooks";
import {Fragment} from "./jsx-runtime";
export type Props = Record;
-export type VirtualElementContext = {
- state: Map,
- setStateFuncs: Map
- element: any,
-}
export type VirtualElement = VirtualElementTag | VirtualElementText | VirtualElementComponent | VirtualElementEmpty | VirtualElementFragment;
export type VirtualElementComponent = {
type: 'component',
- node: Node,
- context: VirtualElementContext,
+ props: Props,
generate: FC,
- children: VirtualElement[],
+ name: string,
+ instance: {
+ element: VirtualElementComponent;
+ },
};
export type VirtualElementTag = {
- node: HTMLElement;
type: 'tag';
tag: string; //html tag
+ props: Props;
children: VirtualElement[];
- otherProps: [string, any][];
- propsFunctions: [string, Function][];
// context: VirtualElementContext
};
export type VirtualElementText = {
type: 'text',
- node: Node,
text: string,
};
export type VirtualElementEmpty = {
type: 'empty',
- node: Node,
};
export type VirtualElementFragment = {
- node: Node,
type: 'fragment',
children: VirtualElement[];
};
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) {
- setContext(context);
- createElement(context.element.generate, {
- componentToUpdate: context.element,
- ...context.element.props
- });
+function createElementFragment(props: Props): VirtualElementFragment {
+ return {
+ type: 'fragment',
+ children: resolveChildrenIntoVirtualElements(props.children || [" "]),
+ }
+}
+
+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 {
- // type="div"
- // props = {
- // children: "Hello world!"
- // }
if (type === Fragment) {
- const element: VirtualElementFragment = {
- type: 'fragment',
- node: document.createTextNode(" "),
- children: [],
- }
- resolveChildren(props.children, element)
- return element;
+ return createElementFragment(props);
}
- const isComponent = typeof type === 'function';
-
- //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 (typeof type === 'function') {
+ return createElementComponent(type, props);
}
-
- // 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;
+ return createElementTag(type, props);
}
-type VirtualElementChildren = false | string | VirtualElement | (false | VirtualElement | string)[];
-
-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) {
+export function resolveChildrenIntoVirtualElements(list: VirtualElementChildren): VirtualElement[] {
const array = Array.isArray(list) ? list : [list];
+ const result: VirtualElement[] = [];
for (let i = 0; i < array.length; i++) {
const child = array[i];
if (typeof child === 'boolean') {
- parent.children[i] = {
- // node: null,
- node: document.createTextNode(" "),
+ result.push({
type: 'empty',
- };
+ });
} else if (typeof child !== 'object') {
- parent.children[i] = {
- // node: null,
- node: document.createTextNode(child),
+ result.push({
type: 'text',
- text: child,
- };
+ text: `${child}`,
+ });
} else {
- if (child.type === 'fragment') {
- for (const c of child.children) {
- parent.node.appendChild(c.node);
- }
+ if (Array.isArray(child)) {
+ result.push(...resolveChildrenIntoVirtualElements(child))
+ } else {
+ result.push(child)
}
- parent.children[i] = child;
- }
-
- if (parent.type !== 'fragment') {
- parent.node.appendChild(parent.children[i].node);
}
}
+ return result;
}
\ No newline at end of file