Skip to content

Release - v2.3.3 #3031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions api/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,24 @@ module.exports = function($window, mountRedraw) {
var ready = false
var hasBeenResolved = false

var compiled, fallbackRoute
var dom, compiled, fallbackRoute

var currentResolver, component, attrs, currentPath, lastUpdate

var RouterRoot = {
onremove: function() {
ready = hasBeenResolved = false
$window.removeEventListener("popstate", fireAsync, false)
},
view: function() {
if (!hasBeenResolved) return
// The route has already been resolved.
// Therefore, the following early return is not needed.
// if (!hasBeenResolved) return

var vnode = Vnode(component, attrs.key, attrs)
if (currentResolver) return currentResolver.render(vnode)
// Wrap in a fragment to preserve existing key semantics
var vnode = [Vnode(component, attrs.key, attrs)]
if (currentResolver) vnode = currentResolver.render(vnode[0])
return vnode
return [vnode]
},
}

Expand Down Expand Up @@ -90,7 +94,7 @@ module.exports = function($window, mountRedraw) {
if (hasBeenResolved) mountRedraw.redraw()
else {
hasBeenResolved = true
mountRedraw.redraw.sync()
mountRedraw.mount(dom, RouterRoot)
}
}
// There's no understating how much I *wish* I could
Expand Down Expand Up @@ -148,11 +152,13 @@ module.exports = function($window, mountRedraw) {
throw new ReferenceError("Default route doesn't match any known routes.")
}
}
dom = root

$window.addEventListener("popstate", fireAsync, false)

ready = true
mountRedraw.mount(root, RouterRoot)

// The RouterRoot component is mounted when the route is first resolved.
resolveRoute()
}
route.set = function(path, data, options) {
Expand Down
173 changes: 173 additions & 0 deletions api/tests/test-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,179 @@ o.spec("route", function() {
o(route.param()).deepEquals({id:"1"})
})
})

o("route component is mounted after the route is initially resolved (synchronous)", function() {
var Component = {
view: lock(function() {
// the first rendered vnode is cleared
o(root.childNodes.length).equals(0)
return m("span")
})
}

// initial root node
root.textContent = "foo"
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("#text")
o(root.childNodes[0].nodeValue).equals("foo")

// render another vnode first
var render = coreRenderer($window)
var vnode = m("a", "loading...")
render(root, vnode)
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[0].firstChild.nodeName).equals("#text")
o(root.childNodes[0].firstChild.nodeValue).equals("loading...")

// call route() (mount synchronously)
$window.location.href = prefix + "/"
route(root, "/", {
"/" : Component
})

// route component is mounted and the first rendered vnode is cleared
o(root.childNodes.length).equals(1)
o(root.childNodes[0]).notEquals(vnode.dom)
o(root.childNodes[0].nodeName).equals("SPAN")
o(root.childNodes[0].childNodes.length).equals(0)
})

o("route component is mounted after the route is initially resolved (render, synchronous)", function() {
var Component = {
render: lock(function() {
// the first rendered vnode is cleared
o(root.childNodes.length).equals(0)
return m("span")
})
}

// initial root node
root.textContent = "foo"
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("#text")
o(root.childNodes[0].nodeValue).equals("foo")

// render another vnode first
var render = coreRenderer($window)
var vnode = m("a", "loading...")
render(root, vnode)
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[0].firstChild.nodeName).equals("#text")
o(root.childNodes[0].firstChild.nodeValue).equals("loading...")

// call route() (mount synchronously)
$window.location.href = prefix + "/"
route(root, "/", {
"/" : Component
})

// route component is mounted and the first rendered vnode is cleared
o(root.childNodes.length).equals(1)
o(root.childNodes[0]).notEquals(vnode.dom)
o(root.childNodes[0].nodeName).equals("SPAN")
o(root.childNodes[0].childNodes.length).equals(0)
})

o("route component is mounted after the route is initially resolved (onmatch, asynchronous)", function() {
var Component = {
view: lock(function() {
return m("span")
})
}

// check for the order of calling onmatch and render
var count = 0

var resolver = {
onmatch: lock(function() {
count += 1
o(count).equals(1)

// the first rendered vnode is not yet cleared
o(root.childNodes.length).equals(1)
o(root.childNodes[0]).equals(vnode.dom)
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[0].firstChild.nodeName).equals("#text")
o(root.childNodes[0].firstChild.nodeValue).equals("loading...")
return Component
}),
render: lock(function(vnode) {
count += 1
o(count).equals(2)

// the first rendered vnode is cleared
o(root.childNodes.length).equals(0)
return vnode
})
}

// initial root node
root.textContent = "foo"
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("#text")
o(root.childNodes[0].nodeValue).equals("foo")

// render another vnode first
var render = coreRenderer($window)
var vnode = m("a", "loading...")
render(root, vnode)
o(root.childNodes.length).equals(1)
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[0].firstChild.nodeName).equals("#text")
o(root.childNodes[0].firstChild.nodeValue).equals("loading...")

// call route() (mount asynchronously)
$window.location.href = prefix + "/"
route(root, "/", {
"/" : resolver
})

// the first rendered vnode is not yet cleared
o(root.childNodes.length).equals(1)
o(root.childNodes[0]).equals(vnode.dom)
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[0].firstChild.nodeName).equals("#text")
o(root.childNodes[0].firstChild.nodeValue).equals("loading...")

// The count of route resolver method calls is still 0
o(count).equals(0)

return waitCycles(1).then(function() {
// route component is mounted and the first rendered vnode is cleared
o(root.childNodes.length).equals(1)
o(root.childNodes[0]).notEquals(vnode.dom)
o(root.childNodes[0].nodeName).equals("SPAN")
o(root.childNodes[0].childNodes.length).equals(0)

o(count).equals(2)
})
})

o("error in the route component is thrown and not caught in the initial rendering (#2621)", function() {
var Component = {
view: lock(function() {
throw Error("foo")
})
}

// Errors thrown during redrawing of mounted components are caught in m.mount()
// and console.error is called.
// Therefore, spy is used to confirm that console.error is not called
// when it is first mounted.
var spy = o.spy(console.error)
console.error = spy

$window.location.href = prefix + "/"
o(function(){
route(root, "/", {
"/" : Component
})
}).throws("foo")

o(spy.callCount).equals(0)
})
})
})
})
Expand Down
Loading