init, v1, createElement create nodes and manipulate DOM
This commit is contained in:
commit
110906370b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
lib
|
||||
.idea
|
||||
13
babel.config.json
Normal file
13
babel.config.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-typescript",
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"importSource": "@keact"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
6644
package-lock.json
generated
Normal file
6644
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "keact",
|
||||
"version": "1.0.0",
|
||||
"description": "Experiment to recreate minimal react implementation.",
|
||||
"license": "ISC",
|
||||
"author": "",
|
||||
"type": "commonjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"babel": "babel src --out-dir lib --extensions \".ts,.tsx\"",
|
||||
"start": "webpack serve",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.3",
|
||||
"@babel/core": "^7.28.5",
|
||||
"@babel/preset-env": "^7.28.5",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"babel-loader": "^10.0.0",
|
||||
"html-webpack-plugin": "^5.6.5",
|
||||
"webpack": "^5.103.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
}
|
||||
}
|
||||
24
src/components/App.tsx
Normal file
24
src/components/App.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import {useState} from "../keact/hooks";
|
||||
import {Toggle} from "./Toggle";
|
||||
|
||||
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`);
|
||||
}
|
||||
// return <div>
|
||||
// {text}
|
||||
// </div>
|
||||
return <div>
|
||||
<div className="text_holder"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<span>{text}</span>
|
||||
<span>clicked {clicked} times</span>
|
||||
</div>
|
||||
<Toggle clicked={clicked}/>
|
||||
</div>
|
||||
}
|
||||
24
src/components/ChooseBg.tsx
Normal file
24
src/components/ChooseBg.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import {useState} from "../keact/hooks";
|
||||
|
||||
export function ChooseBg() {
|
||||
const [bgColor, setBgColor] = useState('#ff0000');
|
||||
|
||||
function changeBgColor(e: any) {
|
||||
const value = e.target.value;
|
||||
if ([4, 7].includes(value.length)) {
|
||||
setBgColor(e.target.value);
|
||||
document.body.style.background = e.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div>
|
||||
<span>input your own bg color:</span>
|
||||
<input type="text"
|
||||
placeholder="#ff0000"
|
||||
onInput={changeBgColor}
|
||||
/>
|
||||
</div>
|
||||
<span>current bg color is {bgColor}</span>
|
||||
</div>
|
||||
}
|
||||
36
src/components/Toggle.tsx
Normal file
36
src/components/Toggle.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import {useState} from "../keact/hooks";
|
||||
import {ChooseBg} from "./ChooseBg";
|
||||
|
||||
export const Toggle = (props) => {
|
||||
let [isOn, setIsOn] = useState(false);
|
||||
|
||||
function toggle() {
|
||||
const newVal = !isOn;
|
||||
setIsOn(newVal);
|
||||
if (newVal) {
|
||||
document.body.style.background = '#ff0000';
|
||||
} else {
|
||||
document.body.style.background = '';
|
||||
}
|
||||
}
|
||||
return <div>
|
||||
{
|
||||
!isOn && <>
|
||||
<span>
|
||||
off
|
||||
</span>
|
||||
click "Toggle"
|
||||
</>
|
||||
}
|
||||
<button onClick={toggle}>
|
||||
Toggle
|
||||
</button>
|
||||
<span>
|
||||
clicked*2: {props.clicked*2}
|
||||
</span>
|
||||
|
||||
{
|
||||
isOn && <ChooseBg />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
10
src/index.html
Normal file
10
src/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Keact test</title>
|
||||
</head>
|
||||
<body id="root">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
12
src/index.tsx
Normal file
12
src/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import KeactDOM from "./keact/keact-dom";
|
||||
import {App} from "./components/App";
|
||||
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
KeactDOM.render(
|
||||
// @ts-ignore
|
||||
<App/>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
}
|
||||
28
src/keact/hooks.ts
Normal file
28
src/keact/hooks.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {reRenderElement, VirtualElementContext} from "./keact";
|
||||
|
||||
let currentInstance: VirtualElementContext;
|
||||
let cursor = 0;
|
||||
|
||||
export function useState(initial: any): [any, Function] {
|
||||
const {state, setStateFuncs} = currentInstance;
|
||||
if (state.has(cursor)) {
|
||||
const result: [any, Function] = [state.get(cursor), setStateFuncs.get(cursor)];
|
||||
cursor++;
|
||||
return result;
|
||||
}
|
||||
const setFunc = setState.bind(this, currentInstance, cursor);
|
||||
state.set(cursor, initial)
|
||||
setStateFuncs.set(cursor, setFunc)
|
||||
cursor++;
|
||||
return [initial, setFunc];
|
||||
}
|
||||
|
||||
function setState(currentInstance: VirtualElementContext, cursor: number, newValue: any) {
|
||||
currentInstance.state.set(cursor, newValue);
|
||||
reRenderElement(currentInstance);
|
||||
}
|
||||
|
||||
export const setContext = (context: VirtualElementContext) => {
|
||||
currentInstance = context;
|
||||
cursor = 0;
|
||||
}
|
||||
14
src/keact/jsx-runtime.ts
Normal file
14
src/keact/jsx-runtime.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {createElement, FC, Props} from "./keact";
|
||||
export type {JSX} from 'react';
|
||||
|
||||
|
||||
export function jsx(type: any, props: Props, key) {
|
||||
// type="div"
|
||||
// props = {
|
||||
// children: "Hello world!"
|
||||
// }
|
||||
return createElement(type, props)
|
||||
}
|
||||
|
||||
export const jsxs = jsx;
|
||||
export const Fragment = Symbol("Fragment") as undefined as FC;
|
||||
12
src/keact/keact-dom.ts
Normal file
12
src/keact/keact-dom.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {VirtualElement} from "./keact";
|
||||
|
||||
function render($element: VirtualElement | undefined, root: HTMLElement) {
|
||||
if (!$element) return;
|
||||
|
||||
root.append($element.node);
|
||||
}
|
||||
|
||||
const KeactDOM = {
|
||||
render,
|
||||
}
|
||||
export default KeactDOM;
|
||||
238
src/keact/keact.ts
Normal file
238
src/keact/keact.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import {setContext} from "./hooks";
|
||||
import {Fragment} from "./jsx-runtime";
|
||||
|
||||
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 VirtualElementComponent = {
|
||||
type: 'component',
|
||||
node: Node,
|
||||
context: VirtualElementContext,
|
||||
generate: FC,
|
||||
children: VirtualElement[],
|
||||
};
|
||||
export type VirtualElementTag = {
|
||||
node: HTMLElement;
|
||||
type: 'tag';
|
||||
tag: string; //html tag
|
||||
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
|
||||
|
||||
export function reRenderElement(context: VirtualElementContext) {
|
||||
setContext(context);
|
||||
createElement(context.element.generate, {
|
||||
componentToUpdate: context.element,
|
||||
...context.element.props
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 (!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)[];
|
||||
|
||||
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];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const child = array[i];
|
||||
if (typeof child === 'boolean') {
|
||||
parent.children[i] = {
|
||||
// node: null,
|
||||
node: document.createTextNode(" "),
|
||||
type: 'empty',
|
||||
};
|
||||
} else if (typeof child !== 'object') {
|
||||
parent.children[i] = {
|
||||
// node: null,
|
||||
node: document.createTextNode(child),
|
||||
type: 'text',
|
||||
text: child,
|
||||
};
|
||||
} else {
|
||||
if (child.type === 'fragment') {
|
||||
for (const c of child.children) {
|
||||
parent.node.appendChild(c.node);
|
||||
}
|
||||
}
|
||||
parent.children[i] = child;
|
||||
}
|
||||
|
||||
if (parent.type !== 'fragment') {
|
||||
parent.node.appendChild(parent.children[i].node);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
webpack.config.js
Normal file
38
webpack.config.js
Normal file
@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
alias: {
|
||||
'@keact': path.resolve(__dirname, './src/keact'),
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: "./src/index.html"
|
||||
})
|
||||
],
|
||||
devServer: {
|
||||
port: 3000, // You can run your app at http://localhost:3000
|
||||
hot: false, // "Hot Module Replacement" (Updates without full reload)
|
||||
static: {
|
||||
directory: path.join(__dirname, 'public'), // If you have static assets (images)
|
||||
},
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user