Skip to content

Instantly share code, notes, and snippets.

@crutchcorn
Last active December 8, 2022 08:29
Show Gist options
  • Save crutchcorn/bdd9b82e95a3776c313640f7cfd95938 to your computer and use it in GitHub Desktop.
Save crutchcorn/bdd9b82e95a3776c313640f7cfd95938 to your computer and use it in GitHub Desktop.
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