Skip to content

Instantly share code, notes, and snippets.

@jasonbyrne
Last active December 18, 2018 01:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonbyrne/aeacbd14f9d1e85fd64ff13af6a6e2a8 to your computer and use it in GitHub Desktop.
Save jasonbyrne/aeacbd14f9d1e85fd64ff13af6a6e2a8 to your computer and use it in GitHub Desktop.
Roll your own framework experiment
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;
}
}
<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>
<template>
<p>
This page is a component that was not referenced on the original page.
</p>
<a href="@contact">back</a>
</template>
<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>
<!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/purecss@1.0.0/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>
<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