Skip to content

Instantly share code, notes, and snippets.

@jasper-lyons
Created April 29, 2019 17:30
Show Gist options
  • Save jasper-lyons/f36c7c2b093fe072edfaf59db95aff2c to your computer and use it in GitHub Desktop.
Save jasper-lyons/f36c7c2b093fe072edfaf59db95aff2c to your computer and use it in GitHub Desktop.
React in 100 lines
<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