Skip to content

UPDATE for Vue3 ( = vuex 4.x, vue-router 4.x) #100

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

Merged
merged 12 commits into from
Feb 5, 2021
Merged
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
},
"homepage": "https://github.com/vuejs/vuex-router-sync#readme",
"peerDependencies": {
"vue-router": "^3.0.0",
"vuex": "^3.0.0"
"vue-router": "^4.0.2",
"vuex": "^4.0.0-rc.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.1.0",
Expand All @@ -59,8 +59,8 @@
"ts-jest": "^25.4.0",
"tslib": "^1.11.1",
"typescript": "^3.8.3",
"vue": "^2.5.0",
"vue-router": "^3.0.0",
"vuex": "^3.0.0"
"vue": "^3.0.5",
"vue-router": "^4.0.2",
"vuex": "^4.0.0-rc.2"
Comment on lines +62 to +64
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to update other dependencies, but that should be other PR to keep it simple...

}
}
45 changes: 19 additions & 26 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
import { Store } from 'vuex'
import VueRouter, { Route } from 'vue-router'
import { Router, RouteLocationNormalized } from 'vue-router'

export interface SyncOptions {
moduleName: string
}

export interface State {
name?: string | null
path: string
hash: string
query: Record<string, string | (string | null)[]>
params: Record<string, string>
fullPath: string
meta?: any
export interface State
extends Omit<RouteLocationNormalized, 'matched' | 'redirectedFrom'> {
from?: Omit<State, 'from'>
}

export interface Transition {
to: Route
from: Route
to: RouteLocationNormalized
from: RouteLocationNormalized
}

export function sync(
store: Store<any>,
router: VueRouter,
router: Router,
options?: SyncOptions
): () => void {
const moduleName = (options || {}).moduleName || 'route'

store.registerModule(moduleName, {
namespaced: true,
state: cloneRoute(router.currentRoute),
state: cloneRoute(router.currentRoute.value),
mutations: {
ROUTE_CHANGED(_state: State, transition: Transition): void {
store.state[moduleName] = cloneRoute(transition.to, transition.from)
Expand All @@ -44,18 +38,18 @@ export function sync(
// sync router on store change
const storeUnwatch = store.watch(
(state) => state[moduleName],
(route: Route) => {
(route: RouteLocationNormalized) => {
const { fullPath } = route
if (fullPath === currentPath) {
return
}
if (currentPath != null) {
isTimeTraveling = true
router.push(route as any)
router.push(route)
}
currentPath = fullPath
},
{ sync: true } as any
{ flush: 'sync' }
)

// sync store on router navigation
Expand All @@ -69,22 +63,21 @@ export function sync(
})

return function unsync(): void {
// On unsync, remove router hook
if (afterEachUnHook != null) {
afterEachUnHook()
}
// remove router hook
afterEachUnHook()

// On unsync, remove store watch
if (storeUnwatch != null) {
storeUnwatch()
}
// remove store watch
storeUnwatch()

// On unsync, unregister Module with store
// unregister Module with store
store.unregisterModule(moduleName)
}
}

function cloneRoute(to: Route, from?: Route): State {
function cloneRoute(
to: RouteLocationNormalized,
from?: RouteLocationNormalized
): State {
const clone: State = {
name: to.name,
path: to.path,
Expand Down
181 changes: 131 additions & 50 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,64 @@
import Vue from 'vue'
import Vuex, { mapState } from 'vuex'
import VueRouter from 'vue-router'
import { createApp, defineComponent, h, computed, nextTick } from 'vue'
import { createStore, useStore } from 'vuex'
import { createRouter, createMemoryHistory, RouterView } from 'vue-router'
import { sync } from '@/index'

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

function run(originalModuleName: string, done: Function): void {
const moduleName: string = originalModuleName || 'route'

const store = new Vuex.Store({
state: { msg: 'foo' }
const store = createStore({
state() {
return { msg: 'foo' }
}
})

const Home = Vue.extend({
computed: mapState(moduleName, {
path: (state: any) => state.fullPath,
foo: (state: any) => state.params.foo,
bar: (state: any) => state.params.bar
}),
render(h) {
return h('div', [this.path, ' ', this.foo, ' ', this.bar])
const Home = defineComponent({
setup() {
const store = useStore()
const path = computed(() => store.state[moduleName].fullPath)
const foo = computed(() => store.state[moduleName].params.foo)
const bar = computed(() => store.state[moduleName].params.bar)
return () => h('div', [path.value, ' ', foo.value, ' ', bar.value])
}
})

const router = new VueRouter({
mode: 'abstract',
routes: [{ path: '/:foo/:bar', component: Home }]
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
component: {
template: 'root'
}
},
{ path: '/:foo/:bar', component: Home }
]
})

sync(store, router, {
moduleName: originalModuleName
})
originalModuleName
? sync(store, router, { moduleName: originalModuleName })
: sync(store, router)

router.push('/a/b')
await router.isReady()
expect((store.state as any)[moduleName].fullPath).toBe('/a/b')
expect((store.state as any)[moduleName].params).toEqual({
foo: 'a',
bar: 'b'
})

const app = new Vue({
store,
router,
render: (h) => h('router-view')
}).$mount()
const rootEl = document.createElement('div')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be easier and safer to use vue test utils?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will try to use vue test utils!
(Actually, I have not use it yet 😅 )

document.body.appendChild(rootEl)

expect(app.$el.textContent).toBe('/a/b a b')
const app = createApp({
render: () => h(RouterView)
})
app.use(store)
app.use(router)
app.mount(rootEl)

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

Vue.nextTick(() => {
expect(app.$el.textContent).toBe('/c/d?n=1#hello c d')
nextTick(() => {
expect(rootEl.textContent).toBe('/c/d?n=1#hello c d')
done()
})
}

test('default usage', (done) => {
run('', done)
test('default usage', async (done) => {
await run('', done)
})

test('with custom moduleName', (done) => {
run('moduleName', done)
test('with custom moduleName', async (done) => {
await run('moduleName', done)
})

test('unsync', (done) => {
const store = new Vuex.Store({})
test('unsync', async (done) => {
const store = createStore({
state() {
return { msg: 'foo' }
}
})

spyOn(store, 'watch').and.callThrough()

const router = new VueRouter()
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
component: {
template: 'root'
}
}
]
})

const moduleName = 'testDesync'
const unsync = sync(store, router, {
moduleName: moduleName
})

expect(unsync).toBeInstanceOf(Function)

// Test module registered, store watched, router hooked
expect((store as any).state[moduleName]).toBeDefined()
expect((store as any).watch).toHaveBeenCalled()
expect((store as any)._watcherVM).toBeDefined()
expect((store as any)._watcherVM._watchers).toBeDefined()
expect((store as any)._watcherVM._watchers.length).toBe(1)
expect((router as any).afterHooks).toBeDefined()
expect((router as any).afterHooks.length).toBe(1)

// Now unsync vuex-router-sync
unsync()

// Ensure router unhooked, store-unwatched, module unregistered
expect((router as any).afterHooks.length).toBe(0)
expect((store as any)._watcherVm).toBeUndefined()
// Ensure module unregistered, no store change
router.push('/')
await router.isReady()
expect((store as any).state[moduleName]).toBeUndefined()

expect((store as any).state).toEqual({ msg: 'foo' })
done()
})

test('time traveling', async () => {
const store = createStore({
state() {
return { msg: 'foo' }
}
})

const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
component: {
template: 'root'
}
},
{
path: '/a',
component: {
template: 'a'
}
}
]
})

sync(store, router)

const state1 = clone(store.state)

// time travel before any route change so that we can test `currentPath`
// being `undefined`
store.replaceState(state1)

expect((store.state as any).route.path).toBe('/')

// change route, save new state to time travel later on
await router.push('/a')

expect((store.state as any).route.path).toBe('/a')

const state2 = clone(store.state)

// change route again so that we're on different route than `state2`
await router.push('/')

expect((store.state as any).route.path).toBe('/')

// time travel to check we go back to the old route
store.replaceState(state2)

expect((store.state as any).route.path).toBe('/a')

// final push to the route to fire `afterEach` hook on router
await router.push('/a')

expect((store.state as any).route.path).toBe('/a')
})

function clone(state: any) {
return JSON.parse(JSON.stringify(state))
}
Loading