Skip to content

Instantly share code, notes, and snippets.

@Xananax
Created May 1, 2016 20:04
Show Gist options
  • Save Xananax/1e029f33dd7952b16617ae9c95e83c73 to your computer and use it in GitHub Desktop.
Save Xananax/1e029f33dd7952b16617ae9c95e83c73 to your computer and use it in GitHub Desktop.
React-like stupid and dirty 180 LOC implementation

Just a little quick and dirty implementation of a react-like interface. OBVIOUSLY not for actual usage, probably buggy as hell and not tested further than the little example provided.

I just wanted to get my hands dirty with typescript to see a bit how it works.

"use strict";
function isPlainObject(obj) {
return obj !== null && typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype;
}
exports.isPlainObject = isPlainObject;
function isFunction(obj) {
return obj && (typeof obj == 'function');
}
exports.isFunction = isFunction;
var renderStyle = function (style) {
if (!style) {
return '';
}
return Object.keys(style).map(function (key) { return (key + ":" + style[key]); }).join(';');
};
var addElementProperty = function (el, descriptor) {
var name = descriptor[0];
var prop = descriptor[1];
var value = (name == 'class') ? prop.join(' ') :
(name == 'style') ? renderStyle(prop) :
prop;
if (name.match(/^on/) && isFunction(value)) {
var evtName = name.replace(/^on/, '').toLowerCase();
el.addEventListener(evtName, value, false);
}
else if (value) {
el.setAttribute(name, value);
}
};
var render = function (dom, mountPoint) {
var tag = dom.tag;
var children = dom.children;
var properties = dom.properties;
var onMount = dom.onMount;
var text = dom.text;
var el = document.createElement(tag);
properties && properties.length && properties.forEach(function (descriptor) {
addElementProperty(el, descriptor);
});
mountPoint.appendChild(el);
var childNodes = children && children.length && children.map(function (child) {
return child.render(child, el);
});
if (text) {
el.appendChild(document.createTextNode(text));
}
dom.render = update.bind(this, el, childNodes, dom);
if (onMount) {
onMount(el);
}
return el;
};
var update = function (el, childNodes, dom, newDom) {
var changed = false;
var tag = dom.tag;
var children = dom.children;
var properties = dom.properties;
var text = dom.text;
var onMount = dom.onMount;
var newTag = newDom.tag;
var newChildren = newDom.children;
var newProperties = newDom.properties;
var newText = newDom.text;
if (newTag != tag) {
throw new Error('tag change not supported');
}
properties && properties.length && properties.forEach(function (descriptor, index) {
if (newProperties.indexOf(descriptor) < 0) {
if (descriptor[0].match(/^on/)) {
el.removeEventListener(descriptor[0].replace(/^on/, '').toLowerCase(), descriptor[1], false);
}
el.removeAttribute(descriptor[0]);
changed = true;
}
});
newProperties && newProperties.length && newProperties.forEach(function (descriptor, index) {
if (properties && descriptor !== properties[index]) {
addElementProperty(el, descriptor);
changed = true;
}
});
children && children.length && children.forEach(function (child, index) {
if (newChildren.indexOf(child) < 0) {
var node = childNodes[index];
el.removeChild(node);
changed = true;
}
;
});
var newChildNodes = newChildren && newChildren.length && newChildren.map(function (child, index) {
if (!children || index >= children.length) {
changed = true;
return child.render(child, el);
}
else if (child !== children[index]) {
changed = true;
var c = child.render(child, el);
el.insertBefore(c, childNodes[index]);
el.removeChild(childNodes[index]);
}
return childNodes[index];
});
if (newText != text) {
el.textContent = newText;
changed = true;
}
dom.render = update.bind(this, el, newChildNodes, newDom);
if (changed && onMount) {
onMount(el);
}
return el;
};
exports.script = function (fn) {
var counter = 0;
var onMount = function (obj) {
fn(obj, counter);
counter++;
};
onMount.___isScript = true;
};
var wrapCustomRenderer = function (tag) {
var previous = null;
var previousHTMLElement = null;
var hasRanOnce = false;
return function customRender(dom, mountPoint) {
var el = tag(dom);
if (!hasRanOnce || el == !previous) {
hasRanOnce = true;
previous = el;
previousHTMLElement = el.render(dom, mountPoint);
}
return previousHTMLElement;
};
};
var Dom = function (n) {
if (typeof n == 'string') {
var ret = {
tag: 'span',
render: render,
properties: null,
children: null,
text: n,
onMount: null
};
return ret;
}
var length = n.length;
if (!n.length) {
return null;
}
var _a = n, tag = _a[0], rest = _a.slice(1);
var tagIsRender = isFunction(tag);
var renderable = {
tag: (tagIsRender ? 'div' : tag),
render: (tagIsRender ? wrapCustomRenderer(tag) : render),
properties: null,
children: null,
onMount: null
};
if (rest.length) {
var properties = rest[0], _children = rest.slice(1);
renderable.properties = properties;
var lastChild = _children.length && _children[_children.length - 1];
if (lastChild && typeof lastChild == 'object' && '___isScript' in lastChild) {
var onMount = _children.pop();
renderable.onMount = onMount;
}
var children = _children.map(function (child) { return Dom(child); });
renderable.children = children;
}
return renderable;
};
exports.mount = function (obj, mountPoint) {
return function () {
obj.render(obj, mountPoint);
};
};
exports.__esModule = true;
exports["default"] = Dom;
type Tag = string|TransformRenderable;
interface TransformRenderable{
(RenderableDomObject):RenderableDomObject
}
interface Style{
[type:string]:string|number
}
interface Property<T> extends Array<any>{
0:T;
1:any;
}
interface classNameProperty extends Property<'class'>{
1:string[];
}
interface StyleProperty extends Property<'style'>{
1:Style;
}
interface EventHandler<T> extends Property<T>{
1:EventListener;
}
interface onClickProperty extends EventHandler<'onClick'>{}
interface Properties extends Array<ValidProperty>{}
type ValidProperty = classNameProperty|StyleProperty|onClickProperty;
type Children = RenderableDomObject[];
type DomArray = [Tag] | [Tag,Properties] | [Tag,Properties,any];
interface Renderer{
(RenderableDomObject,Node):HTMLElement
}
interface onMountHandler{
(obj:Node):void;
___isScript?:boolean;
}
interface RenderableDomObject{
tag?:Tag;
children?:Children;
properties?:Properties;
render?:Renderer;
text?:string
onMount?:Function;
}
type Domable = string | DomArray;
export function isPlainObject(obj:any):boolean{
return obj !== null && typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype;
}
export function isFunction(obj?:any):boolean{
return obj && (typeof obj=='function');
}
const renderStyle = function(style:Style):string{
if(!style){return '';}
return Object.keys(style).map(key=>`${key}:${style[key]}`).join(';');
}
const addElementProperty = function(el:HTMLElement,descriptor:ValidProperty){
const name:string = descriptor[0];
const prop:any = descriptor[1];
let value:any = (name == 'class') ? prop.join(' ') :
(name == 'style') ? renderStyle(<Style>prop) :
prop
;
if(name.match(/^on/) && isFunction(value)){
const evtName = name.replace(/^on/,'').toLowerCase();
el.addEventListener(evtName,value,false);
}
else if(value){
el.setAttribute(name,value);
}
}
const render:Renderer = function(dom:RenderableDomObject,mountPoint:Node):HTMLElement{
const tag:Tag = dom.tag;
const children:Children = dom.children;
const properties:Properties = dom.properties;
const onMount:Function = dom.onMount;
const text:string = dom.text;
const el = document.createElement(<string>tag);
properties && properties.length && properties.forEach(function(descriptor:ValidProperty){
addElementProperty(el,descriptor);
});
mountPoint.appendChild(el);
const childNodes:HTMLElement[] = children && children.length && children.map(function(child:RenderableDomObject){
return child.render(child,el);
});
if(text){
el.appendChild(document.createTextNode(text));
}
dom.render = update.bind(this,el,childNodes,dom);
if(onMount){
onMount(el);
}
return el;
}
const update = function(el:HTMLElement,childNodes:HTMLElement[],dom:RenderableDomObject,newDom:RenderableDomObject):HTMLElement{
let changed = false;
const tag:Tag = dom.tag;
const children:Children = dom.children;
const properties:Properties = dom.properties;
const text:string = dom.text;
const onMount:Function = dom.onMount;
const newTag:Tag = newDom.tag;
const newChildren:Children = newDom.children;
const newProperties:Properties = newDom.properties;
const newText:string = newDom.text;
if(newTag!=tag){
throw new Error('tag change not supported');
}
properties && properties.length && properties.forEach(function(descriptor:ValidProperty,index:number){
if(newProperties.indexOf(descriptor)<0){
if(descriptor[0].match(/^on/)){
el.removeEventListener(descriptor[0].replace(/^on/,'').toLowerCase(),<EventListener>descriptor[1],false);
}
el.removeAttribute(descriptor[0]);
changed = true;
}
})
newProperties && newProperties.length && newProperties.forEach(function(descriptor:ValidProperty,index:number){
if(properties && descriptor!==properties[index]){
addElementProperty(el,descriptor);
changed = true;
}
});
children && children.length && children.forEach(function(child:RenderableDomObject,index:number){
if(newChildren.indexOf(child)<0){
const node = childNodes[index];
el.removeChild(node);
changed = true;
};
})
const newChildNodes:HTMLElement[] = newChildren && newChildren.length && newChildren.map(function(child:RenderableDomObject,index:number){
if(!children || index >= children.length){
changed = true;
return child.render(child,el);
}
else if(child!==children[index]){
changed = true;
const c = child.render(child,el);
el.insertBefore(c,childNodes[index]);
el.removeChild(childNodes[index]);
}
return childNodes[index];
});
if(newText!=text){
el.textContent = newText;
changed = true;
}
dom.render = update.bind(this,el,newChildNodes,newDom);
if(changed && onMount){
onMount(el);
}
return el;
}
export const script = function(fn:Function){
let counter = 0;
const onMount:onMountHandler = function(obj):void{
fn(obj,counter);
counter++;
}
onMount.___isScript = true;
}
const wrapCustomRenderer = function(tag):Renderer{
let previous:RenderableDomObject = null;
let previousHTMLElement:HTMLElement = null;
let hasRanOnce = false;
return function customRender(dom:RenderableDomObject,mountPoint:Node):HTMLElement{
const el:RenderableDomObject = tag(dom);
if(!hasRanOnce || el ==! previous){
hasRanOnce = true;
previous = el;
previousHTMLElement = el.render(dom,mountPoint);
}
return previousHTMLElement;
}
}
const Dom = function(n:Domable):RenderableDomObject{
if(typeof n == 'string'){
const ret:RenderableDomObject = {
tag:'span'
, render:render
, properties:null
, children:null
, text:<string>n
, onMount:null
};
return ret;
}
const {length} = n;
if(!n.length){return null;}
const [tag,...rest] = <DomArray>n;
const tagIsRender = isFunction(tag);
const renderable:RenderableDomObject = {
tag:(tagIsRender ? 'div' : <string>tag)
, render:<Renderer>(tagIsRender ? wrapCustomRenderer(tag) : render)
, properties:null
, children:null
, onMount:null
}
if(rest.length){
const [properties,..._children] = rest;
renderable.properties = <Properties>properties;
const lastChild = _children.length && <any[]>_children[_children.length-1];
if(lastChild && typeof lastChild == 'object' && '___isScript' in lastChild){
const onMount:Function = <Function>_children.pop();
renderable.onMount = onMount;
}
const children:Children = _children.map((child:Domable)=>Dom(child));
renderable.children = children;
}
return renderable;
}
export const mount = function(obj:RenderableDomObject,mountPoint:Node){
return function(){
obj.render(obj,mountPoint);
}
}
export default Dom;
import Dom,{mount,script} from './deact.js';
const obj = Dom([
'div'
, [
['class',['a','b','c']]
, ['style',{'background':'red'}]
]
, ['div'
, [
['onClick',function(evt){alert('works!');}]
]
, ['p',[]
, 'some text'
]
, ['p',[]
,
'another text'
]
]
]);
mount(obj,document.body)();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment