Skip to content

Instantly share code, notes, and snippets.

@cannalee90
Forked from hikaMaeng/3.js
Created January 29, 2020 11:08
Show Gist options
  • Select an option

  • Save cannalee90/c104eeeb9d19397fe088f4b8ed77f2b0 to your computer and use it in GitHub Desktop.

Select an option

Save cannalee90/c104eeeb9d19397fe088f4b8ed77f2b0 to your computer and use it in GitHub Desktop.
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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment