Merge branch 'next' into components

Conflicts:
	mithril.js
	tests/mithril-tests.js
This commit is contained in:
Leo Horie 2015-03-05 20:55:33 -05:00
commit eb8fa6f1c3
3 changed files with 535 additions and 19 deletions

View file

@ -8,6 +8,7 @@
- [Accessing the real DOM element](#accessing-the-real-dom-element) - [Accessing the real DOM element](#accessing-the-real-dom-element)
- [Persisting config data](#persisting-config-data) - [Persisting config data](#persisting-config-data)
- [Destructors](#destructors) - [Destructors](#destructors)
- [Persising DOM elements across route changes](#persising-dom-elements-across-route-changes)
- [SVG](#svg) - [SVG](#svg)
- [Dealing with focus](#dealing-with-focus) - [Dealing with focus](#dealing-with-focus)
- [Dealing with sorting and deleting in lists](#dealing-with-sorting-and-deleting-in-lists) - [Dealing with sorting and deleting in lists](#dealing-with-sorting-and-deleting-in-lists)
@ -342,6 +343,67 @@ m.render(document, m("a")); //logs `unloaded the div` and `alert` never gets cal
--- ---
#### Persising DOM elements across route changes
When using the [router](mithril.route.md), a route change recreates the DOM tree from scratch in order to unload plugins from the previous page. If you want to keep a DOM element intact across a route change, you can set the `retain` flag in the config's context object.
In the example below, there are two routes, each of which loads a module when a user navigates to their respective URLs. Both modules use a `menu` template, which contains links for navigation between the two modules, and an expensive-to-reinitialize element. Setting `context.retain = true` in the element's config function allows the span to stay intact after a route change.
```javascript
//a menu template
var menu = function() {
return m("div", [
m("a[href='/']", {config: m.route}, "Home"),
m("a[href='/contact']", {config: m.route}, "Contact"),
//an expensive-to-initialize DOM element
m("span", {config: persistent})
])
}
//a configuration that persists across route changes
function persistent(el, isInit, context) {
context.retain = true
if (!isInit) {
//only runs once, even if you move back and forth between `/` and `/contact`
doSomethingExpensive(el)
}
}
//modules that use the menu above
var Home = {
controller: function() {},
view: function() {
return [
menu(),
m("h1", "Home")
]
}
}
var Contact = {
view: function() {
return [
menu(),
m("h2", "Contact")
]
}
}
m.route(document.body, "/", {
"/": Home,
"/contact": Contact
})
```
Note that even if you set `context.retain = true`, the element will still be destroyed and recreated if it is different enough from the existing element. An element is considered "different enough" if:
- the tag name changes, or
- the list of HTML attributes changes, or
- the value of the element's id attribute changes
In addition, setting `context.retain = false` will also cause the element to be recreated, even if it is not considered different enough.
---
#### SVG #### SVG
You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively). You can use Mithril to create SVG documents (as long as you don't need to support browsers that don't support SVG natively).

View file

@ -245,7 +245,7 @@ var m = (function app(window, undefined) {
var dataAttrKeys = Object.keys(data.attrs) var dataAttrKeys = Object.keys(data.attrs)
var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
//if an element is different enough from the one in cache, recreate it //if an element is different enough from the one in cache, recreate it
if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key) { if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && cached.configContext && cached.configContext.retain !== true) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) {
if (cached.nodes.length) clear(cached.nodes); if (cached.nodes.length) clear(cached.nodes);
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload()
if (cached.controller && typeof cached.controller.onunload === FUNCTION) cached.controller.onunload({preventDefault: function() {}}) if (cached.controller && typeof cached.controller.onunload === FUNCTION) cached.controller.onunload({preventDefault: function() {}})
@ -293,7 +293,7 @@ var m = (function app(window, undefined) {
} }
//schedule configs to be called. They are called after `build` finishes running //schedule configs to be called. They are called after `build` finishes running
if (typeof data.attrs["config"] === FUNCTION) { if (typeof data.attrs["config"] === FUNCTION) {
var context = cached.configContext = cached.configContext || {}; var context = cached.configContext = cached.configContext || {retain: m.redraw.strategy() == "diff"};
// bind // bind
var callback = function(data, args) { var callback = function(data, args) {
@ -410,7 +410,10 @@ var m = (function app(window, undefined) {
if (nodes.length != 0) nodes.length = 0 if (nodes.length != 0) nodes.length = 0
} }
function unload(cached) { function unload(cached) {
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload(); if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) {
cached.configContext.onunload();
cached.configContext.onunload = null
}
if (cached.controller && typeof cached.controller.onunload === FUNCTION) cached.controller.onunload({preventDefault: function() {}}); if (cached.controller && typeof cached.controller.onunload === FUNCTION) cached.controller.onunload({preventDefault: function() {}});
if (cached.children) { if (cached.children) {
if (type.call(cached.children) === ARRAY) { if (type.call(cached.children) === ARRAY) {
@ -590,11 +593,10 @@ var m = (function app(window, undefined) {
m.redraw.strategy = m.prop(); m.redraw.strategy = m.prop();
var blank = function() {return ""} var blank = function() {return ""}
function redraw() { function redraw() {
var forceRedraw = m.redraw.strategy() === "all";
for (var i = 0, root; root = roots[i]; i++) { for (var i = 0, root; root = roots[i]; i++) {
if (controllers[i]) { if (controllers[i]) {
var args = modules[i].controller && modules[i].controller.$$args ? [controllers[i]].concat(modules[i].controller.$$args) : [controllers[i]] var args = modules[i].controller && modules[i].controller.$$args ? [controllers[i]].concat(modules[i].controller.$$args) : [controllers[i]]
m.render(root, modules[i].view ? modules[i].view(controllers[i], args) : blank(), forceRedraw) m.render(root, modules[i].view ? modules[i].view(controllers[i], args) : blank())
} }
} }
//after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState

View file

@ -476,6 +476,19 @@ function testMithril(mock) {
return count === 3 return count === 3
}) })
test(function() {
var root = mock.document.createElement("div")
var module = {}, unloaded = false
module.controller = function() {
this.onunload = function() {unloaded = true}
}
module.view = function() {}
m.module(root, module)
m.module(root, {controller: function() {}, view: function() {}})
return unloaded === true
})
m.redraw.strategy(undefined) //teardown for m.module tests
//m.withAttr //m.withAttr
test(function() { test(function() {
@ -790,17 +803,6 @@ function testMithril(mock) {
var valueAfter = root.childNodes[0].style.background var valueAfter = root.childNodes[0].style.background
return valueBefore === "red" && valueAfter === undefined return valueBefore === "red" && valueAfter === undefined
}) })
test(function() {
var root = mock.document.createElement("div")
var module = {}, unloaded = false
module.controller = function() {
this.onunload = function() {unloaded = true}
}
module.view = function() {}
m.module(root, module)
m.module(root, {controller: function() {}, view: function() {}})
return unloaded === true
})
test(function() { test(function() {
//https://github.com/lhorie/mithril.js/issues/87 //https://github.com/lhorie/mithril.js/issues/87
var root = mock.document.createElement("div") var root = mock.document.createElement("div")
@ -2137,6 +2139,456 @@ function testMithril(mock) {
return mock.history.$$length == 0 return mock.history.$$length == 0
}) })
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var value
var a = {}
a.controller = function() {}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
value = ctx.retain
}})
}
m.route(root, "/a", {
"/a": a
})
return value === false
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var value
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
value = ctx.retain
}})
}
m.route(root, "/a", {
"/a": a
})
return value === true
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}})
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = a.view
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
ctx.retain = true
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
ctx.retain = false
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 2
})
test(function() {
mock.requestAnimationFrame.$resolve()
mock.location.search = "?"
var root = mock.document.createElement("div")
var initCount = 0
var a = {}
a.controller = function() {m.redraw.strategy("diff")}
a.view = function() {
return m("div", m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}}))
}
var b = {}
b.controller = function() {m.redraw.strategy("diff")}
b.view = function() {
return m("section", m("a", {config: function(el, init, ctx) {
if (!init) initCount++
}}))
}
m.route(root, "/a", {
"/a": a,
"/b": b,
})
mock.requestAnimationFrame.$resolve()
m.route("/b")
mock.requestAnimationFrame.$resolve()
return initCount == 1
})
//end m.route //end m.route
//m.prop //m.prop