Skip to content
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

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

Merged
merged 12 commits into from
Feb 5, 2021
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...

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

export interface SyncOptions {
moduleName: string
}

export interface State {
posva marked this conversation as resolved.
Show resolved Hide resolved
name?: string | null
path: string
hash: string
query: Record<string, string | (string | null)[]>
params: Record<string, string>
fullPath: string
meta?: any
name?: RouteLocationNormalized['name']
path: RouteLocationNormalized['path']
hash: RouteLocationNormalized['hash']
query: RouteLocationNormalized['query']
params: RouteLocationNormalized['params']
fullPath: RouteLocationNormalized['fullPath']
meta?: RouteLocationNormalized['meta']
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,7 +44,7 @@ export function sync(
// sync router on store change
const storeUnwatch = store.watch(
posva marked this conversation as resolved.
Show resolved Hide resolved
(state) => state[moduleName],
(route: Route) => {
(route: RouteLocationNormalized) => {
const { fullPath } = route
if (fullPath === currentPath) {
return
Expand All @@ -55,7 +55,7 @@ export function sync(
}
currentPath = fullPath
},
{ sync: true } as any
{ flush: 'sync' }
)

// sync store on router navigation
Expand Down Expand Up @@ -84,7 +84,10 @@ export function sync(
}
}

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
111 changes: 65 additions & 46 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
import Vue from 'vue'
import Vuex, { mapState } from 'vuex'
import VueRouter from 'vue-router'
import { sync } from '@/index'
import { createApp, defineComponent, h, computed, nextTick } from 'vue'

Vue.use(Vuex)
Vue.use(VueRouter)
import { createStore, useStore } from 'vuex'
import { createRouter, createWebHistory, RouterView } from 'vue-router'
posva marked this conversation as resolved.
Show resolved Hide resolved
import { sync } from '@/index'

function run(originalModuleName: string, done: Function): void {
async function run(originalModuleName: string, done: Function): Promise<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: createWebHistory(),
routes: [
{
path: '/',
component: {
template: 'root'
}
},
{ path: '/:foo/:bar', component: Home }
]
})

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

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 +68,57 @@ 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: createWebHistory(),
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()
})
Loading