|
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; |