/
example2.html
120 lines (112 loc) · 3.57 KB
/
example2.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<!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>