Skip to content

Commit

Permalink
feat: make it compatible with vue3 (vuex 4 and router 4) (#100)
Browse files Browse the repository at this point in the history
Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
  • Loading branch information
Spice-Z and kiaking committed Feb 5, 2021
1 parent b803df3 commit 4aa9a09
Show file tree
Hide file tree
Showing 4 changed files with 1,400 additions and 1,480 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,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": "^17.1.0",
Expand All @@ -58,8 +58,8 @@
"ts-jest": "^26.5.0",
"tslib": "^2.1.0",
"typescript": "3.9.7",
"vue": "^2.6.12",
"vue-router": "^3.5.1",
"vuex": "^3.6.2"
"vue": "^3.0.5",
"vue-router": "^4.0.2",
"vuex": "^4.0.0-rc.2"
}
}
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')
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

0 comments on commit 4aa9a09

Please sign in to comment.