const type = (target, type) => {
if (typeof type == 'string') {
if (typeof target != type) throw `invaild type ${target} : ${type}`;
} else if (!(target instanceof type)) throw `invaild type ${target} : ${type}`;
return target;
};
const ViewModelListener = class {
viewmodelUpdated(updated) {
throw 'override';
}
};
const ViewModelValue = class {
subKey;
cat;
k;
v;
constructor(subKey, cat, k, v) {
this.subKey = subKey;
this.cat = cat;
this.k = k;
this.v = v;
Object.freeze(this);
}
};
const ViewModel = class extends ViewModelListener {
static get(data) {
return new ViewModel(data);
}
static #subjects = new Set;
static #inited = false;
static notify(vm) {
this.#subjects.add(vm);
if (this.#inited) return;
this.#inited = true;
const f = _ => {
this.#subjects.forEach(vm => {
if (vm.#isUpdated.size) {
vm.notify();
vm.#isUpdated.clear();
}
});
requestAnimationFrame(f);
};
requestAnimationFrame(f);
}
static define(vm, cat, obj) {
return Object.defineProperties(obj, Object.entries(obj).reduce((r, [k, v]) => {
r[k] = {
enumerable: true,
get: _ => v,
set: newV => {
v = newV;
vm.#isUpdated.add(new ViewModelValue(vm.subKey, cat, k, v));
},
};
return r;
}, {}));
}
styles = {};
attributes = {};
properties = {};
events = {};
subKey = '';
parent = null;
#isUpdated = new Set;
#listeners = new Set;
constructor(data, _ = type(data, 'object')) {
super();
Object.entries(data).forEach(([k, v]) => {
if ('styles,attributes,properties'.includes(k)) {
if (!v || typeof v != 'object') throw `invalid object k:${k}, v:${v}`;
this[k] = ViewModel.define(this, k, v);
} else {
Object.defineProperty(this, k, {
enumerable: true,
get: _ => v,
set: newV => {
v = newV;
this.#isUpdated.add(new ViewModelValue(this.subKey, '', k, v));
},
});
if (v instanceof ViewModel) {
v.parent = this;
v.subKey = k;
v.addListener(this);
}
}
});
ViewModel.notify(this);
Object.seal(this);
}
viewmodelUpdated(updated) {
updated.forEach(v => this.#isUpdated.add(v));
}
addListener(v, _ = type(v, ViewModelListener)) {
this.#listeners.add(v);
}
removeListener(v, _ = type(v, ViewModelListener)) {
this.#listeners.delete(v);
}
notify() {
this.#listeners.forEach(v => v.viewmodelUpdated(this.#isUpdated));
}
};
const Scanner = class {
scan(el, _ = type(el, HTMLElement)) {
const binder = new Binder;
this.checkItem(binder, el);
const stack = [el.firstElementChild];
let target;
while (target = stack.pop()) {
this.checkItem(binder, target);
if (target.firstElementChild) stack.push(target.firstElementChild);
if (target.nextElementSibling) stack.push(target.nextElementSibling);
}
return binder;
}
checkItem(binder, el) {
const vm = el.getAttribute('data-viewmodel');
if (vm) binder.add(new BinderItem(el, vm));
}
};
const Processor = class {
cat;
constructor(cat) {
this.cat = cat;
Object.freeze(this);
}
process(vm, el, k, v, _0 = type(vm, ViewModel), _1 = type(el, HTMLElement), _2 = type(k, 'string')) {
this._process(vm, el, k, v);
}
_process(vm, el, k, v) {
throw 'override';
}
};
const Binder = class extends ViewModelListener {
#items = new Set;
#processors = {};
add(v, _ = type(v, BinderItem)) {
this.#items.add(v);
}
viewmodelUpdated(updated) {
const items = {};
this.#items.forEach(item => {
items[item.viewmodel] = [
type(viewmodel[item.viewmodel], ViewModel),
item.el,
];
});
updated.forEach(v => {
if (!items[v.subKey]) return;
const [vm, el] = items[v.subKey], processor = this.#processors[v.cat];
if (!el || !processor) return;
processor.process(vm, el, v.k, v.v);
});
}
addProcessor(v, _0 = type(v, Processor)) {
this.#processors[v.cat] = v;
}
watch(viewmodel, _ = type(viewmodel, ViewModel)) {
viewmodel.addListener(this);
this.render(viewmodel);
}
unwatch(viewmodel, _ = type(viewmodel, ViewModel)) {
viewmodel.removeListener(this);
}
render(viewmodel, _ = type(viewmodel, ViewModel)) {
const processores = Object.entries(this.#processors);
this.#items.forEach(item => {
const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el;
processores.forEach(([pk, processor]) => {
Object.entries(vm[pk]).forEach(([k, v]) => {
processor.process(vm, el, k, v);
});
});
});
}
};
const BinderItem = class {
el;
viewmodel;
constructor(el, viewmodel, _0 = type(el, HTMLElement), _1 = type(viewmodel, 'string')) {
this.el = el;
this.viewmodel = viewmodel;
Object.freeze(this);
}
};
const scanner = new Scanner;
const binder = scanner.scan(document.querySelector('#target'));
binder.addProcessor(new (class extends Processor {
_process(vm, el, k, v) {
el.style[k] = v;
}
})('styles'));
binder.addProcessor(new (class extends Processor {
_process(vm, el, k, v) {
el.setAttribute(k, v);
}
})('attributes'));
binder.addProcessor(new (class extends Processor {
_process(vm, el, k, v) {
el[k] = v;
}
})('properties'));
binder.addProcessor(new (class extends Processor {
_process(vm, el, k, v) {
console.log('event', k, v, el);
el['on' + k] = e => v.call(el, e, vm);
}
})('events'));
const viewmodel = ViewModel.get({
isStop: false,
changeContents() {
this.wrapper.styles.background = `rgb(${parseInt(Math.random() * 150) + 100},${parseInt(Math.random() * 150) + 100},${parseInt(Math.random() * 150) + 100})`;
this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '');
},
wrapper: ViewModel.get({
styles: {
width: '50%',
background: '#ffa',
cursor: 'pointer',
},
events: {
click(e, vm) {
vm.parent.isStop = true;
console.log('click', vm);
},
},
}),
title: ViewModel.get({
properties: {
innerHTML: 'Title',
},
}),
contents: ViewModel.get({
properties: {
innerHTML: 'Contents',
},
}),
});
binder.watch(viewmodel);
const f = _ => {
viewmodel.changeContents();
if (!viewmodel.isStop) requestAnimationFrame(f);
};
requestAnimationFrame(f);