Skip to content

Commit d537153

Browse files
committed
fix attribute syncing for input, select, textarea
optimize events
1 parent 4a215a2 commit d537153

File tree

7 files changed

+503
-32
lines changed

7 files changed

+503
-32
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ This rewrite aims to fix longstanding API design issues, significantly improve p
66

77
## Status
88

9-
Code still is in flux. Most notably, thunks (`{subtree: "retain"}`) are currently not implemented yet and there are several use cases that still need to be polished. DO NOT USE IN PRODUCTION YET!
9+
Code still is in flux. Most notably, there's no promise polyfill yet and there are several use cases that still need to be polished. DO NOT USE IN PRODUCTION YET!
1010

1111
Some examples of usage can be found in the [examples](examples) folder. [ThreadItJS](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/threaditjs/index.html) has the largest API surface coverage and comments indicating pending issues in framework usability. Note that the APIs those examples use may not become the final public API points in v1.0.
1212

1313
## Performance
1414

15-
Mithril's virtual DOM engine is now around 400 lines of well organized code and it implements a modern search space reduction diff algorithm and a DOM recycling mechanism, which translate to top-of-class performance. See the [dbmon implementation (non-optimized)](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) (for comparison, here are optimized dbmon implementations for [React v15.0.2](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and [Angular v2.0.0-beta.17](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html)).
15+
Mithril's virtual DOM engine is less than 500 lines of well organized code and it implements a modern search space reduction diff algorithm and a DOM recycling mechanism, which translate to top-of-class performance. See the [dbmon implementation (non-optimized)](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) (for comparison, here are optimized dbmon implementations for [React v15.0.2](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and [Angular v2.0.0-beta.17](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html)).
1616

1717
## Lifecycle methods and Animation Support
1818

render/render.js

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ module.exports = function($window, onevent) {
8282
if (vnode.children != null) {
8383
var children = vnode.children
8484
createNodes(element, children, 0, children.length, hooks, null)
85-
if (tag === "select" && "value" in attrs) {
86-
setAttrs(vnode, { value: attrs.value })
87-
}
85+
setLateAttrs(vnode)
8886
}
8987
return element
9088
}
@@ -214,6 +212,10 @@ module.exports = function($window, onevent) {
214212
}
215213
function updateElement(old, vnode, hooks) {
216214
var element = vnode.dom = old.dom
215+
if (vnode.tag === "textarea") {
216+
if (vnode.attrs == null) vnode.attrs = {}
217+
if (vnode.text != null) vnode.attrs.value = vnode.text //FIXME handle multiple children
218+
}
217219
updateAttrs(vnode, old.attrs, vnode.attrs)
218220
if (old.text != null && vnode.text != null && vnode.text !== "") {
219221
if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text
@@ -343,24 +345,12 @@ module.exports = function($window, onevent) {
343345
function setAttr(vnode, key, old, value) {
344346
//TODO test input undo history
345347
var element = vnode.dom
346-
if (key === "key" || old === value || typeof value === "undefined" || isLifecycleMethod(key)) return
348+
if (key === "key" || (!isFormAttribute(vnode, key) && old === value) || typeof value === "undefined" || isLifecycleMethod(key)) return
347349
var nsLastIndex = key.indexOf(":")
348350
if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") {
349351
element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value)
350352
}
351-
else if (key[0] === "o" && key[1] === "n" && typeof value === "function") {
352-
var eventName = key.slice(2)
353-
if (vnode.events === undefined) vnode.events = {}
354-
if (vnode.events[key] != null) {
355-
element.removeEventListener(eventName, vnode.events[key], false)
356-
}
357-
vnode.events[key] = function(e) {
358-
var result = value.call(element, e)
359-
if (typeof onevent === "function") onevent.call(element, e)
360-
return result
361-
}
362-
element.addEventListener(eventName, vnode.events[key], false)
363-
}
353+
else if (key[0] === "o" && key[1] === "n" && typeof value === "function") updateEvent(vnode, key, value)
364354
else if (key === "style") updateStyle(element, old, value)
365355
else if (key in element && !isAttribute(key) && vnode.ns === undefined) element[key] = value
366356
else {
@@ -371,10 +361,17 @@ module.exports = function($window, onevent) {
371361
else element.setAttribute(key, value)
372362
}
373363
}
364+
function setLateAttrs(vnode) {
365+
var attrs = vnode.attrs
366+
if (vnode.tag === "select" && attrs != null) {
367+
if ("value" in attrs) setAttr(vnode, "value", null, attrs.value)
368+
if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex)
369+
}
370+
}
374371
function updateAttrs(vnode, old, attrs) {
375372
if (attrs != null) {
376373
for (var key in attrs) {
377-
setAttr(vnode, key, old[key], attrs[key])
374+
setAttr(vnode, key, old && old[key], attrs[key])
378375
}
379376
}
380377
if (old != null) {
@@ -385,6 +382,9 @@ module.exports = function($window, onevent) {
385382
}
386383
}
387384
}
385+
function isFormAttribute(vnode, attr) {
386+
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement
387+
}
388388
function isLifecycleMethod(attr) {
389389
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "shouldUpdate"
390390
}
@@ -408,6 +408,24 @@ module.exports = function($window, onevent) {
408408
}
409409
}
410410
}
411+
412+
//event
413+
function updateEvent(vnode, key, value) {
414+
var element = vnode.dom
415+
var callback = function(e) {
416+
var result = value.call(element, e)
417+
if (typeof onevent === "function") onevent.call(element, e)
418+
return result
419+
}
420+
if (key in element) element[key] = callback
421+
else {
422+
var eventName = key.slice(2)
423+
if (vnode.events === undefined) vnode.events = {}
424+
if (vnode.events[key] != null) element.removeEventListener(eventName, vnode.events[key], false)
425+
vnode.events[key] = callback
426+
element.addEventListener(eventName, vnode.events[key], false)
427+
}
428+
}
411429

412430
//lifecycle
413431
function initLifecycle(source, vnode, hooks) {

render/tests/test-input.js

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,138 @@ var o = require("../../ospec/ospec")
44
var domMock = require("../../test-utils/domMock")
55
var vdom = require("../../render/render")
66

7-
o.spec("input", function() {
7+
o.spec("form inputs", function() {
88
var $window, root, render
99
o.beforeEach(function() {
1010
$window = domMock()
11-
root = $window.document.body
1211
render = vdom($window).render
12+
root = $window.document.createElement("div")
13+
$window.document.body.appendChild(root)
14+
})
15+
o.afterEach(function() {
16+
while (root.firstChild) root.removeChild(root.firstChild)
1317
})
1418

15-
o("maintains focus after move", function() {
16-
var input = {tag: "input", key: 1}
17-
var a = {tag: "a", key: 2}
18-
var b = {tag: "b", key: 3}
19+
o.spec("input", function() {
20+
o("maintains focus after move", function() {
21+
var input = {tag: "input", key: 1}
22+
var a = {tag: "a", key: 2}
23+
var b = {tag: "b", key: 3}
24+
25+
render(root, [input, a, b])
26+
input.dom.focus()
27+
render(root, [a, input, b])
28+
29+
o($window.document.activeElement).equals(input.dom)
30+
})
31+
32+
o("syncs input value if DOM value differs from vdom value", function() {
33+
var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}}
34+
var updated = {tag: "input", attrs: {value: "aaa", oninput: function() {}}}
35+
36+
render(root, [input])
37+
38+
//simulate user typing
39+
var e = $window.document.createEvent("KeyboardEvent")
40+
e.initEvent("input", true, true)
41+
input.dom.focus()
42+
input.dom.value += "a"
43+
input.dom.dispatchEvent(e)
44+
45+
//re-render may use same vdom value as previous render call
46+
render(root, [updated])
47+
48+
o(updated.dom.value).equals("aaa")
49+
})
50+
51+
o("syncs input checked attribute if DOM value differs from vdom value", function() {
52+
var input = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}
53+
var updated = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}
54+
55+
render(root, [input])
56+
57+
//simulate user clicking checkbox
58+
var e = $window.document.createEvent("MouseEvents")
59+
e.initEvent("click", true, true)
60+
input.dom.focus()
61+
input.dom.dispatchEvent(e)
62+
63+
//re-render may use same vdom value as previous render call
64+
render(root, [updated])
65+
66+
o(updated.dom.checked).equals(true)
67+
})
68+
})
69+
70+
o.spec("select", function() {
71+
o("select works without attributes", function() {
72+
var select = {tag: "select", children: [
73+
{tag: "option", attrs: {value: "a"}, text: "aaa"},
74+
]}
75+
76+
render(root, [select])
77+
78+
o(select.dom.value).equals("a")
79+
o(select.dom.selectedIndex).equals(0)
80+
})
81+
82+
o("select yields invalid value without children", function() {
83+
var select = {tag: "select", attrs: {value: "a"}}
84+
85+
render(root, [select])
86+
87+
o(select.dom.value).equals("")
88+
o(select.dom.selectedIndex).equals(-1)
89+
})
1990

20-
render(root, [input, a, b])
21-
input.dom.focus()
22-
render(root, [a, input, b])
91+
o("select value is set correctly on first render", function() {
92+
var select = {tag: "select", attrs: {value: "b"}, children: [
93+
{tag: "option", attrs: {value: "a"}, text: "aaa"},
94+
{tag: "option", attrs: {value: "b"}, text: "bbb"},
95+
{tag: "option", attrs: {value: "c"}, text: "ccc"},
96+
]}
97+
98+
render(root, [select])
99+
100+
o(select.dom.value).equals("b")
101+
o(select.dom.selectedIndex).equals(1)
102+
})
23103

24-
o($window.document.activeElement).equals(input.dom)
104+
o("syncs select value if DOM value differs from vdom value", function() {
105+
function makeSelect() {
106+
return {tag: "select", attrs: {value: "b"}, children: [
107+
{tag: "option", attrs: {value: "a"}, text: "aaa"},
108+
{tag: "option", attrs: {value: "b"}, text: "bbb"},
109+
{tag: "option", attrs: {value: "c"}, text: "ccc"},
110+
]}
111+
}
112+
113+
render(root, [makeSelect()])
114+
115+
//simulate user selecting option
116+
root.firstChild.value = "c"
117+
root.firstChild.focus()
118+
119+
//re-render may use same vdom value as previous render call
120+
render(root, [makeSelect()])
121+
122+
o(root.firstChild.value).equals("b")
123+
o(root.firstChild.selectedIndex).equals(1)
124+
})
125+
})
126+
127+
o.spec("textarea", function() {
128+
o("updates after user input", function() {
129+
render(root, [{tag: "textarea", text: "aaa"}])
130+
131+
//simulate typing
132+
root.firstChild.value = "bbb"
133+
134+
//re-render may occur after value attribute is touched
135+
render(root, [{tag: "textarea", text: "ccc"}])
136+
137+
o(root.firstChild.value).equals("ccc")
138+
//FIXME should fail if fix is commented out
139+
})
25140
})
26141
})

render/tests/test-updateElement.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ o.spec("updateElement", function() {
3434
o(updated.dom).equals(root.firstChild)
3535
o(updated.dom.attributes["title"].nodeValue).equals("d")
3636
})
37+
o("adds attr from empty attrs", function() {
38+
var vnode = {tag: "a"}
39+
var updated = {tag: "a", attrs: {title: "d"}}
40+
41+
render(root, [vnode])
42+
render(root, [updated])
43+
44+
o(updated.dom).equals(vnode.dom)
45+
o(updated.dom).equals(root.firstChild)
46+
o(updated.dom.attributes["title"].nodeValue).equals("d")
47+
})
3748
o("removes attr", function() {
3849
var vnode = {tag: "a", attrs: {id: "b", title: "d"}}
3950
var updated = {tag: "a", attrs: {id: "c"}}

test-input.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!doctype html>
2+
<html>
3+
<head></head>
4+
<body>
5+
<textarea id="t">aaa</textarea>
6+
<select multiple id="aaa">
7+
<option value="a">aaa</option>
8+
<option value="b">bbb</option>
9+
<option value="c">ccc</option>
10+
</select>
11+
<div id="root"></div>
12+
<pre id="a"></pre>
13+
<script src="./module/module.js"></script>
14+
<script src="./render/node.js"></script>
15+
<script src="./render/hyperscript.js"></script>
16+
<script src="./render/render.js"></script>
17+
<script type="text/javascript">
18+
var m = require("./render/hyperscript")
19+
var render = require("./render/render")(window, run).render
20+
21+
var value = "asd"
22+
23+
function run() {
24+
console.log("rendering...")
25+
render(root, [
26+
m("textarea", {oninput: (e) => e}, value)
27+
])
28+
}
29+
30+
run()
31+
32+
//setInterval(()=> console.log(document.activeElement), 1000)
33+
34+
var el = document.createElement("br")
35+
var txt = document.createTextNode("ccc")
36+
t.appendChild(el)
37+
t.appendChild(txt)
38+
</script>
39+
</body>
40+
</html>

0 commit comments

Comments
 (0)