Created
April 29, 2019 17:30
-
-
Save jasper-lyons/f36c7c2b093fe072edfaf59db95aff2c to your computer and use it in GitHub Desktop.
React in 100 lines
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
<html> | |
<head> | |
<script> | |
// fn - a function | |
// arg - anything | |
// | |
// Saves us having to pass the same variable into the function over | |
// and over again. | |
function curry(fn, arg) { | |
return function () { | |
return fn(arg) | |
} | |
} | |
// view - a function that returns a shadow element | |
// element - any DOM element | |
// | |
// Converts the shadow nodes from the view function into real DOM nodes | |
// and injects them as children to the given element. Returns a | |
// function render that when called will compare the shadow nodes | |
// returned from calling the view function with the previously rendered | |
// application. If there are differences then changes will be applied | |
// by the diffAndApply function. | |
function mount(view, element) { | |
while (element.childNodes.length > 0) { | |
element.removeChild(element.childNodes[0]) | |
} | |
element.appendChild(toElement(view())) | |
return function render() { | |
diffAndApply(element.children[0], view()) | |
} | |
} | |
// A custom object to remove the need to check for the absence of the | |
// tagName, attributes and children properties. Without this, to check | |
// that something is not a shadow node we'd do | |
// | |
// if (!(element.tagName && element.attributes)) { | |
// ... | |
// | |
// Instead we can do | |
// | |
// if (!(element instanceof ShadownElement)) { | |
// ... | |
// | |
// Which is much clearer though less flexible. | |
function ShadowElement(tagName, attributes, children) { | |
this.tagName = tagName | |
this.attributes = attributes | |
this.children = Array.isArray(children) ? children : [children] | |
} | |
// tagName - a string representing an HTML tag name | |
// attributes - a map of key values representing DOM element attributes | |
// children - shadow element(s) that will be added to the parent DOM node as children via appendChild | |
// | |
// A function analogous to Reacts createComponent function. | |
function h(tagName, attributes, children) { | |
return new ShadowElement(tagName, attributes, children) | |
} | |
// root - a DOM element | |
// newShadowRoot - a shadow element | |
// | |
// Compare the root and shadow root elements and update the root element | |
// if there are differences. This could be: | |
// * replacing the element if the tags are different | |
// * changing only the different attributes | |
// * recursing through the children and applying the same function | |
function diffAndApply(root, newShadowRoot) { | |
// support elements as functions e.g. | |
// | |
// h(App, { title: 'Hello, World!' }) | |
if (typeof newShadowRoot.tagName === 'function') { | |
newShadowRoot = newShadowRoot.tagName(attributes) | |
} | |
// different elements, blow them away | |
if (root.tagName !== newShadowRoot.tagName) { | |
let parent = root.parentNode | |
parent.removeChild(root) | |
parent.appendChild(toElement(newShadowRoot)) | |
return | |
} | |
// only update changed attributes | |
for (key in newShadowRoot.attributes) { | |
if (typeof newShadowRoot.attributes[key] === 'object') | |
Object.assign(root[key], newShadowRoot.attributes[key]) | |
else | |
root[key] = newShadowRoot.attributes[key] | |
} | |
// check the children | |
if (root.childNodes.length > newShadowRoot.children.length) { | |
for (var i = 0; i < root.childNodes; i++) { | |
if (newShadowRoot.children[i]) | |
diffAndApply(root.childNodes[i], newShadowRoot.children[i]) | |
else | |
root.removeChild(root.childNodes[i]) | |
} | |
} else if (root.childNodes.length < newShadowRoot.children.length) { | |
for (var i = 0; i < newShadowRoot.children; i++) { | |
if (root.chidlren[i]) | |
diffAndApply(root.childNodes[i], newShadowRoot.children[i]) | |
else | |
root.appendChild(toElement(newShadowRoot.children[i])) | |
} | |
} else { | |
for (var i = 0; i < newShadowRoot.children; i++) { | |
diffAndApply(root.childNodes[i], newShadownRoot.children[i]) | |
} | |
} | |
} | |
// shadowElement - an object which represents a DOM element | |
// | |
// Takes an object and converts it into a DOM element | |
function toElement(shadowElement) { | |
if (!(shadowElement instanceof ShadowElement)) | |
return document.createTextNode(shadowElement) | |
let { tagName, attributes, children } = shadowElement | |
let element = Object. | |
assign(document.createElement(tagName), attributes) | |
if (attributes.style) | |
Object.assign(element.style, attributes.style) | |
if (children) | |
children. | |
map(toElement). | |
forEach(element.appendChild.bind(element)) | |
return element; | |
} | |
var render = function () {} | |
var data = { message: 'Hello, World!' } | |
function Header(title) { | |
return h('header', {}, | |
h('h1', {}, title)) | |
} | |
function Content() { | |
return h('main', {}, | |
h('div', {}, 'This is a super simple implementation of React in roughly 100 lines (not including comments and the test application code).')) | |
} | |
function Footer() { | |
return h('footer', {}, | |
h('p', {}, 'Made from curiosity.')) | |
} | |
function App(data) { | |
return h('article', {}, [ | |
Header(data.message), | |
Content(), | |
Footer() | |
]) | |
} | |
window.onload = function () { | |
render = mount(curry(App, data), document.getElementById('app')) | |
} | |
</script> | |
</head> | |
<body> | |
<div id="app"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment