diff --git a/examples/async-children/app.js b/examples/async-children/app.js
new file mode 100644
index 000000000..20e7e8c9e
--- /dev/null
+++ b/examples/async-children/app.js
@@ -0,0 +1,55 @@
+import Vue from 'vue'
+import VueRouter from 'vue-router'
+
+Vue.use(VueRouter)
+
+const Home = { template: '
home
' }
+const Foo = { template: 'foo
' }
+const Bar = { template: 'bar
' }
+
+const routes = [
+ { path: '/', component: Home },
+ { path: '/foo', component: Foo },
+ { path: '/bar', component: Bar },
+ {
+ path: '/basic',
+ component: { template: '
' },
+ loadChildren: () => import('./basic-async').then(asyncConfig => asyncConfig.routes)
+ },
+ {
+ path: '/deep',
+ component: { template: '
' },
+ loadChildren: () => import('./deep-async-a').then(asyncConfig => asyncConfig.routes)
+ },
+ {
+ path: '/default-deep',
+ component: { template: '
' },
+ loadChildren: () => import('./deep-async-default-a').then(asyncConfig => asyncConfig.routes)
+ }
+]
+
+const router = new VueRouter({
+ mode: 'history',
+ base: __dirname,
+ routes
+})
+
+new Vue({
+ router,
+ template: `
+
+
Basic
+
+ - /
+ - /foo
+ - /bar
+ - /basic
+ - /basic/foo
+ - /basic/bar
+ - /deep/a/b
+ - /default-deep
+
+
+
+ `
+}).$mount('#app')
diff --git a/examples/async-children/basic-async.js b/examples/async-children/basic-async.js
new file mode 100644
index 000000000..9d43826b0
--- /dev/null
+++ b/examples/async-children/basic-async.js
@@ -0,0 +1,9 @@
+const BasicDefault = { template: 'basic-default
' }
+const BasicFoo = { template: 'basic-foo
' }
+const BasicBar = { template: 'basic-bar
' }
+
+export const routes = [
+ { path: '', component: BasicDefault },
+ { path: 'foo', component: BasicFoo },
+ { path: 'bar', component: BasicBar }
+]
diff --git a/examples/async-children/deep-async-a.js b/examples/async-children/deep-async-a.js
new file mode 100644
index 000000000..0df8caf62
--- /dev/null
+++ b/examples/async-children/deep-async-a.js
@@ -0,0 +1,19 @@
+const DeepA = { template: '
' }
+
+export const routes = [
+ {
+ path: '',
+ component: {},
+ loadChildren: () => Promise.reject('Default async route loaded when it should not have been.')
+ },
+ {
+ path: 'test',
+ component: {},
+ loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
+ },
+ {
+ path: 'a',
+ component: DeepA,
+ loadChildren: () => import('./deep-async-b').then(asyncConfig => asyncConfig.routes)
+ }
+]
diff --git a/examples/async-children/deep-async-b.js b/examples/async-children/deep-async-b.js
new file mode 100644
index 000000000..d6dd56979
--- /dev/null
+++ b/examples/async-children/deep-async-b.js
@@ -0,0 +1,18 @@
+const DeepB = { template: 'deep-async-b
' }
+
+export const routes = [
+ {
+ path: '',
+ component: {},
+ loadChildren: () => Promise.reject('Default async route loaded when it should not have been.')
+ },
+ {
+ path: 'test',
+ component: {},
+ loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
+ },
+ {
+ path: 'b',
+ component: DeepB
+ }
+]
diff --git a/examples/async-children/deep-async-default-a.js b/examples/async-children/deep-async-default-a.js
new file mode 100644
index 000000000..981d49041
--- /dev/null
+++ b/examples/async-children/deep-async-default-a.js
@@ -0,0 +1,14 @@
+const DeepA = { template: '
' }
+
+export const routes = [
+ {
+ path: '',
+ component: DeepA,
+ loadChildren: () => import('./deep-async-default-b').then(asyncConfig => asyncConfig.routes)
+ },
+ {
+ path: 'test',
+ component: {},
+ loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
+ }
+]
diff --git a/examples/async-children/deep-async-default-b.js b/examples/async-children/deep-async-default-b.js
new file mode 100644
index 000000000..5fdbe7866
--- /dev/null
+++ b/examples/async-children/deep-async-default-b.js
@@ -0,0 +1,13 @@
+const DeepB = { template: 'deep-async-default-b
' }
+
+export const routes = [
+ {
+ path: '',
+ component: DeepB
+ },
+ {
+ path: 'test',
+ component: {},
+ loadChildren: () => Promise.reject('Test async route loaded when it should not have been.')
+ }
+]
diff --git a/examples/async-children/index.html b/examples/async-children/index.html
new file mode 100644
index 000000000..4e5aa1549
--- /dev/null
+++ b/examples/async-children/index.html
@@ -0,0 +1,6 @@
+
+
+← Examples index
+
+
+
\ No newline at end of file
diff --git a/flow/declarations.js b/flow/declarations.js
index 0efebfa96..3d30d1ca2 100644
--- a/flow/declarations.js
+++ b/flow/declarations.js
@@ -27,6 +27,8 @@ declare type NavigationGuard = (
declare type AfterNavigationHook = (to: Route, from: Route) => any
+declare type LoadChildrenPromise = () => Promise;
+
type Position = { x: number, y: number };
declare type RouterOptions = {
@@ -59,6 +61,7 @@ declare type RouteConfig = {
props?: boolean | Object | Function;
caseSensitive?: boolean;
pathToRegexpOptions?: PathToRegexpOptions;
+ loadChildren?: string | LoadChildrenPromise | null;
}
declare type RouteRecord = {
@@ -73,6 +76,8 @@ declare type RouteRecord = {
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary;
+ loadChildren?: string | LoadChildrenPromise | null;
+ routeConfig: RouteConfig;
}
declare type Location = {
diff --git a/src/create-matcher.js b/src/create-matcher.js
index 4e96d215e..870825de7 100644
--- a/src/create-matcher.js
+++ b/src/create-matcher.js
@@ -7,10 +7,12 @@ import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'
+import { findParent, addChildren } from './util/async-children'
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array) => void;
+ loadAsyncChildren: (raw: RawLocation, route: Route) => Promise;
};
export function createMatcher (
@@ -71,6 +73,35 @@ export function createMatcher (
return _createRoute(null, location)
}
+ function loadAsyncChildren (
+ raw: RawLocation,
+ currentRoute: Route
+ ): Promise {
+ const location = normalizeLocation(raw, currentRoute, false, router)
+ const asyncMatches = currentRoute.matched.filter(match => match && !!match.loadChildren)
+ if (!asyncMatches) {
+ return Promise.reject(new Error('No matched routes have async children.'))
+ }
+
+ return Promise.all([
+ ...asyncMatches.map(match =>
+ typeof match.loadChildren === 'function'
+ ? match.loadChildren()
+ : Promise.resolve([])
+ )
+ ])
+ .then(allChildren => {
+ return Promise.all([
+ ...allChildren.map((children, i) => {
+ const { path } = asyncMatches[i]
+ const parent = findParent(pathMap[path])
+ return addChildren(parent.routeConfig, children, path, location.path || '/')
+ .then(updatedConfig => addRoutes([updatedConfig]))
+ })
+ ])
+ })
+ }
+
function redirect (
record: RouteRecord,
location: Location
@@ -168,7 +199,8 @@ export function createMatcher (
return {
match,
- addRoutes
+ addRoutes,
+ loadAsyncChildren
}
}
diff --git a/src/create-route-map.js b/src/create-route-map.js
index 4077392a1..b986f5427 100644
--- a/src/create-route-map.js
+++ b/src/create-route-map.js
@@ -8,7 +8,8 @@ export function createRouteMap (
routes: Array,
oldPathList?: Array,
oldPathMap?: Dictionary,
- oldNameMap?: Dictionary
+ oldNameMap?: Dictionary,
+ parentRoute?: RouteRecord
): {
pathList: Array;
pathMap: Dictionary;
@@ -20,7 +21,7 @@ export function createRouteMap (
const nameMap: Dictionary = oldNameMap || Object.create(null)
routes.forEach(route => {
- addRouteRecord(pathList, pathMap, nameMap, route)
+ addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// ensure wildcard routes are always at the end
@@ -48,6 +49,12 @@ function addRouteRecord (
matchAs?: string
) {
const { path, name } = route
+ const hasAsyncChildren = typeof route.loadChildren === 'function'
+ const matchAllChildren = hasAsyncChildren && (
+ (parent && path !== '' && path !== '/') ||
+ !parent
+ )
+
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
@@ -55,13 +62,22 @@ function addRouteRecord (
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)
+
+ if (route.loadChildren) {
+ assert(
+ hasAsyncChildren,
+ `route config "loadChildren" for path: ${String(path || name)} cannot be a ` +
+ `${typeof route.loadChildren}. Use a method that returns a Promise with your child routes.`
+ )
+ }
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(
path,
parent,
- pathToRegexpOptions.strict
+ pathToRegexpOptions.strict,
+ hasAsyncChildren
)
if (typeof route.caseSensitive === 'boolean') {
@@ -70,7 +86,7 @@ function addRouteRecord (
const record: RouteRecord = {
path: normalizedPath,
- regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
+ regex: compileRouteRegex(normalizedPath, pathToRegexpOptions, matchAllChildren),
components: route.components || { default: route.component },
instances: {},
name,
@@ -83,7 +99,12 @@ function addRouteRecord (
? {}
: route.components
? route.props
- : { default: route.props }
+ : { default: route.props },
+ routeConfig: route
+ }
+
+ if (hasAsyncChildren) {
+ record.loadChildren = route.loadChildren
}
if (route.children) {
@@ -118,6 +139,7 @@ function addRouteRecord (
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
+ loadChildren: route.loadChildren,
children: route.children
}
addRouteRecord(
@@ -136,8 +158,18 @@ function addRouteRecord (
pathMap[record.path] = record
}
+ if (parent) {
+ // Ensure the parent route is after all child routes
+ const parentIndex = pathList.indexOf(parent.path)
+ const childIndex = pathList.indexOf(record.path)
+
+ if (parentIndex < childIndex) {
+ pathList.push(pathList.splice(parentIndex, 1)[0])
+ }
+ }
+
if (name) {
- if (!nameMap[name]) {
+ if (!nameMap[name] || nameMap[name].path === record.path) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
@@ -149,8 +181,8 @@ function addRouteRecord (
}
}
-function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp {
- const regex = Regexp(path, [], pathToRegexpOptions)
+function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions, matchAllChildren?: boolean): RouteRegExp {
+ const regex = Regexp(`${path}${matchAllChildren ? '(\/{0,1}.*)' : ''}`, [], pathToRegexpOptions)
if (process.env.NODE_ENV !== 'production') {
const keys: any = {}
regex.keys.forEach(key => {
@@ -161,8 +193,8 @@ function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptio
return regex
}
-function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string {
- if (!strict) path = path.replace(/\/$/, '')
+function normalizePath (path: string, parent?: RouteRecord, strict?: boolean, async?: boolean): string {
+ if (!strict || async) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
diff --git a/src/history/base.js b/src/history/base.js
index 0c90dafa7..fc5885564 100644
--- a/src/history/base.js
+++ b/src/history/base.js
@@ -64,14 +64,32 @@ export class History {
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
- this.updateRoute(route)
- onComplete && onComplete(route)
- this.ensureURL()
+ if (route.loadChildren) {
+ this.router.matcher.loadAsyncChildren(location, route)
+ .then(() => {
+ // Perform transition again to ensure the proper component hierarchy is loaded
+ this.transitionTo(location, onComplete, onAbort)
+ })
+ .catch((err) => {
+ if (onAbort) {
+ onAbort(err)
+ }
- // fire ready cbs once
- if (!this.ready) {
- this.ready = true
- this.readyCbs.forEach(cb => { cb(route) })
+ if (err && !this.ready) {
+ this.ready = true
+ this.readyErrorCbs.forEach(cb => { cb(err) })
+ }
+ })
+ } else {
+ this.updateRoute(route)
+ onComplete && onComplete(route)
+ this.ensureURL()
+
+ // fire ready cbs once
+ if (!this.ready) {
+ this.ready = true
+ this.readyCbs.forEach(cb => { cb(route) })
+ }
}
}, err => {
if (onAbort) {
diff --git a/src/util/async-children.js b/src/util/async-children.js
new file mode 100644
index 000000000..c367aa7e9
--- /dev/null
+++ b/src/util/async-children.js
@@ -0,0 +1,84 @@
+/* @flow */
+
+export function findParent (
+ routeRecord: RouteRecord
+): RouteRecord {
+ if (routeRecord.parent) {
+ return findParent(routeRecord.parent)
+ } else {
+ return routeRecord
+ }
+}
+
+export function loadDefaultAsyncChildren (
+ childrenConfigs: Array
+): Promise {
+ return Promise.all([
+ ...childrenConfigs.map(child => {
+ if (child.path === '' && typeof child.loadChildren === 'function') {
+ return child.loadChildren()
+ .then(asyncChildren =>
+ loadDefaultAsyncChildren(asyncChildren)
+ )
+ .then(asyncChildren => {
+ child.loadChildren = null
+
+ if (child.children) {
+ child.children.push(...asyncChildren)
+ } else {
+ child.children = asyncChildren
+ }
+
+ return child
+ })
+ } else {
+ return child
+ }
+ })
+ ])
+}
+
+export function addChildren (
+ routeConfig: RouteConfig,
+ children: RouteConfig[],
+ childRoutePath: string,
+ location: string
+): Promise {
+ let updatedPath = childRoutePath.replace(/^\//, '')
+ const configPath = routeConfig.path.replace(/^\//, '')
+ const updatedLocation = location.replace(/^\//, '').replace(configPath, '')
+
+ if (updatedPath === configPath) {
+ routeConfig.loadChildren = null
+ if (routeConfig.children) {
+ routeConfig.children.push(...children)
+ } else {
+ routeConfig.children = children
+ }
+
+ if (updatedLocation === '') {
+ // User attempting to load default path
+ return loadDefaultAsyncChildren(routeConfig.children || [])
+ .then(asyncChildren => {
+ routeConfig.children = asyncChildren
+ return routeConfig
+ })
+ } else {
+ return Promise.resolve(routeConfig)
+ }
+ } else if (updatedPath.indexOf(configPath) === 0) {
+ updatedPath = updatedPath.replace(configPath, '')
+
+ return Promise.all([
+ ...(routeConfig.children || []).map(child =>
+ addChildren(child, children, updatedPath, location)
+ )
+ ])
+ .then(children => {
+ routeConfig.children = children
+ return routeConfig
+ })
+ } else {
+ return Promise.resolve(routeConfig)
+ }
+}
diff --git a/src/util/route.js b/src/util/route.js
index 9fee9c099..e09ec7d18 100644
--- a/src/util/route.js
+++ b/src/util/route.js
@@ -20,6 +20,7 @@ export function createRoute (
query: location.query || {},
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
+ loadChildren: record && record.loadChildren ? record.loadChildren : null,
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
diff --git a/test/e2e/specs/async-children.js b/test/e2e/specs/async-children.js
new file mode 100644
index 000000000..6696c1ab6
--- /dev/null
+++ b/test/e2e/specs/async-children.js
@@ -0,0 +1,70 @@
+module.exports = {
+ 'async children': function (browser) {
+ browser
+ .url('http://localhost:8080/async-children/')
+ .waitForElementVisible('#app', 1000)
+ .assert.count('li a', 8)
+ .assert.containsText('.view', 'home')
+
+ .click('li:nth-child(2) a')
+ .assert.containsText('.view', 'foo')
+
+ .click('li:nth-child(3) a')
+ .assert.containsText('.view', 'bar')
+
+ .click('li:nth-child(1) a')
+ .assert.containsText('.view', 'home')
+
+ .click('li:nth-child(4) a')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-default')
+
+ // test linking to children
+ .url('http://localhost:8080/async-children')
+ .waitForElementVisible('#app', 1000)
+ .click('li:nth-child(4) a')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-default')
+
+ .url('http://localhost:8080/async-children')
+ .waitForElementVisible('#app', 1000)
+ .click('li:nth-child(5) a')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-foo')
+
+ .url('http://localhost:8080/async-children')
+ .waitForElementVisible('#app', 1000)
+ .click('li:nth-child(6) a')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-bar')
+
+ // test deep linking to children
+ .url('http://localhost:8080/async-children/basic')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-default')
+
+ .url('http://localhost:8080/async-children/basic/foo')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-foo')
+
+ .url('http://localhost:8080/async-children/basic/bar')
+ .waitForElementVisible('.async-view', 1000)
+ .assert.containsText('.async-view', 'basic-bar')
+
+ // test deep async loading
+ .url('http://localhost:8080/async-children')
+ .waitForElementVisible('#app', 1000)
+ .click('li:nth-child(7) a')
+ .waitForElementVisible('.async-viewA', 1000)
+ .assert.containsText('.async-viewA', 'deep-async-b')
+
+ // test deep default async loading
+ .url('http://localhost:8080/async-children')
+ .waitForElementVisible('#app', 1000)
+ .click('li:nth-child(8) a')
+ .waitForElementVisible('.async-viewA', 1000)
+ .assert.containsText('.async-viewA', 'deep-async-default-b')
+
+ .end()
+ }
+}
diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js
index 57e912cd9..bb9894c66 100644
--- a/test/unit/specs/api.spec.js
+++ b/test/unit/specs/api.spec.js
@@ -75,6 +75,30 @@ describe('router.addRoutes', () => {
expect(components.length).toBe(1)
expect(components[0].name).toBe('A')
})
+
+ it('should load children to an existing parent route', function () {
+ const router = new Router({
+ mode: 'abstract',
+ routes: [
+ { path: '/a', component: { name: 'A' }}
+ ]
+ })
+
+ router.push('/a')
+ let components = router.getMatchedComponents()
+ expect(components.length).toBe(1)
+ expect(components[0].name).toBe('A')
+
+ router.push('/a/b')
+ components = router.getMatchedComponents()
+ expect(components.length).toBe(0)
+
+ // make sure it preserves previous routes
+ router.push('/a')
+ components = router.getMatchedComponents()
+ expect(components.length).toBe(1)
+ expect(components[0].name).toBe('A')
+ })
})
describe('router.push/replace callbacks', () => {
@@ -91,6 +115,36 @@ describe('router.push/replace callbacks', () => {
}
}
+ const Bar = {
+ beforeRouteEnter (to, from, next) {
+ calls.push(5)
+ setTimeout(() => {
+ calls.push(6)
+ next()
+ }, 1)
+ }
+ }
+
+ const AsyncFoo = {
+ beforeRouteEnter (to, from, next) {
+ calls.push(13)
+ setTimeout(() => {
+ calls.push(14)
+ next()
+ }, 1)
+ }
+ }
+
+ const AsyncBar = {
+ beforeRouteEnter (to, from, next) {
+ calls.push(15)
+ setTimeout(() => {
+ calls.push(16)
+ next()
+ }, 1)
+ }
+ }
+
beforeEach(() => {
calls = []
spy1 = jasmine.createSpy('complete')
@@ -98,7 +152,31 @@ describe('router.push/replace callbacks', () => {
router = new Router({
routes: [
- { path: '/foo', component: Foo }
+ { path: '/foo', component: Foo },
+ {
+ path: '/asyncFoo',
+ component: AsyncFoo,
+ loadChildren: function () {
+ return Promise.resolve([
+ {
+ path: 'asyncBar',
+ component: AsyncBar
+ }
+ ])
+ }
+ },
+ {
+ path: '/asyncBiz',
+ component: AsyncFoo,
+ loadChildren: function () {
+ return Promise.resolve([
+ {
+ path: '',
+ component: AsyncBar
+ }
+ ])
+ }
+ }
]
})
@@ -144,4 +222,88 @@ describe('router.push/replace callbacks', () => {
done()
})
})
+
+ describe('async children', function () {
+ it('push complete', done => {
+ router.push('/asyncFoo/asyncBar', () => {
+ expect(calls).toEqual([1, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ done()
+ })
+ })
+
+ it('push abort', done => {
+ router.push('/foo', spy1, spy2)
+ router.push('/asyncFoo/asyncBar', () => {
+ expect(calls).toEqual([1, 1, 2, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ expect(spy1).not.toHaveBeenCalled()
+ expect(spy2).toHaveBeenCalled()
+ done()
+ })
+ })
+
+ it('replace complete', done => {
+ router.replace('/asyncFoo/asyncBar', () => {
+ expect(calls).toEqual([1, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+
+ let components = router.getMatchedComponents()
+ expect(components.length).toBe(2)
+ done()
+ })
+ })
+
+ it('replace abort', done => {
+ router.replace('/foo', spy1, spy2)
+ router.replace('/asyncFoo/asyncBar', () => {
+ expect(calls).toEqual([1, 1, 2, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ expect(spy1).not.toHaveBeenCalled()
+ expect(spy2).toHaveBeenCalled()
+
+ let components = router.getMatchedComponents()
+ expect(components.length).toBe(2)
+
+ done()
+ })
+ })
+
+ it('push default complete', done => {
+ router.push('/asyncBiz', () => {
+ expect(calls).toEqual([1, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ done()
+ })
+ })
+
+ it('push default abort', done => {
+ router.push('/foo', spy1, spy2)
+ router.push('/asyncBiz', () => {
+ expect(calls).toEqual([1, 1, 2, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ expect(spy1).not.toHaveBeenCalled()
+ expect(spy2).toHaveBeenCalled()
+ done()
+ })
+ })
+
+ it('replace default complete', done => {
+ router.replace('/asyncBiz', () => {
+ expect(calls).toEqual([1, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+
+ let components = router.getMatchedComponents()
+ expect(components.length).toBe(2)
+ done()
+ })
+ })
+
+ it('replace default abort', done => {
+ router.replace('/foo', spy1, spy2)
+ router.replace('/asyncBiz', () => {
+ expect(calls).toEqual([1, 1, 2, 2, 13, 14, 1, 2, 13, 14, 15, 16])
+ expect(spy1).not.toHaveBeenCalled()
+ expect(spy2).toHaveBeenCalled()
+
+ let components = router.getMatchedComponents()
+ expect(components.length).toBe(2)
+
+ done()
+ })
+ })
+ })
})
diff --git a/test/unit/specs/create-map.spec.js b/test/unit/specs/create-map.spec.js
index 9d8e62f36..544a62f28 100644
--- a/test/unit/specs/create-map.spec.js
+++ b/test/unit/specs/create-map.spec.js
@@ -71,7 +71,7 @@ describe('Creating Route Map', function () {
expect(console.warn.calls.argsFor(0)[0]).toMatch('vue-router] Named Route \'bar\'')
})
- it('in production, it has not logged this warning', function () {
+ it('in production, has not logged a warning concerning named route of parent and default subroute', function () {
maps = createRouteMap(routes)
expect(console.warn).not.toHaveBeenCalled()
})
@@ -164,4 +164,57 @@ describe('Creating Route Map', function () {
expect(pathList).toEqual(['/foo/', '/bar'])
})
})
+
+ describe('async children', function () {
+ it('should log a warning in development when loadChildren is not a method', function () {
+ const maps = function () {
+ return createRouteMap([
+ {
+ name: 'asyncFoo',
+ path: '/asyncFoo',
+ loadChildren: '/async/routes'
+ }
+ ])
+ }
+
+ process.env.NODE_ENV = 'development'
+ expect(maps).toThrow()
+ })
+
+ it('should not log a warning in production when loadChildren is not a method', function () {
+ const maps = function () {
+ return createRouteMap([
+ {
+ name: 'asyncFoo',
+ path: '/asyncFoo',
+ loadChildren: '/async/routes'
+ }
+ ])
+ }
+
+ expect(maps).not.toThrow()
+ })
+
+ it('should create a regex that matches the route and any sub-routes of the async route', function () {
+ const { nameMap } = createRouteMap([
+ {
+ name: 'asyncFoo',
+ path: '/asyncFoo',
+ loadChildren: function () {
+ return Promise.resolve([
+ {
+ name: 'asyncBar',
+ path: '/asyncBar',
+ component: Foo
+ }
+ ])
+ }
+ }
+ ])
+
+ expect(nameMap.asyncFoo.regex.test('/asyncFoo')).toBe(true)
+ expect(nameMap.asyncFoo.regex.test('/asyncFoo/')).toBe(true)
+ expect(nameMap.asyncFoo.regex.test('/asyncFoo/asyncBar')).toBe(true)
+ })
+ })
})
diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js
index bb932c702..857a1d4e4 100644
--- a/test/unit/specs/create-matcher.spec.js
+++ b/test/unit/specs/create-matcher.spec.js
@@ -4,6 +4,18 @@ import { createMatcher } from '../../../src/create-matcher'
const routes = [
{ path: '/', name: 'home', component: { name: 'home' }},
{ path: '/foo', name: 'foo', component: { name: 'foo' }},
+ {
+ path: '/async',
+ name: 'async',
+ loadChildren: function () {
+ return Promise.resolve([
+ {
+ name: 'asyncBar',
+ component: Foo
+ }
+ ])
+ }
+ }
]
describe('Creating Matcher', function () {
@@ -32,4 +44,34 @@ describe('Creating Matcher', function () {
match({ name: 'foo' }, routes[0])
expect(console.warn).not.toHaveBeenCalled()
})
+
+ describe('async children', function () {
+ it('should match the async route', function () {
+ const { name, matched } = match({ path: '/async' }, routes[0])
+ expect(matched.length).toBe(1)
+ expect(name).toBe('async')
+ })
+
+ it('should match the async route ending with a slash', function () {
+ const { name, matched } = match('/async/' , routes[0])
+ expect(matched.length).toBe(1)
+ expect(name).toBe('async')
+ })
+
+ it('should match the async route when the children have not been loaded', function () {
+ const { name, matched } = match('/async/foo', routes[0])
+ expect(matched.length).toBe(1)
+ expect(name).toBe('async')
+ })
+
+ it('should container property loadChildren with a value of null for non-async routes', function () {
+ const { loadChildren } = match('/foo', routes[0])
+ expect(loadChildren).toBe(null)
+ })
+
+ it('should container property loadChildren without a value of null for async routes', function () {
+ const { loadChildren } = match('/async/foo', routes[0])
+ expect(loadChildren).not.toBe(null)
+ })
+ })
})
diff --git a/types/router.d.ts b/types/router.d.ts
index 4d4c6a1d1..c7d9fe2c6 100644
--- a/types/router.d.ts
+++ b/types/router.d.ts
@@ -12,6 +12,7 @@ export type NavigationGuard = (
from: Route,
next: (to?: RawLocation | false | ((vm: Vue) => any) | void) => void
) => any
+export type LoadChildrenPromise = () => Promise;
declare class VueRouter {
constructor (options?: RouterOptions);
@@ -83,6 +84,7 @@ export interface RouteConfig {
props?: boolean | Object | RoutePropsFunction;
caseSensitive?: boolean;
pathToRegexpOptions?: PathToRegexpOptions;
+ loadChildren?: string | LoadChildrenPromise;
}
export interface RouteRecord {
diff --git a/types/test/index.ts b/types/test/index.ts
index 5fc8a51fc..4dee16454 100644
--- a/types/test/index.ts
+++ b/types/test/index.ts
@@ -82,6 +82,24 @@ const router = new VueRouter({
to.params;
return "/child";
}
+ },
+ {
+ path: "asyncChildren-webpackLoader",
+ loadChildren: "./children/routes#routes"
+ },
+ {
+ path: "asyncChildren",
+ loadChildren: () => new Promise(resolve => {
+ resolve([
+ {
+ path: "childA",
+ components: {
+ default: Foo,
+ bar: Bar
+ }
+ }
+ ]);
+ })
}
]},
{ path: "/home", alias: "/" },
@@ -161,7 +179,7 @@ router.go(-1);
router.back();
router.forward();
-const Components: ComponentOptions | typeof Vue = router.getMatchedComponents();
+const Components: (ComponentOptions | typeof Vue)[] = router.getMatchedComponents();
const vm = new Vue({
router,