Last active
December 18, 2018 01:15
-
-
Save jasonbyrne/aeacbd14f9d1e85fd64ff13af6a6e2a8 to your computer and use it in GitHub Desktop.
Roll your own framework experiment
This file contains hidden or 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
import { isFunction } from 'util'; | |
abstract class Listener { | |
protected listeners: { [eventName: string]: [Function] } = {}; | |
public on(eventName: string, callback: Function) { | |
this.listeners[eventName] = this.listeners[eventName] || []; | |
this.listeners[eventName].push(callback); | |
} | |
protected raise(eventName: string, payloadArgs: any[]) { | |
this.listeners[eventName] = this.listeners[eventName] || []; | |
this.listeners[eventName].forEach((callback: Function) => { | |
callback.apply(this, payloadArgs); | |
}); | |
} | |
} | |
class Data extends Listener { | |
protected data: { [key: string]: any } = {}; | |
constructor(data?: any, prefix: string = '') { | |
super(); | |
if (data) { | |
this.apply(data, prefix); | |
} | |
} | |
protected _set(key: string, value: any) { | |
const oldValue: any = this.data[key]; | |
this.data[key] = value; | |
this.raise.call(this, 'set', [key, value, oldValue]); | |
} | |
public set(key: string, value: any) { | |
if (!/^[a-z0-9]/.test(key)) { | |
throw new Error('Data key must start with a letter or number'); | |
} | |
this._set(key, value); | |
} | |
public get(key: string): any { | |
return this.data[key]; | |
} | |
public apply(data: any, prefix: string = '') { | |
if (typeof data == 'object' || Array.isArray(data)) { | |
for (let key in data) { | |
this._set(prefix + key, data[key]); | |
} | |
} | |
else { | |
this._set('.', data); | |
} | |
} | |
} | |
class Component extends Listener { | |
protected directive: ComponentDirective; | |
protected $element: JQuery; | |
protected dataBindings: string[] = []; | |
constructor(directive: ComponentDirective, $element: JQuery) { | |
super(); | |
this.directive = directive; | |
this.$element = $element; | |
} | |
public element(): JQuery { | |
return this.$element; | |
} | |
public find(selector: string): JQuery { | |
return this.$element.find(selector); | |
} | |
protected addBinding(key: string) { | |
this.dataBindings.push(key); | |
} | |
protected getReplacedHtml(html: string, data: Data, bind: boolean = true) { | |
const compontent: Component = this; | |
html = html.replace(/{{([^}]+)}}/g, function (wholeMatch, key) { | |
key = $.trim(key); | |
var substitution = (function () { | |
return data.get(key); | |
})(); | |
if (bind) { | |
compontent.addBinding(key); | |
} | |
return (substitution === undefined ? wholeMatch : substitution); | |
}); | |
return html; | |
} | |
protected parseTemplate(data: Data): string { | |
const compontent: Component = this; | |
let html: string = this.directive.template().html(); | |
this.dataBindings = []; | |
html = this.getReplacedHtml(html, data); | |
return html; | |
} | |
public render(data: Data) { | |
const compontent: Component = this; | |
this.$element.html(this.parseTemplate(data)); | |
this.$element.find('[each]').each((index: number, element: HTMLElement) => { | |
const $loop: JQuery = $(element); | |
const dataKey: string = $loop.attr('each') || ''; | |
const array: any = data.get(dataKey); | |
if (Array.isArray(array)) { | |
this.addBinding(dataKey) | |
array.forEach((value: any) => { | |
let $item = $loop.clone(); | |
let data: Data = new Data(value, '.'); | |
$item.html( | |
compontent.getReplacedHtml($item.html(), data, false) | |
) | |
$loop.before($item); | |
}); | |
} | |
$loop.remove(); | |
}); | |
this.$element.find('[if]').each((index: number, element: HTMLElement) => { | |
const $if: JQuery = $(element); | |
const statement: string = $if.attr('if') || ''; | |
let isTrue: boolean = false; | |
if (/^[a-z0-9][a-z0-9_]+$/i.test(statement)) { | |
this.addBinding(statement); | |
isTrue = !!data.get(statement); | |
} | |
if (!isTrue) { | |
$if.remove(); | |
} | |
}); | |
this.$element.find('[not]').each((index: number, element: HTMLElement) => { | |
const $not: JQuery = $(element); | |
const statement: string = $not.attr('not') || ''; | |
let isFalse: boolean = true; | |
if (/^[a-z0-9][a-z0-9_]+$/i.test(statement)) { | |
this.addBinding(statement); | |
isFalse = !data.get(statement); | |
} | |
if (!isFalse) { | |
$not.remove(); | |
} | |
}); | |
this.$element.attr('data-bindings', this.dataBindings.join(' ')) | |
.data('component', this); | |
} | |
} | |
class ComponentDirective extends Listener { | |
protected $directive: JQuery; | |
protected name: string; | |
protected href: string; | |
protected $content: JQuery | null = null; | |
protected listeners: { [eventName: string]: [Function] } = {}; | |
constructor(app: App, $directive: JQuery) { | |
super(); | |
this.$directive = $directive; | |
this.name = $directive.attr('name') || ''; | |
this.href = $directive.attr('href') || ''; | |
if (this.name.length == 0) { | |
throw new Error('Must have a name attribute.') | |
} | |
if (this.href.length == 0) { | |
throw new Error('Must have a href attribute.') | |
} | |
} | |
protected find(selector: string): JQuery { | |
if (this.$content === null) { | |
throw new Error('Must load the component first.'); | |
} | |
return this.$content.find(selector); | |
} | |
public root(): JQuery { | |
if (this.$content === null) { | |
throw new Error('Must load the component first.'); | |
} | |
return this.$content; | |
} | |
public template(): JQuery { | |
return this.find('template'); | |
} | |
public load(): Promise<ComponentDirective> { | |
const component: ComponentDirective = this; | |
return new Promise((resolve, reject) => { | |
if (component.$content !== null) { | |
return resolve(component); | |
} | |
$.get(component.href).then(function (response) { | |
component.$content = $('<component>' + response + '</component>'); | |
component.raise('loaded', [component]); | |
resolve(component); | |
}).catch((err) => { | |
reject(err); | |
}); | |
}); | |
} | |
} | |
class App { | |
protected $root: JQuery; | |
public routerView: null | JQuery = null; | |
public components: { [name: string]: ComponentDirective } = {}; | |
public data: Data = new Data(); | |
constructor(rootSelector?: string) { | |
this.$root = $(rootSelector || 'html'); | |
const app = this; | |
this.findComponentDirectives(this.$root); | |
this.$root.bind('DOMNodeInserted', function (e) { | |
let $el = $(e.target); | |
let tagName: string = e.target.tagName.toLocaleLowerCase(); | |
}); | |
this.$root.on('click', 'a[href^="@"]', function (e) { | |
e.preventDefault(); | |
const $el: JQuery = $(this); | |
if (typeof $el != 'undefined') { | |
const link: string | undefined = $el.attr('href'); | |
const target: string = $el.attr('target') || ''; | |
if (typeof link != 'undefined' && link.length > 0) { | |
const directive: ComponentDirective = app.components[link.substr(1)]; | |
if (typeof directive != 'undefined') { | |
const $target = target.length ? $(target) : $('[router-output]'); | |
directive.load().then(() => { | |
app.replaceElement($target, directive); | |
}); | |
} | |
} | |
} | |
}); | |
this.data.on('set', function (key: string) { | |
const elements: JQuery = $('[data-bindings~="' + key + '"]'); | |
elements.each((index: number, element: HTMLElement) => { | |
const component: Component = $(element).data('component'); | |
if (typeof component != 'undefined') { | |
component.render(app.data); | |
} | |
}); | |
}); | |
} | |
public replaceElement($el: JQuery, directive: ComponentDirective) { | |
const app: App = this; | |
const component: Component = new Component(directive, $el.clone()); | |
component.render(app.data); | |
component.find(Object.keys(this.components).join(',')).each(function () { | |
const $replace: JQuery = $(this); | |
const directive: ComponentDirective = app.components[this.tagName.toLocaleLowerCase()]; | |
directive.load().then(() => { | |
const component: Component = new Component(directive, $replace); | |
component.render(app.data); | |
}); | |
}); | |
$el.after(component.element()).remove(); | |
} | |
protected parseTemplate(html: string): string { | |
const app: App = this; | |
html = html.replace(/{{([^}]+)}}/g, function (wholeMatch, key) { | |
var substitution = app.data[$.trim(key)]; | |
return (substitution === undefined ? wholeMatch : substitution); | |
}); | |
return html; | |
} | |
protected findComponents($element) { | |
$element.find(Object.keys(this.components).join(',')) | |
.each((index: number, htmlElement: HTMLElement) => { | |
let componentName: string = htmlElement.tagName.toLocaleLowerCase(); | |
}); | |
} | |
protected findComponentDirectives($container: JQuery) { | |
const app: App = this; | |
$container.find('link[rel~="component"][name][href]') | |
.each((index: number, htmlElement: HTMLElement) => { | |
const $el: JQuery = $(htmlElement); | |
let name: string = $el.attr('name') || ''; | |
let href: string = $el.attr('href') || ''; | |
if (typeof name != 'undefined' && typeof href != 'undefined') { | |
app.foundComponentDirective(name, $el); | |
} | |
}); | |
} | |
protected foundComponentDirective(name: string, $el: JQuery): ComponentDirective { | |
let directive: ComponentDirective = this.components[name]; | |
const app: App = this; | |
if (typeof directive == 'undefined') { | |
directive = new ComponentDirective(app, $el); | |
this.components[name] = directive; | |
// Listen for it to load | |
directive.on('loaded', function () { | |
app.findComponentDirectives(directive.root()); | |
}); | |
// If this component is already in the DOM, replace it right away | |
this.$root.find(name).each(function () { | |
directive.load().then(() => { | |
app.replaceElement($(this), directive); | |
}); | |
}); | |
} | |
return directive; | |
} | |
} |
This file contains hidden or 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
<link rel="component" name="dynamic" href="html/dynamic.html" /> | |
<template> | |
<strong> | |
Contact Us | |
</strong> | |
<matt /> | |
<p> | |
Hi, friends. This is my sample. | |
</p> | |
<a href="@dynamic">Lazy</a> | |
</template> | |
This file contains hidden or 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
<template> | |
<p> | |
This page is a component that was not referenced on the original page. | |
</p> | |
<a href="@contact">back</a> | |
</template> |
This file contains hidden or 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
<template> | |
<p> | |
Hello there, <strong>{{ name }}</strong>! | |
</p> | |
<p if="enabled"> | |
I am enabled | |
</p> | |
<ul if="list"> | |
<li each="list">{{ .id }}) {{ .name }}</li> | |
</ul> | |
<div not="list"> | |
Nothing to list. | |
</div> | |
</template> |
This file contains hidden or 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
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>JS Framework Example</title> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/build/base-min.css"> | |
<link rel="component" name="home" href="html/home.html" path="/" /> | |
<link rel="component" name="contact" href="html/contact.html" path="/contact" /> | |
<link rel="component" name="matt" href="html/matt.html" /> | |
</head> | |
<body> | |
<div id="app"> | |
<header> | |
<h1>JS Framework Example</h1> | |
<nav> | |
<a href="@home">Home</a> | |
<a href="@contact">Contact Us</a> | |
</nav> | |
</header> | |
<main router-output> | |
<home>Loading...</home> | |
</main> | |
</div> | |
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> | |
<script src="app/main.js"></script> | |
<script> | |
let MyApp = new App(); | |
MyApp.data.apply({ | |
'name': 'Jason Byrne', | |
'enabled': false, | |
'list': [ | |
{ id: 1, name: 'Florida' }, | |
{ id: 2, name: 'Georgia' }, | |
{ id: 3, name: 'Alabama' } | |
] | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or 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
<template> | |
<b>{{ name }} is awesome!!</b> | |
</template> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment