- ViewModel
- binder
- scanner
- client
const ViewModel = class {
static #private = Symbol(); // #을 통해 private로 외부에서 접근불가
static get(data) {
return new ViewModel(this.#private, data); //this는 클래스를 나타냄
}
styles = {};
attributes = {};
properties = {};
events = {};
constructor(checker, data) {
if (checker != ViewModel.#private) throw 'useViewModel.get()!';
Object.entries(data).forEach(([k, v]) => {
switch (k) {
case'styles':
this.styles = v;
break;
case'attributes':
this.attributes = v;
break;
case'properties':
this.properties = v;
break;
case'events':
this.events = v;
break;
default:
this[k] = v;
}
});
Object.seal(this);
}
};
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 Binder = class {
#items = new Set;
add(v, _ = type(v, BinderItem)) {
this.#items.add(v);
}
render(viewmodel, _ = type(viewmodel, ViewModel)) {
this.#items.forEach(item => {
const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el;
Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v);
Object.entries(vm.attributes).forEach(([k, v]) => el.setAttribute(k, v));
Object.entries(vm.properties).forEach(([k, v]) => el[k] = v);
Object.entries(vm.events).forEach(([k, v]) => el['on' + k] = e => v.call(el, e, viewmodel)); // this 를 el 로 바인딩
});
}
};
해당 Binder에서 필요한 기능을 추가하거나 처리하는 기능을 수행한다. (제어역전)
const Scanner = class {
scan(el, _ = type(el, HTMLElement)) {
const binder = newBinder;
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 viewmodel = ViewModel.get({
wrapper: ViewModel.get({styles: {width: '50%', background: '#ffa', cursor: 'pointer'}}),
title: ViewModel.get({properties: {innerHTML: 'Title'}}),
contents: ViewModel.get({properties: {innerHTML: 'Contents'}}),
});
const scanner = new Scanner;
const binder = scanner.scan(document.querySelector('#target'));
binder.render(viewmodel);
const viewmodel = ViewModel.get({
isStop: false,
changeContents() {
this.wrapper.styles.background = `rgb(${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.isStop = true;
},
},
}),
// ...
});
const f = _ => {
viewmodel.changeContents();
binder.render(viewmodel);
if (!viewmodel.isStop) requestAnimationFrame(f);
};
requestAnimationFrame(f);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM</title>
</head>
<body>
<section id="target" data-viewmodel="wrapper">
<h2 data-viewmodel="title"></h2>
<section data-viewmodel="contents"></section>
</section>
<script>
const type = (target, type) => {
if (typeof type == "string") {
if (typeof target != type) throw `invalid type ${target} : ${type}`
} else if (!(target instanceof type)) {
throw `invalid type ${target} : ${type}`
}
return target;
}
const ViewModel = class {
static #private = Symbol()
static get (data) {
return new ViewModel(this.#private, data)
}
styles = {}; attributes = {}; properties = {}; events = {};
constructor(checker, data) {
if (checker != ViewModel.#private) throw 'use ViewModel.get()!'
Object.entries(data).forEach(([k, v]) => {
switch (k) {
case 'styles': this.styles = v; break;
case 'attributes': this.attributes = v; break;
case 'properties': this.properties = v; break;
case 'events': this.events = v; break;
default: this[k] = v;
}
});
Object.seal(this); // Value를 바꿀 순 있지만 Key를 추가할 순 없다.
}
}
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 Binder = class {
#items = new Set()
add (v, _ = type(v, BinderItem)) { this.#items.add(v) }
render (viewmodel, _ = type(viewmodel, ViewModel)) {
this.#items.forEach(item => {
const vm = type(viewmodel[item.viewmodel], ViewModel), el = item.el
Object.entries(vm.styles).forEach(([k, v]) => el.style[k] = v)
Object.entries(vm.attributes).forEach(([k, v]) => el.attribute[k] = v)
Object.entries(vm.properties).forEach(([k, v]) => el[k] = v)
Object.entries(vm.events).forEach(([k, v]) => el[`on${k}`] = e => v.call(el, e, viewmodel))
})
}
}
const Scanner = class {
scan (el, _ = type(el, HTMLElement)) {
const binder = new Binder();
this.checkItem(binder, el)
const stack = [el.firstElementChild]
// HTML 전체에 대한 순회
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 scanner = new Scanner()
const binder = scanner.scan(document.querySelector('#target'))
const getRandom = () => parseInt(Math.random() * 150) + 100
const viewmodel2 = ViewModel.get({
isStop: false,
changeContents () {
this.wrapper.styles.background = `rgb(${getRandom()},${getRandom()},${getRandom()})`
this.contents.properties.innerHTML = Math.random().toString(16).replace('.', '')
binder.render(viewmodel2)
// viewmodel을 갱신하면, binder가 viewmodel을 view에 rendering 한다.
// 즉, '인메모리 객체'만 수정하면 된다.
},
wrapper: ViewModel.get({
styles: { width: '50%', background: '#fff', cursor: 'pointer' },
events: { click(e, vm) { vm.isStop = true } }
}),
title: ViewModel.get({
properties: { innerHTML: 'Title' }
}),
contents: ViewModel.get({
properties: { innerHTML: 'Contents' }
})
})
const f = () => {
viewmodel2.changeContents()
binder.render(viewmodel2)
if (!viewmodel2.isStop) requestAnimationFrame(f)
}
requestAnimationFrame(f)
</script>
</body>
</html>
간단히는 우리는 필요한 viewmodel만 수정함으로써 해당 style, events, properties와 관련한 내용들이 알아서 추가되고 핸들러가 등록되고 style이 적용되는 것을 알 수 있다. (해당 역할들을 Binder에서 처리하는 제어역전이 일어나기때문에)
new Scanner
를 통한 scanner 인스턴스 생성data-viewmodel
속성이 있는지 확인 후 binder를 리턴한 const binder에 저장return new ViewModel()
을 통해 인스턴스 생성binder.render
를 통해 적용시킨다. 해당 Binder는 #items로 이루어져있는데 해당 배열 내용들은 각각 BinderItem이 들어있다. (첫번째 BinderItem의 내용과 Binder의 0번째 배열의 BinderItem의 속성내용은 같다. )
겨우 이해는 되는거 같지만 이런 것을 코드로 작성하라고 하면 정말 어렵고 현재는 못할거같다. => 결국 많이하면 알게되겠지 ... 열심히하자
- HTMLElement를 포함한 내용을 Scanner를 통해 가져온다.
- Scanner.scan을 통해 binder 생성
- checkItem을 실행 => scan함수 안의 while문을 통해 태그안의 태그도 조사하며 checkItem 실행
- checkitem을 통해 data-viewmodel속성을 포함한다면 binder의 내용에 BinderItem 형태로 추가
- return binder를 한다. (BinderItem의 형태의 태그를 binder값에 생성)
- requestAnimationFrame 실행
- viewModel2.changeContents 실행
- binder.render() 실행
- viewModel2가 순수하게 계속 변한다.
- viewModel2가 브라우저에서 클릭하면 멈추어 끝낸다.
이를 통해 HTMLElement가 scanner의 영향을 받아 알아내고 이를 binder와 연관시키고 이를 viewModel에서 필요에따라 바꾸어 처리한다.