Skip to content
Open
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
12 changes: 7 additions & 5 deletions docs/en/advanced/navigation-guards.md
Original file line number Diff line number Diff line change
@@ -116,17 +116,19 @@ beforeRouteEnter (to, from, next) {
}
```

Note that `beforeRouteEnter` is the only guard that supports passing a callback to `next`. For `beforeRouteUpdate` and `beforeRouteLeave`, `this` is already available, so passing a callback is unnecessary and therefore *not supported*:

```js
beforeRouteUpdate (to, from, next) {
// just use `this`
// may use `this`
this.name = to.params.name
next()

// may also pass a callback to next
next(vm => {
vm.name = to.params.name
})
}
```

The **leave guard** is usually used to prevent the user from accidentally leaving the route with unsaved edits. The navigation can be canceled by calling `next(false)`.
The **leave guard** is usually used to prevent the user from accidentally leaving the route with unsaved edits. The navigation can be canceled by calling `next(false)`. Note that `beforeRouteLeave` is the only guard that does not support passing a callback to `next`.

```js
beforeRouteLeave (to, from , next) {
68 changes: 65 additions & 3 deletions examples/navigation-guards/app.js
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ const Baz = {
},
template: `
<div>
<p>baz ({{ saved ? 'saved' : 'not saved' }})<p>
<p>baz ({{ saved ? 'saved' : 'not saved' }})</p>
<button @click="saved = true">save</button>
</div>
`,
@@ -46,7 +46,7 @@ const Baz = {
}
}

// Baz implements an in-component beforeRouteEnter hook
// Qux implements an in-component beforeRouteEnter hook
const Qux = {
data () {
return {
@@ -87,6 +87,57 @@ const Quux = {
}
}

// Nested implements an in-component beforeRouteUpdate hook that uses
// next(vm => {})
const Nested = {
data () {
return {
calls: 0,
updates: 0
}
},
template: `
<div>
<p>
nested calls:{{ calls }} updates:{{ updates }}
</p>
<router-view></router-view>
</div>
`,
beforeRouteUpdate (to, from, next) {
// calls is incremented every time anything navigates, even if the
// navigation is canceled by a child component.
this.calls++

next(vm => {
// updates is only incremented once all children confirm and complete
// navigation. If any children load data async, then this will not be
// called until all children have called next() themselves. If any child
// cancels navigation, then this will never be called.
vm.updates++
})
}
}

// Buux implements a cancelable beforeRouteUpdate hook
const Buux = {
data () {
return {
prevId: 0
}
},
template: `<div>buux id:{{ $route.params.id }} prevId:{{ prevId }}</div>`,
beforeRouteUpdate (to, from, next) {
if (window.confirm(`Navigate to ${to.path}?`)) {
next(vm => {
vm.prevId = from.params.id
})
} else {
next(false)
}
}
}

const router = new VueRouter({
mode: 'history',
base: __dirname,
@@ -114,7 +165,14 @@ const router = new VueRouter({
} },

// in-component beforeRouteUpdate hook
{ path: '/quux/:id', component: Quux }
{ path: '/quux/:id', component: Quux },
{ path: '/nested', component: Nested,
children: [
{ path: '', component: Home },
{ path: 'qux', component: Qux },
{ path: 'buux/:id', component: Buux }
]
}
]
})

@@ -140,6 +198,10 @@ new Vue({
<li><router-link to="/qux-async">/qux-async</router-link></li>
<li><router-link to="/quux/1">/quux/1</router-link></li>
<li><router-link to="/quux/2">/quux/2</router-link></li>
<li><router-link to="/nested">/nested</router-link></li>
<li><router-link to="/nested/qux">/nested/qux</router-link></li>
<li><router-link to="/nested/buux/1">/nested/buux/1</router-link></li>
<li><router-link to="/nested/buux/2">/nested/buux/2</router-link></li>
</ul>
<router-view class="view"></router-view>
</div>
33 changes: 21 additions & 12 deletions src/history/base.js
Original file line number Diff line number Diff line change
@@ -112,13 +112,15 @@ export class History {
activated
} = resolveQueue(this.current.matched, route.matched)

const postCbs = []
const isValid = () => this.current === route
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
extractUpdateHooks(updated, postCbs, isValid),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
@@ -161,11 +163,9 @@ export class History {
}

runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const enterGuards = extractEnterGuards(activated, postCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
@@ -175,7 +175,7 @@ export class History {
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
postCbs.forEach(cb => { cb() })
})
}
})
@@ -266,10 +266,6 @@ function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function>
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
@@ -278,24 +274,37 @@ function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
}
}

function extractUpdateHooks (
updated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', (guard, instance, match, key) => {
var boundGuard = bindGuard(guard, instance)
if (boundGuard) {
return bindCbGuard(boundGuard, match, key, cbs, isValid)
}
})
}

function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
return bindEnterGuard(guard, match, key, cbs, isValid)
return bindCbGuard(guard, match, key, cbs, isValid)
})
}

function bindEnterGuard (
function bindCbGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return function (to, from, next) {
return guard(to, from, cb => {
next(cb)
if (typeof cb === 'function') {
30 changes: 28 additions & 2 deletions test/e2e/specs/navigation-guards.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ module.exports = {
browser
.url('http://localhost:8080/navigation-guards/')
.waitForElementVisible('#app', 1000)
.assert.count('li a', 8)
.assert.count('li a', 12)
.assert.containsText('.view', 'home')

.click('li:nth-child(2) a')
@@ -130,6 +130,32 @@ module.exports = {
.click('li:nth-child(7) a')
.assert.urlEquals('http://localhost:8080/navigation-guards/quux/1')
.assert.containsText('.view', 'id:1 prevId:2')
.end()
.click('li:nth-child(9) a')
.assert.urlEquals('http://localhost:8080/navigation-guards/nested')
.assert.containsText('.view', 'nested calls:0 updates:0')
.click('li:nth-child(10) a')
.assert.containsText('.view', 'nested calls:1 updates:0')
.waitFor(300)
.assert.urlEquals('http://localhost:8080/navigation-guards/nested/qux')
.assert.containsText('.view', 'nested calls:1 updates:1')
.assert.containsText('.view', 'Qux')
.click('li:nth-child(11) a')
.assert.urlEquals('http://localhost:8080/navigation-guards/nested/buux/1')
.assert.containsText('.view', 'nested calls:2 updates:2')
.assert.containsText('.view', 'buux id:1 prevId:0')
.click('li:nth-child(12) a')
.dismissAlert()
.assert.urlEquals('http://localhost:8080/navigation-guards/nested/buux/1')
.assert.containsText('.view', 'nested calls:3 updates:2')
.assert.containsText('.view', 'buux id:1 prevId:0')
.click('li:nth-child(12) a')
.acceptAlert()
.assert.urlEquals('http://localhost:8080/navigation-guards/nested/buux/2')
.assert.containsText('.view', 'nested calls:4 updates:3')
.assert.containsText('.view', 'buux id:2 prevId:1')
.click('li:nth-child(9) a')
.assert.urlEquals('http://localhost:8080/navigation-guards/nested')
.assert.containsText('.view', 'nested calls:5 updates:4')
.end()
}
}