Skip to content

Commit a2a300d

Browse files
Spice-Zkiaking
andcommitted
feat: make it compatible with vue3 (vuex 4 and router 4) (#100)
Co-authored-by: Kia King Ishii <[email protected]>
1 parent f3bf651 commit a2a300d

File tree

4 files changed

+1400
-1480
lines changed

4 files changed

+1400
-1480
lines changed

Diff for: package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
},
3636
"homepage": "https://github.com/vuejs/vuex-router-sync#readme",
3737
"peerDependencies": {
38-
"vue-router": "^3.0.0",
39-
"vuex": "^3.0.0"
38+
"vue-router": "^4.0.2",
39+
"vuex": "^4.0.0-rc.2"
4040
},
4141
"devDependencies": {
4242
"@rollup/plugin-commonjs": "^17.1.0",
@@ -58,8 +58,8 @@
5858
"ts-jest": "^26.5.0",
5959
"tslib": "^2.1.0",
6060
"typescript": "3.9.7",
61-
"vue": "^2.6.12",
62-
"vue-router": "^3.5.1",
63-
"vuex": "^3.6.2"
61+
"vue": "^3.0.5",
62+
"vue-router": "^4.0.2",
63+
"vuex": "^4.0.0-rc.2"
6464
}
6565
}

Diff for: src/index.ts

+19-26
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,30 @@
11
import { Store } from 'vuex'
2-
import VueRouter, { Route } from 'vue-router'
2+
import { Router, RouteLocationNormalized } from 'vue-router'
33

44
export interface SyncOptions {
55
moduleName: string
66
}
77

8-
export interface State {
9-
name?: string | null
10-
path: string
11-
hash: string
12-
query: Record<string, string | (string | null)[]>
13-
params: Record<string, string>
14-
fullPath: string
15-
meta?: any
8+
export interface State
9+
extends Omit<RouteLocationNormalized, 'matched' | 'redirectedFrom'> {
1610
from?: Omit<State, 'from'>
1711
}
1812

1913
export interface Transition {
20-
to: Route
21-
from: Route
14+
to: RouteLocationNormalized
15+
from: RouteLocationNormalized
2216
}
2317

2418
export function sync(
2519
store: Store<any>,
26-
router: VueRouter,
20+
router: Router,
2721
options?: SyncOptions
2822
): () => void {
2923
const moduleName = (options || {}).moduleName || 'route'
3024

3125
store.registerModule(moduleName, {
3226
namespaced: true,
33-
state: cloneRoute(router.currentRoute),
27+
state: cloneRoute(router.currentRoute.value),
3428
mutations: {
3529
ROUTE_CHANGED(_state: State, transition: Transition): void {
3630
store.state[moduleName] = cloneRoute(transition.to, transition.from)
@@ -44,18 +38,18 @@ export function sync(
4438
// sync router on store change
4539
const storeUnwatch = store.watch(
4640
(state) => state[moduleName],
47-
(route: Route) => {
41+
(route: RouteLocationNormalized) => {
4842
const { fullPath } = route
4943
if (fullPath === currentPath) {
5044
return
5145
}
5246
if (currentPath != null) {
5347
isTimeTraveling = true
54-
router.push(route as any)
48+
router.push(route)
5549
}
5650
currentPath = fullPath
5751
},
58-
{ sync: true } as any
52+
{ flush: 'sync' }
5953
)
6054

6155
// sync store on router navigation
@@ -69,22 +63,21 @@ export function sync(
6963
})
7064

7165
return function unsync(): void {
72-
// On unsync, remove router hook
73-
if (afterEachUnHook != null) {
74-
afterEachUnHook()
75-
}
66+
// remove router hook
67+
afterEachUnHook()
7668

77-
// On unsync, remove store watch
78-
if (storeUnwatch != null) {
79-
storeUnwatch()
80-
}
69+
// remove store watch
70+
storeUnwatch()
8171

82-
// On unsync, unregister Module with store
72+
// unregister Module with store
8373
store.unregisterModule(moduleName)
8474
}
8575
}
8676

87-
function cloneRoute(to: Route, from?: Route): State {
77+
function cloneRoute(
78+
to: RouteLocationNormalized,
79+
from?: RouteLocationNormalized
80+
): State {
8881
const clone: State = {
8982
name: to.name,
9083
path: to.path,

Diff for: test/index.spec.ts

+131-50
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,64 @@
1-
import Vue from 'vue'
2-
import Vuex, { mapState } from 'vuex'
3-
import VueRouter from 'vue-router'
1+
import { createApp, defineComponent, h, computed, nextTick } from 'vue'
2+
import { createStore, useStore } from 'vuex'
3+
import { createRouter, createMemoryHistory, RouterView } from 'vue-router'
44
import { sync } from '@/index'
55

6-
Vue.use(Vuex)
7-
Vue.use(VueRouter)
6+
async function run(originalModuleName: string, done: Function): Promise<void> {
7+
const moduleName = originalModuleName || 'route'
88

9-
function run(originalModuleName: string, done: Function): void {
10-
const moduleName: string = originalModuleName || 'route'
11-
12-
const store = new Vuex.Store({
13-
state: { msg: 'foo' }
9+
const store = createStore({
10+
state() {
11+
return { msg: 'foo' }
12+
}
1413
})
1514

16-
const Home = Vue.extend({
17-
computed: mapState(moduleName, {
18-
path: (state: any) => state.fullPath,
19-
foo: (state: any) => state.params.foo,
20-
bar: (state: any) => state.params.bar
21-
}),
22-
render(h) {
23-
return h('div', [this.path, ' ', this.foo, ' ', this.bar])
15+
const Home = defineComponent({
16+
setup() {
17+
const store = useStore()
18+
const path = computed(() => store.state[moduleName].fullPath)
19+
const foo = computed(() => store.state[moduleName].params.foo)
20+
const bar = computed(() => store.state[moduleName].params.bar)
21+
return () => h('div', [path.value, ' ', foo.value, ' ', bar.value])
2422
}
2523
})
2624

27-
const router = new VueRouter({
28-
mode: 'abstract',
29-
routes: [{ path: '/:foo/:bar', component: Home }]
25+
const router = createRouter({
26+
history: createMemoryHistory(),
27+
routes: [
28+
{
29+
path: '/',
30+
component: {
31+
template: 'root'
32+
}
33+
},
34+
{ path: '/:foo/:bar', component: Home }
35+
]
3036
})
3137

32-
sync(store, router, {
33-
moduleName: originalModuleName
34-
})
38+
originalModuleName
39+
? sync(store, router, { moduleName: originalModuleName })
40+
: sync(store, router)
3541

3642
router.push('/a/b')
43+
await router.isReady()
3744
expect((store.state as any)[moduleName].fullPath).toBe('/a/b')
3845
expect((store.state as any)[moduleName].params).toEqual({
3946
foo: 'a',
4047
bar: 'b'
4148
})
4249

43-
const app = new Vue({
44-
store,
45-
router,
46-
render: (h) => h('router-view')
47-
}).$mount()
50+
const rootEl = document.createElement('div')
51+
document.body.appendChild(rootEl)
4852

49-
expect(app.$el.textContent).toBe('/a/b a b')
53+
const app = createApp({
54+
render: () => h(RouterView)
55+
})
56+
app.use(store)
57+
app.use(router)
58+
app.mount(rootEl)
5059

51-
router.push('/c/d?n=1#hello')
60+
expect(rootEl.textContent).toBe('/a/b a b')
61+
await router.push('/c/d?n=1#hello')
5262
expect((store.state as any)[moduleName].fullPath).toBe('/c/d?n=1#hello')
5363
expect((store.state as any)[moduleName].params).toEqual({
5464
foo: 'c',
@@ -57,49 +67,120 @@ function run(originalModuleName: string, done: Function): void {
5767
expect((store.state as any)[moduleName].query).toEqual({ n: '1' })
5868
expect((store.state as any)[moduleName].hash).toEqual('#hello')
5969

60-
Vue.nextTick(() => {
61-
expect(app.$el.textContent).toBe('/c/d?n=1#hello c d')
70+
nextTick(() => {
71+
expect(rootEl.textContent).toBe('/c/d?n=1#hello c d')
6272
done()
6373
})
6474
}
6575

66-
test('default usage', (done) => {
67-
run('', done)
76+
test('default usage', async (done) => {
77+
await run('', done)
6878
})
6979

70-
test('with custom moduleName', (done) => {
71-
run('moduleName', done)
80+
test('with custom moduleName', async (done) => {
81+
await run('moduleName', done)
7282
})
7383

74-
test('unsync', (done) => {
75-
const store = new Vuex.Store({})
84+
test('unsync', async (done) => {
85+
const store = createStore({
86+
state() {
87+
return { msg: 'foo' }
88+
}
89+
})
90+
7691
spyOn(store, 'watch').and.callThrough()
7792

78-
const router = new VueRouter()
93+
const router = createRouter({
94+
history: createMemoryHistory(),
95+
routes: [
96+
{
97+
path: '/',
98+
component: {
99+
template: 'root'
100+
}
101+
}
102+
]
103+
})
79104

80105
const moduleName = 'testDesync'
81106
const unsync = sync(store, router, {
82107
moduleName: moduleName
83108
})
84109

85110
expect(unsync).toBeInstanceOf(Function)
86-
87111
// Test module registered, store watched, router hooked
88112
expect((store as any).state[moduleName]).toBeDefined()
89113
expect((store as any).watch).toHaveBeenCalled()
90-
expect((store as any)._watcherVM).toBeDefined()
91-
expect((store as any)._watcherVM._watchers).toBeDefined()
92-
expect((store as any)._watcherVM._watchers.length).toBe(1)
93-
expect((router as any).afterHooks).toBeDefined()
94-
expect((router as any).afterHooks.length).toBe(1)
95114

96115
// Now unsync vuex-router-sync
97116
unsync()
98117

99-
// Ensure router unhooked, store-unwatched, module unregistered
100-
expect((router as any).afterHooks.length).toBe(0)
101-
expect((store as any)._watcherVm).toBeUndefined()
118+
// Ensure module unregistered, no store change
119+
router.push('/')
120+
await router.isReady()
102121
expect((store as any).state[moduleName]).toBeUndefined()
103-
122+
expect((store as any).state).toEqual({ msg: 'foo' })
104123
done()
105124
})
125+
126+
test('time traveling', async () => {
127+
const store = createStore({
128+
state() {
129+
return { msg: 'foo' }
130+
}
131+
})
132+
133+
const router = createRouter({
134+
history: createMemoryHistory(),
135+
routes: [
136+
{
137+
path: '/',
138+
component: {
139+
template: 'root'
140+
}
141+
},
142+
{
143+
path: '/a',
144+
component: {
145+
template: 'a'
146+
}
147+
}
148+
]
149+
})
150+
151+
sync(store, router)
152+
153+
const state1 = clone(store.state)
154+
155+
// time travel before any route change so that we can test `currentPath`
156+
// being `undefined`
157+
store.replaceState(state1)
158+
159+
expect((store.state as any).route.path).toBe('/')
160+
161+
// change route, save new state to time travel later on
162+
await router.push('/a')
163+
164+
expect((store.state as any).route.path).toBe('/a')
165+
166+
const state2 = clone(store.state)
167+
168+
// change route again so that we're on different route than `state2`
169+
await router.push('/')
170+
171+
expect((store.state as any).route.path).toBe('/')
172+
173+
// time travel to check we go back to the old route
174+
store.replaceState(state2)
175+
176+
expect((store.state as any).route.path).toBe('/a')
177+
178+
// final push to the route to fire `afterEach` hook on router
179+
await router.push('/a')
180+
181+
expect((store.state as any).route.path).toBe('/a')
182+
})
183+
184+
function clone(state: any) {
185+
return JSON.parse(JSON.stringify(state))
186+
}

0 commit comments

Comments
 (0)