Skip to content

feat(router): add 'replaceRoutes' method #1603

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

Closed
Closed
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
6 changes: 6 additions & 0 deletions docs/en/api/router-instance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ <h1>Vue Router Examples</h1>
<li><a href="auth-flow">Auth Flow</a></li>
<li><a href="discrete-components">Discrete Components</a></li>
<li><a href="nested-router">Nested Routers</a></li>
<li><a href="replace-routes">Replace Routes</a></li>
</ul>
</body>
</html>
103 changes: 103 additions & 0 deletions examples/replace-routes/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const RouteX = { template: '<div>Route X</div>' }
const RouteY = { template: '<div>Route Y</div>' }
const RouteZ = { template: '<div>Route Z</div>' }

const Route1 = { template: '<div>Route 1</div>' }
const Route2 = { template: '<div>Route 2</div>' }

const Route3 = { template: '<div>Route 3</div>' }
const Route4 = { template: '<div>Route 4</div>' }

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: `
<div id="app">
<h1>Replace Routes</h1>

<div style="display: inline-block;">
<ul class="set1">
<li><router-link to="/common1">
/common1 (X)
</router-link></li>
<li><router-link to="/common2">
/common2 (Z)
</router-link></li>
<li><router-link to="/route-1">
/route-1
</router-link></li>
<li><router-link :to="{ name: 'route2' }">
/route-2 (named)
</router-link></li>
</ul>
<button @click="loadRoutes1" class="btn1">Load routes set 1</button>
</div>
<div style="display: inline-block;">
<ul class="set2">
<li><router-link to="/common1">
/common1 (Y)
</router-link></li>
<li><router-link to="/common2">
/common2 (Z)
</router-link></li>
<li><router-link to="/route-3">
/route-3
</router-link></li>
<li><router-link :to="{ name: 'route4' }">
/route-4 (named)
</router-link></li>
</ul>
<button @click="loadRoutes2" class="btn2">Load route set 2</button>
</div>
<button @click="removeAllRoutes" class="btn3">Remove all routes</button>
<br/>
<br/>
Current route set: <span class="route-set">{{ routeSet }}</span>
<br/>
Current route content:
<div class="view"><router-view :style="{ display: 'inline-block' }"></router-view></div>

</div>
`,
data () {
return { routeSet: null }
},
methods: {
loadRoutes1 () {
this.routeSet = 'set1'
router.replaceRoutes(routes1)
},
loadRoutes2 () {
this.routeSet = 'set2'
router.replaceRoutes(routes2)
},
removeAllRoutes () {
this.routeSet = null
router.replaceRoutes([])
}
}
}).$mount('#app')
6 changes: 6 additions & 0 deletions examples/replace-routes/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<script src="/__build__/shared.js"></script>
<script src="/__build__/replace-routes.js"></script>
10 changes: 9 additions & 1 deletion src/create-matcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { normalizeLocation } from './util/location'
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
removeRoutes: () => void;
};

export function createMatcher (
Expand All @@ -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,
Expand Down Expand Up @@ -168,7 +175,8 @@ export function createMatcher (

return {
match,
addRoutes
addRoutes,
removeRoutes
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/history/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ export default class VueRouter {
this.history.transitionTo(this.history.getCurrentLocation())
}
}

replaceRoutes (routes: Array<RouteConfig>) {
this.matcher.removeRoutes()
this.addRoutes(routes)
}
}

function registerHook (list: Array<any>, fn: Function): Function {
Expand Down
95 changes: 95 additions & 0 deletions test/e2e/specs/replace-routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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')
.waitFor(300)
.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')
.waitFor(300)
.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')
.waitFor(300)
.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')
.waitFor(300)
.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')
.waitFor(300)
.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')
.waitFor(300)
.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')
.waitFor(300)
.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()
}
}
98 changes: 98 additions & 0 deletions test/unit/specs/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down