Last active
December 11, 2024 08:23
-
-
Save faustinoaq/b19da758fc45155a0b3b10d9f578c5ce to your computer and use it in GitHub Desktop.
Front-end libraries (React, Vue, Angular) and the basic principles of how they work, all in a single file using pure JavaScript (VanillaJS).
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>My Angular from Scratch</title> | |
<style> | |
.my-component { | |
font-family: Arial, sans-serif; | |
text-align: center; | |
padding: 50px; | |
background: #f0f0f0; | |
} | |
.my-component .container { | |
background: white; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | |
} | |
.my-component .message { | |
font-size: 24px; | |
margin-bottom: 20px; | |
} | |
.my-component .buttons button { | |
font-size: 16px; | |
padding: 10px 20px; | |
margin: 5px; | |
cursor: pointer; | |
border: none; | |
border-radius: 5px; | |
transition: background 0.3s; | |
} | |
.my-component .buttons button:hover { | |
background: #ddd; | |
} | |
.my-component .buttons button:active { | |
background: #ccc; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="app"> | |
<div class="my-component"> | |
<div class="container"> | |
<p class="message">Count: <span ng-bind="count"></span></p> | |
<div class="buttons"> | |
<button ng-click="increment">Increment</button> | |
<button ng-click="decrement">Decrease</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Reactive system to track changes | |
function reactive(data) { | |
const listeners = []; | |
const proxy = new Proxy(data, { | |
set(target, property, value) { | |
target[property] = value; | |
listeners.forEach(listener => listener()); // Notify all listeners on data change | |
return true; | |
} | |
}); | |
proxy.subscribe = function (listener) { | |
listeners.push(listener); | |
}; | |
return proxy; | |
} | |
// Our basic Angular-like app system | |
function myAngularApp(rootElement, controller) { | |
const data = reactive(controller.data()); | |
// Bind methods to data | |
for (const key in controller.methods) { | |
data[key] = function () { | |
controller.methods[key].call(data); // Call the controller method in context of data | |
data.notify(); // Trigger re-render | |
}; | |
} | |
// Notify function to re-render on changes | |
data.notify = function () { | |
compile(rootElement); | |
}; | |
function compile(element) { | |
const bindElements = element.querySelectorAll('[ng-bind]'); | |
const clickElements = element.querySelectorAll('[ng-click]'); | |
// Set text content for bound elements | |
bindElements.forEach(el => { | |
const property = el.getAttribute('ng-bind'); | |
el.textContent = data[property]; | |
}); | |
// Set up click handlers for elements with ng-click | |
clickElements.forEach(el => { | |
const methodName = el.getAttribute('ng-click'); | |
el.onclick = data[methodName]; // Assign the method directly to onclick | |
}); | |
} | |
// Initial compilation | |
compile(rootElement); | |
data.subscribe(() => compile(rootElement)); // Subscribe compile function to re-render on data change | |
} | |
// Define controller with data and methods | |
const MyController = { | |
data() { | |
return { count: 0 }; | |
}, | |
methods: { | |
increment() { | |
this.count++; | |
}, | |
decrement() { | |
this.count--; | |
} | |
} | |
}; | |
// Initialize the app | |
document.addEventListener('DOMContentLoaded', function () { | |
const rootElement = document.getElementById('app'); | |
myAngularApp(rootElement, MyController); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>My React from Scratch</title> | |
</head> | |
<body> | |
<script> | |
// Stateful logic with hooks | |
let currentComponent = null; | |
function useState(initialValue) { | |
if (!currentComponent) { | |
throw new Error('useState must be called within a component'); | |
} | |
const stateIndex = currentComponent.stateIndex; | |
if (!currentComponent.state[stateIndex]) { | |
currentComponent.state[stateIndex] = [ | |
initialValue, | |
(value) => { | |
currentComponent.state[stateIndex][0] = value; | |
currentComponent.render(); | |
}, | |
]; | |
} | |
const stateTuple = currentComponent.state[stateIndex]; | |
currentComponent.stateIndex++; | |
return [stateTuple[0], stateTuple[1]]; | |
} | |
function createComponent(renderFn) { | |
return function Component() { | |
currentComponent = { | |
state: [], | |
stateIndex: 0, | |
renderFn: renderFn, | |
render: function () { | |
this.stateIndex = 0; // Reset index on each render | |
const newVNode = this.renderFn(); | |
const rootElement = document.getElementById('root') || document.body; | |
if (!this.vnode) { | |
// Initial render | |
this.vnode = newVNode; | |
rootElement.appendChild(createElement(newVNode)); | |
} else { | |
const patches = diff(this.vnode, newVNode); | |
patch(rootElement, patches); | |
this.vnode = newVNode; | |
} | |
}, | |
}; | |
currentComponent.render(); | |
return currentComponent; | |
}; | |
} | |
function h(tag, props, ...children) { | |
return { tag, props, children }; | |
} | |
function createElement(vnode) { | |
if (typeof vnode === 'string') { | |
return document.createTextNode(vnode); | |
} | |
const { tag, props, children } = vnode; | |
const element = document.createElement(tag); | |
for (let key in props) { | |
element[key] = props[key]; | |
} | |
children.forEach(child => element.appendChild(createElement(child))); | |
return element; | |
} | |
function diff(oldVNode, newVNode) { | |
if (!oldVNode) { | |
return { type: 'CREATE', newVNode }; | |
} | |
if (!newVNode) { | |
return { type: 'REMOVE' }; | |
} | |
if (typeof oldVNode !== typeof newVNode || oldVNode.tag !== newVNode.tag) { | |
return { type: 'REPLACE', newVNode }; | |
} | |
if (typeof newVNode === 'string') { | |
if (oldVNode !== newVNode) { | |
return { type: 'TEXT', newVNode }; | |
} else { | |
return null; | |
} | |
} | |
const patch = { | |
type: 'UPDATE', | |
props: diffProps(oldVNode.props, newVNode.props), | |
children: diffChildren(oldVNode.children, newVNode.children), | |
}; | |
return patch; | |
} | |
function diffProps(oldProps, newProps) { | |
const patches = []; | |
for (let key in newProps) { | |
if (newProps[key] !== oldProps[key]) { | |
patches.push({ key, value: newProps[key] }); | |
} | |
} | |
for (let key in oldProps) { | |
if (!(key in newProps)) { | |
patches.push({ key, value: undefined }); | |
} | |
} | |
return patches; | |
} | |
function diffChildren(oldChildren, newChildren) { | |
const patches = []; | |
const maxLen = Math.max(oldChildren.length, newChildren.length); | |
for (let i = 0; i < maxLen; i++) { | |
patches.push(diff(oldChildren[i], newChildren[i])); | |
} | |
return patches; | |
} | |
function patch(parent, patchObj, index = 0) { | |
if (!patchObj) return; | |
const el = parent.childNodes[index]; | |
switch (patchObj.type) { | |
case 'CREATE': { | |
const newEl = createElement(patchObj.newVNode); | |
parent.appendChild(newEl); | |
break; | |
} | |
case 'REMOVE': { | |
if (el) { | |
parent.removeChild(el); | |
} | |
break; | |
} | |
case 'REPLACE': { | |
const newEl = createElement(patchObj.newVNode); | |
if (el) { | |
parent.replaceChild(newEl, el); | |
} else { | |
parent.appendChild(newEl); | |
} | |
break; | |
} | |
case 'TEXT': { | |
if (el) { | |
el.textContent = patchObj.newVNode; | |
} | |
break; | |
} | |
case 'UPDATE': { | |
if (el) { | |
const { props, children } = patchObj; | |
props.forEach(({ key, value }) => { | |
if (value === undefined) { | |
el.removeAttribute(key); | |
} else { | |
el[key] = value; | |
} | |
}); | |
children.forEach((childPatch, i) => { | |
patch(el, childPatch, i); | |
}); | |
} | |
break; | |
} | |
} | |
} | |
const MyComponent = createComponent(function () { | |
const [count, setCount] = useState(0); | |
function increment() { | |
setCount(count + 1); | |
} | |
function decrement() { | |
setCount(count - 1); | |
} | |
return h('div', { className: 'my-component' }, | |
h('div', { className: 'container' }, | |
h('p', { className: 'message' }, `Count: ${count}`), | |
h('div', { className: 'buttons' }, | |
h('button', { onclick: () => increment() }, 'Increment'), | |
h('button', { onclick: () => decrement() }, 'Decrease') | |
) | |
) | |
); | |
}); | |
// Create an initial root element | |
const root = document.createElement('div'); | |
root.id = 'root'; | |
document.body.appendChild(root); | |
// Initialize App | |
const App = MyComponent(); | |
App.render(); | |
// Add CSS styling scoped to the component | |
const style = document.createElement('style'); | |
style.textContent = ` | |
.my-component { | |
font-family: Arial, sans-serif; | |
text-align: center; | |
padding: 50px; | |
background: #f0f0f0; | |
} | |
.my-component .container { | |
background: white; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 0 10px rgba(0,0,0,0.1); | |
} | |
.my-component .message { | |
font-size: 24px; | |
margin-bottom: 20px; | |
} | |
.my-component .buttons button { | |
font-size: 16px; | |
padding: 10px 20px; | |
margin: 5px; | |
cursor: pointer; | |
border: none; | |
border-radius: 5px; | |
transition: background 0.3s; | |
} | |
.my-component .buttons button:hover { | |
background: #ddd; | |
} | |
.my-component .buttons button:active { | |
background: #ccc; | |
} | |
`; | |
document.head.appendChild(style); | |
</script> | |
</body> | |
</html> |
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>My Vue from Scratch</title> | |
</head> | |
<body> | |
<script> | |
function reactive(obj) { | |
const listeners = new Set(); | |
const proxy = new Proxy(obj, { | |
get(target, property, receiver) { | |
if (typeof target[property] === 'object' && target[property] !== null) { | |
return reactive(target[property]); | |
} | |
return Reflect.get(target, property, receiver); | |
}, | |
set(target, property, value, receiver) { | |
const result = Reflect.set(target, property, value, receiver); | |
listeners.forEach(fn => fn()); | |
return result; | |
} | |
}); | |
proxy.subscribe = function (fn) { | |
listeners.add(fn); | |
}; | |
proxy.unsubscribe = function (fn) { | |
listeners.delete(fn); | |
}; | |
return proxy; | |
} | |
class Component { | |
constructor(options) { | |
this.template = options.template; | |
this.data = reactive(options.data()); | |
this.methods = options.methods; | |
this.style = options.style; | |
this.rootId = options.rootId; | |
// Ensure root element exists | |
if (!document.getElementById(this.rootId)) { | |
const rootElement = document.createElement('div'); | |
rootElement.id = this.rootId; | |
document.body.appendChild(rootElement); | |
} | |
this.data.subscribe(this.render.bind(this)); | |
this.render(); | |
} | |
compileTemplate(template) { | |
const match = template.match(/{{\s*(\w+)\s*}}/g); | |
return () => { | |
let compiledTemplate = template; | |
if (match) { | |
match.forEach(item => { | |
const key = item.replace(/{{\s*|\s*}}/g, ''); | |
compiledTemplate = compiledTemplate.replace(item, this.data[key]); | |
}); | |
} | |
return compiledTemplate; | |
}; | |
} | |
render() { | |
const el = document.getElementById(this.rootId); | |
if (el) { | |
el.innerHTML = this.compileTemplate(this.template)(); | |
this.applyMethods(el); | |
} | |
} | |
applyMethods(el) { | |
Object.keys(this.methods).forEach(methodName => { | |
const matches = el.querySelectorAll(`[data-action="${methodName}"]`); | |
matches.forEach(match => { | |
match.onclick = this.methods[methodName].bind(this.data); | |
}); | |
}); | |
} | |
} | |
const MyComponent = new Component({ | |
template: ` | |
<div class="my-component"> | |
<div class="container"> | |
<p class="message">Count: {{ count }}</p> | |
<div class="buttons"> | |
<button data-action="increment">Increment</button> | |
<button data-action="decrement">Decrease</button> | |
</div> | |
</div> | |
</div> | |
`, | |
data() { | |
return { | |
count: 0, | |
}; | |
}, | |
methods: { | |
increment() { | |
this.count += 1; | |
}, | |
decrement() { | |
this.count -= 1; | |
}, | |
}, | |
style: ` | |
.my-component { | |
font-family: Arial, sans-serif; | |
text-align: center; | |
padding: 50px; | |
background: #f0f0f0; | |
} | |
.my-component .container { | |
background: white; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 0 10px rgba(0,0,0,0.1); | |
} | |
.my-component .message { | |
font-size: 24px; | |
margin-bottom: 20px; | |
} | |
.my-component .buttons button { | |
font-size: 16px; | |
padding: 10px 20px; | |
margin: 5px; | |
cursor: pointer; | |
border: none; | |
border-radius: 5px; | |
transition: background 0.3s; | |
} | |
.my-component .buttons button:hover { | |
background: #ddd; | |
} | |
.my-component .buttons button:active { | |
background: #ccc; | |
} | |
`, | |
rootId: 'root' // Specify the ID for the root element | |
}); | |
// Ensure CSS is applied to the component | |
const style = document.createElement('style'); | |
style.textContent = MyComponent.style; | |
document.head.appendChild(style); | |
</script> | |
</body> | |
</html> |
Author
faustinoaq
commented
Oct 31, 2024
Awesome, thank you very much :-)
It's funny that Angular version appears to be the most concise among others, which is not something anyone would expect 😅 (especially as it's not Angular but rather AngularJS, the older, heavier version — and the same for Vue though, the good ol' Vue 2)
I would like to introduce what I wrote
https://github.com/tofu-xx/vueey
Achieve the most core functions with the least amount of code
function Vue(opt) {
let _active;
const _deps = {};
const $el = opt.el?.at ? document.querySelector(opt.el) : opt.el ?? document;
const that = Object.assign(new Proxy(typeof opt.data == 'object' ? opt.data : opt.data?.() ?? {}, {
get: (...args) => [Reflect.get(...args), (_deps[args[1]] ??= new Set()).add(_active)][0],
set: (...args) => [Reflect.set(...args), _deps[args[1]]?.forEach(f => f?.())][0],
}), opt.methods, { $el, $refs: {} });
const thatKeyRex = new RegExp(Object.keys(that).join('\\b|').replaceAll('$', '\\$'), 'g');
const toExpression = (raw) => new Function('$event', 'return ' + raw.replace(thatKeyRex, k => 'this.' + k));
const walk = (walker, effect) => {
const node = walker.currentNode;
const { nodeType, data: tem } = node;
if (nodeType == 1) for (const { name, value: raw } of node.attributes) {
const bindName = name.slice(1);
if (name[0] == '@')
node['on' + bindName] = (/[^\s\w$]/.test(raw) ? toExpression(raw) : that[raw.trim()])?.bind(that);
if (name[0] == ':')
effect(() => node.setAttribute(bindName, node[bindName] = toExpression(raw).call(that)));
name == 'ref' && (that.$refs[raw] = node);
}
if (nodeType == 3)
effect(() => node.data = tem.replace(/\{\{(.*?)\}\}/g, (_, raw) => toExpression(raw).call(that)));
walker.nextNode() ? walk(walker, effect) : opt.mounted?.call(that);
};
walk(document.createTreeWalker($el, 7), fn => (_active = fn, fn()));
for (const [key, fn] of Object.entries(opt.watch ?? {})) {
let oldVal = that[key];
_deps[key].add(() => Promise.resolve().then(() => {
const val = that[key];
fn.call(that, val, oldVal);
oldVal = val;
}));
}
};
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment