Skip to content

Instantly share code, notes, and snippets.

@faustinoaq
Last active November 20, 2024 22:05
Show Gist options
  • Save faustinoaq/b19da758fc45155a0b3b10d9f578c5ce to your computer and use it in GitHub Desktop.
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).
<!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>
<!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>
<!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>
@faustinoaq
Copy link
Author

image

@artydev
Copy link

artydev commented Nov 4, 2024

Awesome, thank you very much :-)

@fyodorio
Copy link

fyodorio commented Nov 5, 2024

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)

@Tofu-Xx
Copy link

Tofu-Xx commented Nov 20, 2024

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