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

+ + +
+ ` +}).$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,