From 9dc2c4807d1f80cae1dac0d1b58d489f1042c7be Mon Sep 17 00:00:00 2001 From: Aleksander Barszczewski Date: Fri, 14 Jul 2017 22:46:27 +0200 Subject: [PATCH 1/4] feat(router): add 'replaceRoutes' method 'replaceRoutes' method replaces all routes in router close #1234 --- docs/en/api/router-instance.md | 6 ++ examples/index.html | 1 + examples/replace-routes/app.js | 103 +++++++++++++++++++++++++++++ examples/replace-routes/index.html | 6 ++ src/create-matcher.js | 10 ++- src/history/base.js | 7 +- src/index.js | 5 ++ test/e2e/specs/replace-routes.js | 88 ++++++++++++++++++++++++ test/unit/specs/api.spec.js | 98 +++++++++++++++++++++++++++ 9 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 examples/replace-routes/app.js create mode 100644 examples/replace-routes/index.html create mode 100644 test/e2e/specs/replace-routes.js diff --git a/docs/en/api/router-instance.md b/docs/en/api/router-instance.md index ec94e06bb..6f94c7208 100644 --- a/docs/en/api/router-instance.md +++ b/docs/en/api/router-instance.md @@ -65,6 +65,12 @@ Dynamically add more routes to the router. The argument must be an Array using the same route config format with the `routes` constructor option. +- **router.replaceRoutes(routes)** + + > 2.8.0+ + + Dynamically replace all routes in the router. The argument must be an Array using the same route config format with the `routes` constructor option. + - **router.onReady(callback, [errorCallback])** > 2.2.0+ diff --git a/examples/index.html b/examples/index.html index f2bdf7225..c0c682f62 100644 --- a/examples/index.html +++ b/examples/index.html @@ -25,6 +25,7 @@

Vue Router Examples

  • Auth Flow
  • Discrete Components
  • Nested Routers
  • +
  • Replace Routes
  • diff --git a/examples/replace-routes/app.js b/examples/replace-routes/app.js new file mode 100644 index 000000000..4e9acd929 --- /dev/null +++ b/examples/replace-routes/app.js @@ -0,0 +1,103 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +Vue.use(VueRouter) + +const RouteX = { template: '
    Route X
    ' } +const RouteY = { template: '
    Route Y
    ' } +const RouteZ = { template: '
    Route Z
    ' } + +const Route1 = { template: '
    Route 1
    ' } +const Route2 = { template: '
    Route 2
    ' } + +const Route3 = { template: '
    Route 3
    ' } +const Route4 = { template: '
    Route 4
    ' } + +const routes1 = [ + { path: '/common1', name: 'common1', component: RouteX }, + { path: '/common2', name: 'common2', component: RouteZ }, + { path: '/route-1', name: 'route1', component: Route1 }, + { path: '/route-2', name: 'route2', component: Route2 } +] + +const routes2 = [ + { path: '/common1', name: 'common1', component: RouteY }, + { path: '/common2', name: 'common2', component: RouteZ }, + { path: '/route-3', name: 'route3', component: Route3 }, + { path: '/route-4', name: 'route4', component: Route4 } +] + +const router = new VueRouter({ + mode: 'history', + base: __dirname, + routes: [] +}) + +new Vue({ + router, + template: ` +
    +

    Replace Routes

    + +
    +
      +
    • + /common1 (X) +
    • +
    • + /common2 (Z) +
    • +
    • + /route-1 +
    • +
    • + /route-2 (named) +
    • +
    + +
    +
    +
      +
    • + /common1 (Y) +
    • +
    • + /common2 (Z) +
    • +
    • + /route-3 +
    • +
    • + /route-4 (named) +
    • +
    + +
    + +
    +
    + Current route set: {{ routeSet }} +
    + Current route content: +
    + +
    + `, + data () { + return { routeSet: null } + }, + methods: { + loadRoutes1 () { + router.replaceRoutes(routes1) + this.routeSet = 'set1' + }, + loadRoutes2 () { + router.replaceRoutes(routes2) + this.routeSet = 'set2' + }, + removeAllRoutes () { + router.replaceRoutes([]) + this.routeSet = null + } + } +}).$mount('#app') diff --git a/examples/replace-routes/index.html b/examples/replace-routes/index.html new file mode 100644 index 000000000..ff4d5ea7e --- /dev/null +++ b/examples/replace-routes/index.html @@ -0,0 +1,6 @@ + + +← Examples index +
    + + diff --git a/src/create-matcher.js b/src/create-matcher.js index 4e96d215e..aeeb2b1c0 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -11,6 +11,7 @@ import { normalizeLocation } from './util/location' export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; addRoutes: (routes: Array) => void; + removeRoutes: () => void; }; export function createMatcher ( @@ -23,6 +24,12 @@ export function createMatcher ( createRouteMap(routes, pathList, pathMap, nameMap) } + function removeRoutes () { + pathList.splice(0, pathList.length) + Object.keys(pathMap).forEach(key => (delete pathMap[key])) + Object.keys(nameMap).forEach(key => (delete nameMap[key])) + } + function match ( raw: RawLocation, currentRoute?: Route, @@ -168,7 +175,8 @@ export function createMatcher ( return { match, - addRoutes + addRoutes, + removeRoutes } } diff --git a/src/history/base.js b/src/history/base.js index 0c90dafa7..d60538abc 100644 --- a/src/history/base.js +++ b/src/history/base.js @@ -100,7 +100,12 @@ export class History { if ( isSameRoute(route, current) && // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length + route.matched.length === current.matched.length && + // additional check for case when routes were replaced and we have + // exactly same route but different component(s) + !route.matched.find((matched, index) => { + return matched.components !== current.matched[index].components + }) ) { this.ensureURL() return abort() diff --git a/src/index.js b/src/index.js index a1a7b8507..c16cb91f6 100644 --- a/src/index.js +++ b/src/index.js @@ -212,6 +212,11 @@ export default class VueRouter { this.history.transitionTo(this.history.getCurrentLocation()) } } + + replaceRoutes (routes: Array) { + this.matcher.removeRoutes() + this.addRoutes(routes) + } } function registerHook (list: Array, fn: Function): Function { diff --git a/test/e2e/specs/replace-routes.js b/test/e2e/specs/replace-routes.js new file mode 100644 index 000000000..a8c5ed21b --- /dev/null +++ b/test/e2e/specs/replace-routes.js @@ -0,0 +1,88 @@ +module.exports = { + 'replace routes': function (browser) { + browser + .url('http://localhost:8080/replace-routes/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 8) + + // not set is loaded + .assert.containsText('.route-set', '') + .assert.containsText('.view', '') + + // load set 1 + .click('.btn1') + .assert.urlEquals('http://localhost:8080/replace-routes/') + .assert.containsText('.route-set', 'set1') + .assert.containsText('.view', '') + + // load route set1 / route X + .click('.set1 li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/replace-routes/common1') + .assert.containsText('.route-set', 'set1') + .assert.containsText('.view', 'Route X') + + // load set2 and check transition to Route Y + .click('.btn2') + .assert.urlEquals('http://localhost:8080/replace-routes/common1') + .assert.containsText('.route-set', 'set2') + .assert.containsText('.view', 'Route Y') + + // load route set2 / route Z + .click('.set2 li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/replace-routes/common2') + .assert.containsText('.route-set', 'set2') + .assert.containsText('.view', 'Route Z') + + // load set1 and check transition to route Z + .click('.btn1') + .assert.urlEquals('http://localhost:8080/replace-routes/common2') + .assert.containsText('.route-set', 'set1') + .assert.containsText('.view', 'Route Z') + + // load set1 / route 1 + .click('.set1 li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/replace-routes/route-1') + .assert.containsText('.route-set', 'set1') + .assert.containsText('.view', 'Route 1') + + // load set2 and check that no route matched + .click('.btn2') + .assert.urlEquals('http://localhost:8080/replace-routes/route-1') + .assert.containsText('.route-set', 'set2') + .assert.containsText('.view', '') + + // load set2 / route 3 + .click('.set2 li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/replace-routes/route-3') + .assert.containsText('.route-set', 'set2') + .assert.containsText('.view', 'Route 3') + + // load set1 and route 2 + .click('.btn1') + .click('.set1 li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/replace-routes/route-2') + .assert.containsText('.route-set', 'set1') + .assert.containsText('.view', 'Route 2') + + // load set2 and route 4 + .click('.btn2') + .click('.set2 li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/replace-routes/route-4') + .assert.containsText('.route-set', 'set2') + .assert.containsText('.view', 'Route 4') + + // remove all routes + .click('.btn3') + .assert.urlEquals('http://localhost:8080/replace-routes/route-4') + .assert.containsText('.route-set', '') + .assert.containsText('.view', '') + + // load set1 / route X and check that no route matched + .click('.set1 li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/replace-routes/common1') + .assert.containsText('.route-set', '') + .assert.containsText('.view', '') + + .end() + } +} diff --git a/test/unit/specs/api.spec.js b/test/unit/specs/api.spec.js index 57e912cd9..a05dfb510 100644 --- a/test/unit/specs/api.spec.js +++ b/test/unit/specs/api.spec.js @@ -77,6 +77,104 @@ describe('router.addRoutes', () => { }) }) +describe('router.replaceRoutes', () => { + it('should replace routes', () => { + const routes1 = [ + { path: '/a', component: { name: 'A' }} + ] + const routes2 = [ + { path: '/b', component: { name: 'B' }} + ] + const router = new Router({ + mode: 'abstract', + routes: routes1 + }) + + router.push('/a') + let components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('A') + + router.push('/b') + components = router.getMatchedComponents() + expect(components.length).toBe(0) + + router.replaceRoutes(routes2) + + // /b should be added + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('B') + + // /a should be removed + router.push('/a') + components = router.getMatchedComponents() + expect(components.length).toBe(0) + + router.push('/b') + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('B') + + router.replaceRoutes(routes1) + + // /b should be removed + components = router.getMatchedComponents() + expect(components.length).toBe(0) + }) + + it('should remove all routes if empty array of routes passed', () => { + const router = new Router({ + mode: 'abstract', + routes: [ + { path: '/a', component: { name: 'A' }}, + { path: '/b', component: { name: 'B' }} + ] + }) + + router.push('/a') + let components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('A') + + router.push('/b') + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('B') + + router.replaceRoutes([]) + + components = router.getMatchedComponents() + expect(components.length).toBe(0) + + router.push('/b') + components = router.getMatchedComponents() + expect(components.length).toBe(0) + }) + + it('should update components of routes with same path', () => { + 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.replaceRoutes([ + { path: '/a', component: { name: 'B' }} + ]) + + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('B') + }) +}) + describe('router.push/replace callbacks', () => { let calls = [] let router, spy1, spy2 From 7d09dd4d99da83374089b75c6b08e272c2c176e1 Mon Sep 17 00:00:00 2001 From: Aleksander Barszczewski Date: Fri, 14 Jul 2017 23:13:20 +0200 Subject: [PATCH 2/4] fix: fix replace-routes example and test --- examples/replace-routes/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/replace-routes/app.js b/examples/replace-routes/app.js index 4e9acd929..85fbb028b 100644 --- a/examples/replace-routes/app.js +++ b/examples/replace-routes/app.js @@ -88,16 +88,16 @@ new Vue({ }, methods: { loadRoutes1 () { - router.replaceRoutes(routes1) this.routeSet = 'set1' + router.replaceRoutes(routes1) }, loadRoutes2 () { - router.replaceRoutes(routes2) this.routeSet = 'set2' + router.replaceRoutes(routes2) }, removeAllRoutes () { - router.replaceRoutes([]) this.routeSet = null + router.replaceRoutes([]) } } }).$mount('#app') From 0bff147fa59d36e6774a9c899a01984dea13c4e3 Mon Sep 17 00:00:00 2001 From: Aleksander Barszczewski Date: Fri, 14 Jul 2017 23:19:49 +0200 Subject: [PATCH 3/4] fix: fix replace-routes test --- test/e2e/specs/replace-routes.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/e2e/specs/replace-routes.js b/test/e2e/specs/replace-routes.js index a8c5ed21b..2fc0b825d 100644 --- a/test/e2e/specs/replace-routes.js +++ b/test/e2e/specs/replace-routes.js @@ -11,6 +11,7 @@ module.exports = { // load set 1 .click('.btn1') + .waitFor(100) .assert.urlEquals('http://localhost:8080/replace-routes/') .assert.containsText('.route-set', 'set1') .assert.containsText('.view', '') @@ -23,6 +24,7 @@ module.exports = { // load set2 and check transition to Route Y .click('.btn2') + .waitFor(100) .assert.urlEquals('http://localhost:8080/replace-routes/common1') .assert.containsText('.route-set', 'set2') .assert.containsText('.view', 'Route Y') @@ -35,6 +37,7 @@ module.exports = { // load set1 and check transition to route Z .click('.btn1') + .waitFor(100) .assert.urlEquals('http://localhost:8080/replace-routes/common2') .assert.containsText('.route-set', 'set1') .assert.containsText('.view', 'Route Z') @@ -47,6 +50,7 @@ module.exports = { // load set2 and check that no route matched .click('.btn2') + .waitFor(100) .assert.urlEquals('http://localhost:8080/replace-routes/route-1') .assert.containsText('.route-set', 'set2') .assert.containsText('.view', '') @@ -59,6 +63,7 @@ module.exports = { // load set1 and route 2 .click('.btn1') + .waitFor(100) .click('.set1 li:nth-child(4) a') .assert.urlEquals('http://localhost:8080/replace-routes/route-2') .assert.containsText('.route-set', 'set1') @@ -66,6 +71,7 @@ module.exports = { // load set2 and route 4 .click('.btn2') + .waitFor(100) .click('.set2 li:nth-child(4) a') .assert.urlEquals('http://localhost:8080/replace-routes/route-4') .assert.containsText('.route-set', 'set2') @@ -73,6 +79,7 @@ module.exports = { // remove all routes .click('.btn3') + .waitFor(100) .assert.urlEquals('http://localhost:8080/replace-routes/route-4') .assert.containsText('.route-set', '') .assert.containsText('.view', '') From a85c6168e404cea3bd82d5365d7182f1070acbab Mon Sep 17 00:00:00 2001 From: Aleksander Barszczewski Date: Fri, 14 Jul 2017 23:24:56 +0200 Subject: [PATCH 4/4] fix: increase timeout after replacing routes in tests --- test/e2e/specs/replace-routes.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/specs/replace-routes.js b/test/e2e/specs/replace-routes.js index 2fc0b825d..f2c852428 100644 --- a/test/e2e/specs/replace-routes.js +++ b/test/e2e/specs/replace-routes.js @@ -11,7 +11,7 @@ module.exports = { // load set 1 .click('.btn1') - .waitFor(100) + .waitFor(300) .assert.urlEquals('http://localhost:8080/replace-routes/') .assert.containsText('.route-set', 'set1') .assert.containsText('.view', '') @@ -24,7 +24,7 @@ module.exports = { // load set2 and check transition to Route Y .click('.btn2') - .waitFor(100) + .waitFor(300) .assert.urlEquals('http://localhost:8080/replace-routes/common1') .assert.containsText('.route-set', 'set2') .assert.containsText('.view', 'Route Y') @@ -37,7 +37,7 @@ module.exports = { // load set1 and check transition to route Z .click('.btn1') - .waitFor(100) + .waitFor(300) .assert.urlEquals('http://localhost:8080/replace-routes/common2') .assert.containsText('.route-set', 'set1') .assert.containsText('.view', 'Route Z') @@ -50,7 +50,7 @@ module.exports = { // load set2 and check that no route matched .click('.btn2') - .waitFor(100) + .waitFor(300) .assert.urlEquals('http://localhost:8080/replace-routes/route-1') .assert.containsText('.route-set', 'set2') .assert.containsText('.view', '') @@ -63,7 +63,7 @@ module.exports = { // load set1 and route 2 .click('.btn1') - .waitFor(100) + .waitFor(300) .click('.set1 li:nth-child(4) a') .assert.urlEquals('http://localhost:8080/replace-routes/route-2') .assert.containsText('.route-set', 'set1') @@ -71,7 +71,7 @@ module.exports = { // load set2 and route 4 .click('.btn2') - .waitFor(100) + .waitFor(300) .click('.set2 li:nth-child(4) a') .assert.urlEquals('http://localhost:8080/replace-routes/route-4') .assert.containsText('.route-set', 'set2') @@ -79,7 +79,7 @@ module.exports = { // remove all routes .click('.btn3') - .waitFor(100) + .waitFor(300) .assert.urlEquals('http://localhost:8080/replace-routes/route-4') .assert.containsText('.route-set', '') .assert.containsText('.view', '')