This file contains 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
class Zone { | |
constructor() { | |
this.tasks = []; | |
} | |
// Add a new task to the zone | |
add(task) { | |
this.tasks.push(task); | |
} | |
// Run all the tasks in the zone | |
run() { | |
currentZone = this; | |
this.tasks.forEach(task => task.task()); | |
} | |
} | |
var currentZone = new Zone(); | |
class Renderer { | |
// Render the template of a component | |
render(component) { | |
const container = document.getElementById("root"); | |
const template = component.template(); | |
const matchingEl = Array.from(container.children).find(child => child.nodeName === template.nodeName); | |
if (matchingEl) { | |
matchingEl.replaceWith(template); | |
return; | |
} | |
container.appendChild(template); | |
} | |
} | |
class ChangeDetector { | |
constructor(zone, renderer) { | |
this.zone = zone; | |
this.renderer = renderer; | |
} | |
// Detect changes in the components in the application | |
detectChanges() { | |
this.zone.tasks.forEach(task => { | |
if (task.name === "ngDoCheck") { | |
task.task(); | |
this.renderer.render(task.component); | |
} | |
}); | |
} | |
} | |
class BaseComponent { | |
constructor(name) { | |
this.name = name; | |
} | |
// A method that will be called when the component is initialized | |
ngOnInit() { | |
console.log(`Initializing component: ${this.name}`); | |
} | |
// A method that will be called by the ChangeDetector to check for changes in the component | |
ngDoCheck() { | |
console.log(`Checking for changes in component: ${this.name}`); | |
} | |
} | |
class Application { | |
constructor() { | |
this.zone = new Zone(); | |
this.renderer = new Renderer(); | |
this.changeDetector = new ChangeDetector(this.zone, this.renderer); | |
this.zone.add({ task: () => this.tick() }); | |
} | |
// Create a new component and add it to the zone | |
createComponent(component) { | |
this.zone.add({ component, task: () => component.ngOnInit(), name: "ngOnInit" }); | |
this.zone.add({ component, task: () => component.ngDoCheck(), name: "ngDoCheck" }); | |
return component; | |
} | |
// Initialize the components in the zone, render their templates, and detect changes | |
run(component) { | |
this.zone.run(); | |
this.renderer.render(component); | |
this.changeDetector.detectChanges(); | |
} | |
// Run the detectChanges method of the ChangeDetector at regular intervals | |
tick() { | |
this.changeDetector.detectChanges(); | |
} | |
} | |
// This has a LOT of limitations, namely that it's bad | |
// It only handles one element + child text | |
// It only handles (eventName)="$1" and $1 is a templateArgs function syntax | |
function compileToFunction(templateStr, templateArgs) { | |
const matchTemplateArgs = str => { | |
if (str[0] === '$') { | |
const index = Number(str.slice(1)); | |
return templateArgs[index - 1]; | |
} | |
return str; | |
} | |
const openingTagStartIndex = templateStr.indexOf("<"); | |
const openingTagEndIndex = templateStr.indexOf(">", openingTagStartIndex); | |
const openingTagStr = templateStr.slice(openingTagStartIndex, openingTagEndIndex + 1); | |
const [_, elName] = /^<([a-z0-9]+)/.exec(openingTagStr); | |
// [{name: "click", value: "$1"}] | |
const events = []; | |
const eventMatches = openingTagStr.matchAll(/\(([a-z]+)\)="(.*?)"/g); | |
for (const match of eventMatches) { | |
events.push({ name: match[1], value: matchTemplateArgs(match[2]) }); | |
} | |
const closingTagStartIndex = templateStr.indexOf(`</${elName}>`, openingTagEndIndex); | |
const textBetweenTags = matchTemplateArgs(templateStr.slice(openingTagEndIndex + 1, closingTagStartIndex)); | |
const el = document.createElement(elName); | |
for (const { name, value } of events) { | |
el.addEventListener(name, value); | |
} | |
el.innerHTML = textBetweenTags; | |
return el; | |
} | |
class MyComponent extends BaseComponent { | |
constructor() { | |
super("MyComponent"); | |
this.count = 0; | |
this.template = () => compileToFunction(`<button (click)="$1">$2</button>`, [() => { | |
setTimeout(() => { | |
this.count++; | |
}) | |
}, this.count]); | |
} | |
} | |
const app = new Application(); | |
const component = app.createComponent(new MyComponent()); | |
app.run(component); | |
// Change the state of the component and trigger-CD: | |
// component.count += 1; | |
// app.tick(); | |
/* ---- OR ---- */ | |
const setTimeoutPatch = () => { | |
const originalSetTimeout = setTimeout; | |
setTimeout = (callback, delay, ...args) => { | |
const context = this; | |
return originalSetTimeout(() => { | |
callback.apply(context, args); | |
currentZone.run(); | |
}, delay); | |
}; | |
}; | |
setTimeoutPatch(); | |
setTimeout(() => { | |
component.count += 1; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment