-
Notifications
You must be signed in to change notification settings - Fork 5
/
advanced.html
301 lines (277 loc) · 36.5 KB
/
advanced.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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<!DOCTYPE html>
<html lang="en"><head>
<link rel="icon" href="/logo.svg">
<title>VanJS - Advanced Topics</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/poppins/v20/pxiEyp8kv8JHgFVrJJfedw.ttf) format('truetype');
}
</style>
<link rel="stylesheet" href="/code/w3-v1.css">
<link rel="stylesheet" href="/code/prism-v1.css">
<link rel="stylesheet" href="/vanjs.css">
</head>
<body class="line-numbers" data-prismjs-copy="📋">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q0NB75RY7E"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Q0NB75RY7E');
</script>
<script type="text/javascript" src="/code/prism-v1.js" defer></script><script type="text/javascript" src="/code/van-1.5.2.nomodule.min.js" defer></script><script type="text/javascript" src="advanced.js" defer></script>
<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-red w3-collapse w3-top w3-large w3-padding" style="z-index:3;width:280px;font-weight:bold;" id="mySidebar"><br>
<a href="javascript:void(0)" onclick="w3_close()" class="w3-button w3-hide-large w3-display-topleft" style="width:100%;font-size:22px">Close Menu</a>
<div class="w3-container">
<h1 class="w3-padding-16 w3-xxxlarge">
<img src="/logo.svg" alt="VanJS" width="192px" height="192px">
</h1>
</div>
<div id="nav" class="w3-bar-block"><a href="/" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">Home</a><a href="/start" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">Getting Started</a><a href="/tutorial" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">Tutorial</a><a href="/demo" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">VanJS by Example</a><a href="/convert" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">HTML/MD to VanJS</a><a href="/vanui" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">VanUI</a><a href="/minivan" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">Mini-Van</a><a href="/ssr" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">SSR & Hydration</a><a href="/x" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">X</a><a href="/advanced" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white current">Advanced Topics</a><a href="/media" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">Media Coverage</a><a href="/about" onclick="w3_close()" class="w3-bar-item w3-button w3-hover-white">About</a></div>
</nav>
<!-- Top menu on small screens -->
<header class="w3-container w3-top w3-hide-large w3-red w3-xlarge w3-padding">
<a href="javascript:void(0)" class="w3-button w3-red w3-margin-right" onclick="w3_open()">☰</a>
<span id="title-bar">Advanced Topics</span>
</header>
<!-- Overlay effect when opening sidebar on small screens -->
<div class="w3-overlay w3-hide-large" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>
<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:300px;">
<div id="page">
<div id="content"><h1 class="w3-xxlarge"><b>VanJS</b>: Advanced Topics</h1><blockquote><i>Everything should be made as simple as possible, but not simpler.<br><br>-- Albert Einstein</i></blockquote><h2 class="w3-xxlarge w3-text-red" id="dom-attributes-vs-properties"><a class="self-link" href="#dom-attributes-vs-properties">DOM Attributes vs. Properties</a></h2><hr style="width:50px;border:5px solid red" class="w3-round"><p>In <code class="symbol"><a href="/tutorial#api-tags" class="w3-hover-opacity">tag functions</a></code>, while assigning values from <code class="symbol">props</code> parameter to the created HTML element, there are 2 ways of doing it: via <code class="symbol"><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes" class="w3-hover-opacity">HTML attributes</a></code> (<code class="language-js">dom.setAttribute(<key>, <value>)</code>), or via the properties of the created HTML element (<code class="language-js">dom[<key>] = <value></code>). <b>VanJS</b> follows a consistent rule that makes sense for most use cases regarding which option to choose: when a settable property exists in a given <code class="symbol"><key></code> for the specific element type, we will assign the value via property, otherwise we will assign the value via attribute.</p><p>For instance, <code class="language-js">input({type: "text", value: "Hello 🍦VanJS"})</code> will create an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text" class="w3-hover-opacity">input box</a> with <code class="symbol">Hello 🍦VanJS</code> as the value of the <code class="symbol">value</code> property, while <code class="language-js">div({"data-index": 1})</code> will create the tag: <code class="language-html"><div data-index="1"></div></code>.</p><p>Note that, for readonly properties of HTML elements, we will still assign <code class="symbol">props</code> values via <code class="symbol">setAttribute</code>. For instance, in the code snippet below, the <code class="symbol">list</code> of the <code class="language-html"><input></code> element is set via <code class="symbol">setAttribute</code>:</p><pre><code class="language-js">const Datalist = () => div(
label({for: "ice-cream-choice"}, "Choose a flavor: "),
input({
list: "ice-cream-flavors",
id: "ice-cream-choice",
name: "ice-cream-choice",
}),
datalist(
{id: "ice-cream-flavors"},
option({value: "Chocolate"}),
option({value: "Coconut"}),
option({value: "Mint"}),
option({value: "Strawberry"}),
option({value: "Vanilla"}),
)
)
</code></pre><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/readonly-prop">Try on jsfiddle</a></p><p><b>NOTE:</b> for <b>Mini-Van</b>, since <code class="symbol">0.4.0</code>, we consistently assign the <code class="symbol">props</code> values via <code class="symbol">setAttribute</code> for all property keys in tag functions. This is because for SSR (server-side rendering), which is <b>Mini-Van</b>'s primary use case, setting the properties of a DOM node won't be visible in the rendered HTML string unless the action of setting the property itself will also set the corresponding HTML attribute (e.g.: setting the <code class="symbol">id</code> property of a DOM node will also set the <code class="symbol">id</code> attribute). This is helpful as <code class="language-js">input({type: "text", value: "value"})</code> can be rendered as <code class="language-html"><input type="text" value="value"></code> in <b>Mini-Van</b> but would be rendered as <code class="language-html"><input type="text"></code> if we set the property value via DOM property.</p><h2 class="w3-xxlarge w3-text-red" id="state-and-state-binding"><a class="self-link" href="#state-and-state-binding">State and State Binding</a></h2><hr style="width:50px;border:5px solid red" class="w3-round"><h3 class="w3-large w3-text-red" id="why-not-dom-valued-states"><a class="self-link" href="#why-not-dom-valued-states">Why can't states have DOM node as values?</a></h3><p>We might be prompted to assign a DOM node to a <code class="symbol">State</code> object, especially when the <code class="symbol">State</code> object is used as a <code class="symbol">State</code>-typed child node. However, this is problematic when the state is bound in multiple places, like the example below:</p><pre><code class="language-js">const TurnBold = () => {
const vanJS = van.state("VanJS")
return span(
button({onclick: () => vanJS.val = b("VanJS")}, "Turn Bold"),
" Welcome to ", vanJS, ". ", vanJS, " is awesome!"
)
}
</code></pre><p><b>Demo:</b> <span id="demo-dom-valued-state"></span></p><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/dom-valued-state">Try on jsfiddle</a></p><p>In this example, if we click the "Turn Bold" button, the first "<b>VanJS</b>" text will disappear, which is unexpected. This is because the same DOM node <code class="language-js">b("VanJS")</code> is used twice in the DOM tree. For this reason, an error will be thrown in <code class="symbol">van-{version}.debug.js</code> whenever we assign a DOM node to a <code class="symbol">State</code> object.</p><h3 class="w3-large w3-text-red" id="state-granularity"><a class="self-link" href="#state-granularity">State granularity</a></h3><p>Whenever possible, it's encouraged to define states at a more granular level. That is, it's recommended to define states like this:</p><pre><code class="language-js">const appState = {
a: van.state(1),
b: van.state(2),
}
</code></pre><p>instead of this:</p><pre><code class="language-js">const appState = van.state({
a: 1,
b: 2,
})
</code></pre><p>More granular <code class="symbol">State</code> objects can help state bindings be more locally scoped, which make reactive UI updates more efficient by eliminating unnecessary DOM tree construction and replacement.</p><h3 class="w3-large w3-text-red" id="minimize-the-scope-of-dom-updates"><a class="self-link" href="#minimize-the-scope-of-dom-updates">Minimize the scope of DOM updates</a></h3><p>It's encouraged to organize your code in way that the scope of DOM updates is minimized upon the changes of <code class="symbol">State</code> objects. For instance, the 2 components below (<code class="symbol">Name1</code> and <code class="symbol">Name2</code>) are semantically equivalent:</p><pre><code class="language-js">const name = van.state("")
const Name1 = () => div(() => name.val.trim().length === 0 ?
p("Please enter your name") :
p("Hello ", b(name)),
)
const Name2 = () => {
const isNameEmpty = van.derive(() => name.val.trim().length === 0)
return div(() => isNameEmpty.val ?
p("Please enter your name") :
p("Hello ", b(name)),
)
}
</code></pre><p>But <code class="symbol">Name2</code>'s implementation is more preferable. With <code class="symbol">Name1</code>'s implementation, the entire <code class="language-html"><p></code> element will be refreshed whenever <code class="symbol">name</code> state is updated. This is because the entire <code class="language-html"><p></code> element is bound to <code class="symbol">name</code> state as specified in the binding function. On the other hand, with <code class="symbol">Name2</code>'s implementation, the <code class="language-html"><p></code> element is only refreshed when <code class="symbol">name</code> state is changed from empty to non-empty, or vice versa, as the <code class="language-html"><p></code> element is bound to derived state - <code class="symbol">isNameEmpty</code>. For other changes to <code class="symbol">name</code> state, only the <code class="symbol">Text node</code> inside the <code class="language-html"><b></code> element will be refreshed.</p><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/minimize-dom-updates">Try on jsfiddle</a></p><h3 class="w3-large w3-text-red" id="advanced-state-derivation"><a class="self-link" href="#advanced-state-derivation">Advanced state derivation</a></h3><blockquote><i>道生一,一生二,二生三,三生万物<br>(Tao derives one, one derives two, two derive three, and three derive everything)<br><br>-- 老子,道德经</i></blockquote><p>A broad set of advanced state derivation (derived states and side effects) can indeed be defined with <code class="symbol"><a href="/tutorial#api-derive" class="w3-hover-opacity">van.derive</a></code>, as illustrated in the following piece of code:</p><pre><code class="language-js">const fullName = van.state(localStorage.getItem("fullName") ?? "Tao Xin")
// State persistence with `localStorage`
van.derive(() => localStorage.setItem("fullName", fullName.val))
// Defining multiple derived states
const firstName = van.state(), lastName = van.state()
van.derive(() => [firstName.val, lastName.val] = fullName.val.split(" "))
const elapsed = van.state(0)
setInterval(() => elapsed.val += .01, 10)
// Same as `elapsed`, but delay the state propagation by 1s
const delayed = van.state(0)
van.derive(() => setTimeout(v => delayed.val = v, 1000, elapsed.val))
// Same as `elapsed`, but throttle the state update to every 100ms
const throttled = van.state(0)
setInterval(() => throttled.val = elapsed.val, 100)
// Generate a data stream for all value updates of a given state `s`
const streamOf = s => {
let resolver
van.derive(() => resolver ? resolver({value: s.val, done: false}) : s.val)
return {
[Symbol.asyncIterator]: () => ({
next: () => new Promise(resolve => resolver = resolve)
})
}
}
(async () => {
// To subscribe the data stream
for await (const v of streamOf(throttled)) {
console.log("elapsed: ", v)
}
// You can also chain the data stream with `map`, `filter`, etc. by integrating with
// rubico (https://rubico.land) or wu.js (https://fitzgen.github.io/wu.js/).
})()
</code></pre><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/advanced-state-derivation">Try on jsfiddle</a></p><h3 class="w3-large w3-text-red" id="conditional-state-binding"><a class="self-link" href="#conditional-state-binding">Conditional state binding</a></h3><p>In <a href="/tutorial#state-derived-prop" class="w3-hover-opacity"><code class="symbol">State</code>-derived properties</a> and <a href="/tutorial#state-derived-child" class="w3-hover-opacity"><code class="symbol">State</code>-derived child nodes</a>, it is guaranteed that the binding function will (only) be triggered when the dependency states change. This is true even for complex binding functions, who have different dependency states under different conditions.</p><p>For instance, the binding function <code class="language-js">() => cond.val ? a.val + b.val : c.val + d.val</code> will (only) be triggered by updates of state <code class="symbol">a</code>, <code class="symbol">b</code> and <code class="symbol">cond</code> if <code class="symbol">cond.val</code> is true, and will (only) be triggered by updates of state <code class="symbol">c</code>, <code class="symbol">d</code> and <code class="symbol">cond</code> if <code class="symbol">cond.val</code> is false. This can be illustrated with the code below:</p><pre><code class="language-js">const ConditionalBinding = () => {
const formula = van.state("a + b")
const a = van.state(1), b = van.state(2), c = van.state(3), d = van.state(4)
const triggeredTimes = new Text(0)
return div(
div(
"formula: ",
select({oninput: e => formula.val = e.target.value},
option({selected: () => formula.val === "a + b"}, "a + b"),
option({selected: () => formula.val === "c + d"}, "c + d"),
),
" a: ",
input({type: "number", min: 0, max: 9, value: a, oninput: e => a.val = Number(e.target.value)}),
" b: ",
input({type: "number", min: 0, max: 9, value: b, oninput: e => b.val = Number(e.target.value)}),
" c: ",
input({type: "number", min: 0, max: 9, value: c, oninput: e => c.val = Number(e.target.value)}),
" d: ",
input({type: "number", min: 0, max: 9, value: d, oninput: e => d.val = Number(e.target.value)}),
),
div("sum: ", () => {
triggeredTimes.textContent = Number(triggeredTimes.textContent) + 1
return formula.val === "a + b" ? a.val + b.val : c.val + d.val
}),
div("Binding function triggered: ", triggeredTimes, " time(s)"),
)
}
</code></pre><p><b>Demo:</b></p><p id="demo-conditional-binding"></p><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/conditional-binding">Try on jsfiddle</a></p><p>Conditional state binding works for <a href="/tutorial#derived-state" class="w3-hover-opacity">derived states</a> and <a href="/tutorial#side-effect" class="w3-hover-opacity">side effects</a> registered via <code class="symbol">van.derive</code> as well:</p><pre><code class="language-js">const ConditionalDerive = () => {
const formula = van.state("a + b")
const a = van.state(1), b = van.state(2), c = van.state(3), d = van.state(4)
const triggeredTimes = new Text(0)
const sum = van.derive(() => {
triggeredTimes.textContent = Number(triggeredTimes.textContent) + 1
return formula.val === "a + b" ? a.val + b.val : c.val + d.val
})
return div(
div(
"formula: ",
select({oninput: e => formula.val = e.target.value},
option({selected: () => formula.val === "a + b"}, "a + b"),
option({selected: () => formula.val === "c + d"}, "c + d"),
),
" a: ",
input({type: "number", min: 0, max: 9, value: a, oninput: e => a.val = Number(e.target.value)}),
" b: ",
input({type: "number", min: 0, max: 9, value: b, oninput: e => b.val = Number(e.target.value)}),
" c: ",
input({type: "number", min: 0, max: 9, value: c, oninput: e => c.val = Number(e.target.value)}),
" d: ",
input({type: "number", min: 0, max: 9, value: d, oninput: e => d.val = Number(e.target.value)}),
),
div("sum: ", sum),
div("Binding function triggered: ", triggeredTimes, " time(s)"),
)
}
</code></pre><p><b>Demo:</b></p><p id="demo-conditional-derive"></p><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/conditional-derive">Try on jsfiddle</a></p><h3 class="w3-large w3-text-red" id="self-referencing-in-side-effects"><a class="self-link" href="#self-referencing-in-side-effects">Self-referencing in side effects</a></h3><blockquote><i>The barber is the "one who shaves all those, and those only, who do not shave themselves". The question is, does the barber shave himself?<br><br>-- Bertrand Russell, Barber paradox</i></blockquote><p>Sometimes side effects could lead to trick situations:</p><pre><code class="language-js">const CheckboxCounter = () => {
const checked = van.state(false), numChecked = van.state(0)
van.derive(() => {
if (checked.val) ++numChecked.val
})
return div(
input({type: "checkbox", checked, onclick: e => checked.val = e.target.checked}),
" Checked ", numChecked, " times. ",
button({onclick: () => numChecked.val = 0}, "Reset"),
)
}
</code></pre><p>Prior to <b>VanJS</b> 1.3.0, the code above is problematic. The intention of the code is to count the number of times that the checkbox is checked. The code:</p><pre><code class="language-js" data-jsfiddle-ignore="1"> van.derive(() => {
if (checked.val) ++numChecked.val
})</code></pre><p>defines the side effect to increment <code class="symbol">numChecked</code> whenever <code class="symbol">checked</code> state is turned to be <code class="symbol">true</code>. However, since <code class="language-js">++numChecked.val</code> de-sugars to <code class="language-js">numChecked.val = numChecked.val + 1</code>, the side effect actually depends on <code class="symbol">numChecked</code> state as well. As a result, when the <code class="symbol">Reset</code> button is clicked, it updates the <code class="symbol">numChecked</code> state, which leads to the side effect to increment <code class="symbol">numChecked</code> state, which will further trigger the same side effect and increment <code class="symbol">numChecked</code>, over and over again - an endless loop. Eventually a stack overflow error will occur to stop the loop, leaving <code class="symbol">numChecked</code> state ending in an arbitrary number.</p><p><b>VanJS</b> 1.3.0 adjusts the dependency detection mechanism in this situation to avoid the problem. That is, if we're setting the <code class="symbol">val</code> property of some state inside a binding function (be it in <code class="symbol">van.derive</code>, for state-derived properties, or for state-derived child nodes), that state will not be consider as a dependency of the binding function, even if its <code class="symbol">val</code> property is being read there. The adjustment is aimed to avoid the self-referencing problem discussed above, making it impossible to trigger an side effect to update a state that re-triggers the same side effect again. Thus in <b>VanJS</b> 1.3.0 or later, the code above has the correct behavior - clicking the <code class="symbol">Reset</code> button will just reset <code class="symbol">numChecked</code> to <code class="symbol">0</code>.</p><p>You can try out the program before and after the 1.3.0 update:</p><ul><li><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/self-ref-old">Before 1.3.0 update</a></li><li><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/self-ref-new">After 1.3.0 update</a></li></ul><h2 class="w3-xxlarge w3-text-red" id="gc"><a class="self-link" href="#gc">Garbage Collection</a></h2><hr style="width:50px;border:5px solid red" class="w3-round"><p>There is garbage collection mechanism implemented in <b>VanJS</b> to recycle obsolete state bindings. To illustrate the necessity of garbage collection, let's take a look at the code below:</p><pre><code class="language-js">const renderPre = van.state(false)
const text = van.state("Text")
const TextDiv = () => div(
() => (renderPre.val ? pre : span)(text),
)
</code></pre><p>In this piece of code, the <code class="symbol">TextDiv</code> component has a <code class="language-html"><div></code> element whose only child is bound to a <code class="symbol">boolean</code> state - <code class="symbol">renderPre</code>, which determines whether the <code class="language-html"><div></code> has a <code class="language-html"><pre></code> or <code class="language-html"><span></code> child. Inside the child element, the underlying text is bound to a <code class="symbol">string</code> state - <code class="symbol">text</code>. Whenever the value of <code class="symbol">renderPre</code> is toggled, a new version of the <code class="language-html"><div></code> element will be generated, and we will add a new binding from <code class="symbol">text</code> state to the child text node of the newly created <code class="language-html"><div></code> element.</p><p>Without proper garbage collection implemented, <code class="symbol">text</code> state will eventually be bound to many text nodes after <code class="symbol">renderPre</code> is toggled many times. All the of bindings, except for the most recently added one, are actually obsolete, as they bind the <code class="symbol">text</code> state to a text node that is not currently being used. i.e.: disconnected from the document tree. Meanwhile, because internally, a <code class="symbol">State</code> object holds the reference to all DOM elements are bound to it, these DOM elements won't be GC-ed by JavaScript runtime, causing <a href="https://en.wikipedia.org/wiki/Memory_leak" class="w3-hover-opacity">memory leaks</a>.</p><p>Garbage collection is implemented in <b>VanJS</b> to resolve the issue. There are 2 ways a garbage collection activity can be triggered:</p><ol><li><b>Periodic recycling:</b> periodically, <b>VanJS</b> will scan all <code class="symbol">State</code> objects that have new bindings added recently, and remove all bindings to disconnected DOM elements. i.e.: <code class="symbol"><a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected" class="w3-hover-opacity">isConnected</a></code> property is <code class="symbol">false</code>.</li><li><b>Pre-rendering recycling:</b> before <b>VanJS</b> re-render the DOM tree in response to state changes, it will first check all the states whose values have been changed in this render cycle, and remove all bindings to disconnected DOM elements.</li></ol><p><a href="/code/gc-ui" class="w3-hover-opacity">Try out the example here</a> (You can use <a href="https://en.wikipedia.org/wiki/Web_development_tools" class="w3-hover-opacity">developer console</a> to watch <code class="symbol">text</code>'s UI <code class="symbol">_bindings</code>).</p><h3 class="w3-large w3-text-red" id="avoid-your-bindings-to-be-gc-ed-unexpectedly"><a class="self-link" href="#avoid-your-bindings-to-be-gc-ed-unexpectedly">Avoid your bindings to be GC-ed unexpectedly</a></h3><p>There are some general guidelines to follow to avoid your bindings being garbage collected unexpectedly:</p><ol><li>Please complete the construction of the DOM tree and connect the newly constructed DOM tree to the <code class="symbol"><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/document" class="w3-hover-opacity">document</a></code> object before making any state changes. Otherwise, the bindings to yet-to-be-connected DOM elements will be garbage collected.</li><li>DOM tree construction needs to be synchronous. i.e.: you shouldn't have any suspension point while building the DOM tree (e.g.: <code class="symbol">await</code> something in an <code class="symbol"><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" class="w3-hover-opacity">async function</a></code>). Otherwise, periodic recycling might be scheduled in the middle of the suspension point which can cause bindings to yet-to-be-connected DOM elements being garbage collected.</li></ol><h3 class="w3-large w3-text-red" id="derived-states-and-side-effects-registered-inside-a-binding-function"><a class="self-link" href="#derived-states-and-side-effects-registered-inside-a-binding-function">Derived states and side effects registered inside a binding function</a></h3><p>For derived states and side effects registered via <code class="symbol"><a href="tutorial#api-derive" class="w3-hover-opacity">van.derive</a></code>, if they are registered inside a binding function, they will be garbage collected if the DOM node returned by the binding function becomes disconnected from the document tree. For instance, for the code below:</p><pre><code class="language-js">const renderPre = van.state(false)
const prefix = van.state("Prefix")
const TextDiv = () => div(() => {
const suffix = van.state("Suffix")
const text = van.derive(() => `${prefix.val} - ${suffix.val}`)
return (renderPre.val ? pre : span)(text)
})
</code></pre><p>whenever <code class="symbol">renderPre</code> is toggled, a new <code class="symbol">text</code> state will be created and subscribe to changes of the <code class="symbol">prefix</code> state. However, the derivation from <code class="symbol">prefix</code> to the previous <code class="symbol">text</code> state will be garbage collected as the derivation was created while executing a binding function whose result DOM node no longer connects to the document tree. This is the mechanism to avoid memory leaks caused by state derivations that hold onto memory indefinitely.</p><p><a href="/code/gc-derive" class="w3-hover-opacity">Try out the example here</a> (You can use developer console to watch <code class="symbol">prefix</code>'s <code class="symbol">_listeners</code>).</p><h2 class="w3-xxlarge w3-text-red" id="lifecycle-hooks"><a class="self-link" href="#lifecycle-hooks">Lifecycle Hooks</a></h2><hr style="width:50px;border:5px solid red" class="w3-round"><p>To keep <b>VanJS</b>'s simplicity, there isn't a direct support of lifecycle hooks in <b>VanJS</b> API. That being said, there are multiple ways to inject custom code upon lifecycle events (mount/unmount) of DOM elements.</p><h3 class="w3-large w3-text-red" id="using-settimeout"><a class="self-link" href="#using-settimeout">Using <code class="symbol">setTimeout</code></a></h3><p>A quick and dirty way to inject custom code upon a DOM element is mounted is to use <code class="symbol">setTimeout</code> with a small <code class="symbol">delay</code>. Since the rendering cycle starts right after the current thread of execution (internally, the rendering cycle is rescheduled via <code class="symbol">setTimeout</code> with a <code class="symbol">0</code> <code class="symbol">delay</code>), the custom code injected via <code class="symbol">setTimeout</code> with a small <code class="symbol">delay</code> is guaranteed to be executed right after the upcoming rendering cycle, which ensures its execution upon the DOM element being mounted. This simple technique is used in a few places of the official <b>VanJS</b> codebase (in the website and demos), e.g.: <a href="https://github.com/vanjs-org/vanjs-org.github.io/blob/e149ba503bf2da80d3023bb314e7fab57edbfa17/code/code-browser/src/main.js#L39" class="w3-hover-opacity">1</a>, <a href="https://github.com/vanjs-org/vanjs-org.github.io/blob/e149ba503bf2da80d3023bb314e7fab57edbfa17/converter-ui/convert.ts#L81" class="w3-hover-opacity">2</a>.</p><h3 class="w3-large w3-text-red" id="registering-a-side-effect-via-van-derive"><a class="self-link" href="#registering-a-side-effect-via-van-derive">Registering a side effect via <code class="symbol">van.derive</code></a></h3><p><i>Requires <b>VanJS</b> 1.5.0 or later.</i></p><p>If you want to get rid of <code class="symbol">setTimeout</code> (thus the small <code class="symbol">delay</code> introduced by it). You can leverage the technique of registering a side effect via <code class="symbol"><a href="/tutorial#api-derive" class="w3-hover-opacity">van.derive</a></code>, as demonstrated in the code below:</p><pre><code class="language-js">const Label = ({text, onmount}) => {
if (onmount) {
const trigger = van.state(false)
van.derive(() => trigger.val && onmount())
trigger.val = true
}
return div({class: "label"}, text)
}
const App = () => {
const counter = van.state(0)
return div(
div(button({onclick: () => ++counter.val}, "Increment")),
() => Label({
text: counter.val,
onmount: () => document.getElementById("msg").innerText =
"Current label: " + document.querySelector(".label").innerText,
}),
div({id: "msg"}),
)
}
</code></pre><p><b>Demo:</b></p><p id="demo-onmount"></p><p><a href="https://jsfiddle.net/gh/get/library/pure/vanjs-org/vanjs-org.github.io/tree/master/jsfiddle/advanced/onmount">Try on jsfiddle</a></p><p>This technique works because in <b>VanJS</b> 1.5.0 or later, derived states and side effects caused by state changes are scheduled asynchronously right in the next rendering cycle. Thus the side effects caused by the state changes of the current rendering cycle are guaranteed to be executed right after the completion of the current rendering cycle.</p><h3 class="w3-large w3-text-red" id="register-connectedcallback-and-disconnectedcallback-of-custom-elements"><a class="self-link" href="#register-connectedcallback-and-disconnectedcallback-of-custom-elements">Register <code class="symbol">connectedCallback</code> and <code class="symbol">disconnectedCallback</code> of custom elements</a></h3><p>Another option is to leverage the <code class="symbol">connectedCallback</code> and <code class="symbol">disconnectedCallback</code> of custom elements in <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" class="w3-hover-opacity">Web Components</a>. This is the only option to reliably inject custom code upon the unmount events of DOM elements. Note that in <b>VanJS</b>'s add-on: <a href="https://van-element.pages.dev/" class="w3-hover-opacity">van_element</a>, you can easily <a href="https://van-element.pages.dev/learn/lifecycle.html" class="w3-hover-opacity">register mount/unmount handlers</a> with the help of the add-on.</p></div>
<aside id="toc"><ul><li><a href="#dom-attributes-vs-properties" class="w3-hover-opacity">DOM Attributes vs. Properties</a></li><li><a href="#state-and-state-binding" class="w3-hover-opacity">State and State Binding</a><ul><li><a href="#why-not-dom-valued-states" class="w3-hover-opacity">Why can't states have DOM node as values?</a></li><li><a href="#state-granularity" class="w3-hover-opacity">State granularity</a></li><li><a href="#minimize-the-scope-of-dom-updates" class="w3-hover-opacity">Minimize the scope of DOM updates</a></li><li><a href="#advanced-state-derivation" class="w3-hover-opacity">Advanced state derivation</a></li><li><a href="#conditional-state-binding" class="w3-hover-opacity">Conditional state binding</a></li><li><a href="#self-referencing-in-side-effects" class="w3-hover-opacity">Self-referencing in side effects</a></li></ul></li><li><a href="#gc" class="w3-hover-opacity">Garbage Collection</a><ul><li><a href="#avoid-your-bindings-to-be-gc-ed-unexpectedly" class="w3-hover-opacity">Avoid your bindings to be GC-ed unexpectedly</a></li><li><a href="#derived-states-and-side-effects-registered-inside-a-binding-function" class="w3-hover-opacity">Derived states and side effects registered inside a binding function</a></li></ul></li><li><a href="#lifecycle-hooks" class="w3-hover-opacity">Lifecycle Hooks</a><ul><li><a href="#using-settimeout" class="w3-hover-opacity">Using setTimeout</a></li><li><a href="#registering-a-side-effect-via-van-derive" class="w3-hover-opacity">Registering a side effect via van.derive</a></li><li><a href="#register-connectedcallback-and-disconnectedcallback-of-custom-elements" class="w3-hover-opacity">Register connectedCallback and disconnectedCallback of custom elements</a></li></ul></li></ul></aside>
</div>
</div>
<script>
// Script to open and close sidebar
const w3_open = () => {
document.getElementById("mySidebar").style.display = "block"
document.getElementById("myOverlay").style.display = "block"
}
const w3_close = () => {
document.getElementById("mySidebar").style.display = "none"
document.getElementById("myOverlay").style.display = "none"
}
const tocDom = document.getElementById("toc")
// Tracks the current toc item
const trackToc = () => {
const allHeadings = [...document.querySelectorAll("h2,h3")]
const currentHeadingIndex = allHeadings.findIndex(h => h.getBoundingClientRect().top >= 0)
let currentHeading
if (currentHeadingIndex < 0) currentHeading = allHeadings[allHeadings.length - 1]; else {
currentHeading = allHeadings[currentHeadingIndex]
if (currentHeadingIndex > 0 && currentHeading.getBoundingClientRect().top > innerHeight)
currentHeading = allHeadings[currentHeadingIndex - 1]
}
for (const e of document.querySelectorAll("#toc li a"))
if (e.href.split("#")[1] === currentHeading?.id) {
e.classList.add("current-heading")
const {top: tocTop, bottom: tocBottom} = tocDom.getBoundingClientRect()
const {top: eTop, bottom: eBottom} = e.getBoundingClientRect()
if (eBottom > tocBottom) tocDom.scrollTop += eBottom - tocBottom
else if (eTop < tocTop) tocDom.scrollTop -= tocTop - eTop
} else
e.classList.remove("current-heading")
}
trackToc()
document.addEventListener("scroll", trackToc)
addEventListener("resize", trackToc)
const copy = e => {
const file = e.previousElementSibling.innerText
const importLine = file.includes("nomodule") ?
`<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/vanjs-org/van/public/${file}"><\/script>` :
`import van from "https://cdn.jsdelivr.net/gh/vanjs-org/van/public/${file}"`
navigator.clipboard.writeText(importLine)
.then(() => e.querySelector(".tooltip").innerText = "Copied")
.catch(() => e.querySelector(".tooltip").innerText = "Copy failed")
}
const resetTooltip = e => e.querySelector(".tooltip").innerText = "Copy import line"
</script>
<script src="https://guru-widget.pages.dev/guru_widget.latest.min.js" data-text="Ask AI" data-link="https://gurubase.io/g/vanjs" data-bg-color="rgba(244, 67, 54, 0.3)" data-icon-url="/ask_ai.svg" data-font-color="#ffffff" data-margins="{"bottom": "1rem", "left": "1430px", "right": "unset"}">
</script>
<!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
<link rel="prefetch" href="/code/prism-v1.js" as="script"><link rel="prefetch" href="https://www.gstatic.com/charts/loader.js" as="script"><link rel="prefetch" href="/code/diff.min.js" as="script"><link rel="prefetch" href="/code/van-1.5.2.nomodule.min.js" as="script"><link rel="prefetch" href="/code/van-x-0.6.2.nomodule.min.js" as="script"></body></html>