Skip to content

Commit

Permalink
feat: allow passing state to history
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Mar 24, 2020
1 parent ffb13ad commit ac1c96f
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 250 deletions.
31 changes: 30 additions & 1 deletion e2e/modal/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,42 @@
<title>Vue Router Examples - Encoding</title>
<!-- TODO: replace with local imports for promises and anything else needed -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015"></script>

<style>
#dialog {
top: 0;
left: 0;
position: fixed;
width: 100vw;
height: 100vh;
border: none;
margin: 0;
padding: 0;
background-color: rgb(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
padding-top: 2%;
}
#dialog:not([open]) {
display: none;
}

/* container */
#dialog > * {
background-color: white;
max-width: 500px;
max-height: 300px;
padding: 1rem;
}
</style>
</head>
<body>
<a href="/">&lt;&lt; Back to Homepage</a>
<hr />

<div id="app">
<router-view></router-view>
<component :is="ViewComponent"></component>
</div>
</body>
</html>
278 changes: 67 additions & 211 deletions e2e/modal/index.ts
Original file line number Diff line number Diff line change
@@ -1,185 +1,25 @@
import { createRouter, createWebHistory, useRoute } from '../../src'
import { RouteComponent } from '../../src/types'
import { createApp, readonly, reactive, ref, watchEffect } from 'vue'
import { createRouter, createWebHistory, useRoute, useView } from '../../src'
import {
RouteComponent,
RouteLocationNormalizedResolved,
} from '../../src/types'
import { createApp, readonly, ref, watchEffect, computed, toRefs } from 'vue'

const users = readonly([
{ name: 'John' },
{ name: 'Jessica' },
{ name: 'James' },
])

const modalState = reactive({
showModal: false,
userId: 0,
})

const enum GhostNavigation {
none = 0,
restoreGhostUrl,
backToOriginal,
}

let navigationState: GhostNavigation = GhostNavigation.none
window.addEventListener('popstate', function customPopListener(event) {
let { state } = event
console.log('popstate!', navigationState, event.state)

// nested state machine to handle
if (navigationState !== GhostNavigation.none) {
if (navigationState === GhostNavigation.restoreGhostUrl) {
webHistory.replace(state.ghostURL)
console.log('replaced ghost', state.ghostURL)
navigationState = GhostNavigation.backToOriginal
webHistory.back(false)
} else if (navigationState === GhostNavigation.backToOriginal) {
navigationState = GhostNavigation.none
Object.assign(modalState, state.modalState)
console.log('came from a ghost navigation, nothing to do')
// let's remove the guard from navigating away, it will be added again by afterEach when
// entering the url
historyCleaner && historyCleaner()
historyCleaner = undefined
event.stopImmediatePropagation()
}

return
}
async function showUserModal(id: number) {
// add backgroundView state to the location so we can render a different view from the one
const backgroundView = router.currentRoute.value.fullPath

if (!state) return
// we did a back from a modal
if (state.forwardGhost && webHistory.state.ghostURL === state.forwardGhost) {
// make sure the url saved in the history stack is good
navigationState = GhostNavigation.restoreGhostUrl
cleanNavigationFromModalListener && cleanNavigationFromModalListener()
webHistory.forward(false)
// we did a forward to a modal
} else if (
state.ghostURL &&
state.ghostURL === webHistory.state.forwardGhost
) {
webHistory.replace(state.displayURL)
event.stopImmediatePropagation()
Object.assign(modalState, state.modalState)
// TODO: setup same listeners as state S
// we did a back to a modal
} else if (
state.ghostURL &&
state.ghostURL === webHistory.state.backwardGhost
) {
let remove = router.afterEach(() => {
Object.assign(modalState, state.modalState)
remove()
removeError()
})
// if the navigation fails, remove the listeners
let removeError = router.onError(() => {
console.log('navigation aborted, removing stuff')
remove()
removeError()
})
}
// if ((state && !state.forward) || state.showModal) {
// console.log('stopping it!')
// // copy showModal state
// modalState.showModal = !!state.showModal
// // don't let the router catch this one
// event.stopImmediatePropagation()
// }
})

const About: RouteComponent = {
template: `<div>
<h1>About</h1>
<p>If you came from a user modal, you should go back to it</p>
<button @click="back">Back</button>
</div>
`,
methods: {
back() {
window.history.back()
},
},
}

let historyCleaner: (() => void) | undefined

let cleanNavigationFromModalListener: (() => void) | undefined

function setupPostNavigationFromModal(ghostURL: string) {
let removePost: (() => void) | undefined
const removeGuard = router.beforeEach((to, from, next) => {
console.log('From', from.fullPath, '->', to.fullPath)
// change the URL before leaving so that when we go back we are navigating to the right url
webHistory.replace(ghostURL)
console.log('changed url', ghostURL)
removeGuard()
removePost = router.afterEach(() => {
console.log('✅ navigated away')
webHistory.replace(webHistory.location, {
backwardGhost: ghostURL,
})
removePost && removePost()
})

// trigger the navigation again, TODO: does it change anything
next(to.fullPath)
await router.push({
name: 'user',
params: { id: '' + id },
state: { backgroundView },
})

// remove any existing listener
cleanNavigationFromModalListener && cleanNavigationFromModalListener()

cleanNavigationFromModalListener = () => {
removeGuard()
removePost && removePost()
cleanNavigationFromModalListener = undefined
}
}

function showUserModal(id: number) {
const route = router.currentRoute.value
// generate a new entry that is exactly like the one we are on but with an extra query
// so it still counts like a navigation for the router when leaving it or when pushing on top
const ghostURLNormalized = router.resolve({
path: route.path,
query: { ...route.query, __m: Math.random() },
hash: route.hash,
})
// the url we want to show
let url = router.resolve({ name: 'user', params: { id: '' + id } })
const displayURL = url.fullPath
const ghostURL = ghostURLNormalized.fullPath
const originalURL = router.currentRoute.value.fullPath

webHistory.replace(router.currentRoute.value, {
// save that we are going to a ghost route
forwardGhost: ghostURL,
// save current modalState to be able to restore it when navigating away
// from the modal
modalState: { ...modalState },
})

// after saving the modal state, we can change it
modalState.userId = id
modalState.showModal = true

// push a new entry in the history stack with the ghost url in the state
// to be able to restore it
webHistory.push(displayURL, {
// the url that should be displayed while being on this entry
displayURL,
// the original url TODO: is it necessary?
originalURL,
// the url that resolves to the same components as originalURL but slightly different
// so that the router doesn't consider it as a duplicated navigation
ghostURL,
modalState: { ...modalState },
})

// make sure we clear what we did before leaving
// this will only trigger on `push`/`replace` because we are listening on `popstate`
// so that if we go to the previous entry we can stop the propagation so the router never knows
// and remove this listener ourselves
setupPostNavigationFromModal(ghostURL)
}

function closeUserModal() {
Expand All @@ -198,38 +38,65 @@ const Home: RouteComponent = {
</ul>
<dialog ref="modal" id="dialog">
<p>
User #{{ modalState.userId }}
<br>
Name: {{ users[modalState.userId].name }}
</p>
<router-link to="/about">Go somewhere else</router-link>
<br>
<button @click="closeUserModal">Close</button>
<div>
<div v-if="userId">
<p>
User #{{ userId }}
<br>
Name: {{ users[userId].name }}
</p>
<router-link to="/about">Go somewhere else</router-link>
<br>
<button @click="closeUserModal">Close</button>
</div>
</div>
</dialog>
</div>`,
setup() {
const modal = ref()
const modal = ref<HTMLDialogElement | HTMLElement>()
const route = useRoute()
const historyState = computed(() => route.fullPath && window.history.state)

const userId = computed(() => route.params.id)

watchEffect(() => {
if (!modal.value) return
const el = modal.value
if (!el) return

const show = modalState.showModal
const show = historyState.value.backgroundView
console.log('show modal?', show)
if (show) modal.value.show()
else modal.value.close()
if (show) {
if ('show' in el) el.show()
else el.setAttribute('open', '')
} else {
if ('close' in el) el.close()
else el.removeAttribute('open')
}
})

return {
modal,
historyState,
showUserModal,
closeUserModal,
modalState,
userId,
users,
}
},
}

const About: RouteComponent = {
template: `<div>
<h1>About</h1>
<button @click="back">Back</button>
<span> | </span>
<router-link to="/">Back home</router-link>
</div>`,
methods: {
back: () => history.back(),
},
}

const UserDetails: RouteComponent = {
template: `<div>
<h1>User #{{ id }}</h1>
Expand Down Expand Up @@ -260,33 +127,22 @@ router.beforeEach((to, from, next) => {
next()
})

router.afterEach(() => {
const { state } = window.history
console.log('afterEach', state)
if (state && state.displayURL) {
console.log('restoring', state.displayURL, 'for', state.originalURL)
// restore the state
Object.assign(modalState, state.modalState)
webHistory.replace(state.displayURL)
// history.pushState({ showModal: true }, '', url)
// historyCleaner && historyCleaner()
historyCleaner = router.beforeEach((to, from, next) => {
// add data to history state so it can be restored if we go back
webHistory.replace(state.ghostURL, {
modalState: { ...modalState },
})
// remove this guard
historyCleaner && historyCleaner()
// trigger the same navigation again
next(to.fullPath)
})
}
})

const app = createApp({
setup() {
const route = useRoute()
return { route }
const routeWithModal = computed(() => {
if (historyState.value.backgroundView) {
return router.resolve(
historyState.value.backgroundView
) as RouteLocationNormalizedResolved
} else {
return route
}
})
const historyState = computed(() => route.fullPath && window.history.state)
const ViewComponent = useView({ route: routeWithModal, name: 'default' })

return { route, ViewComponent, historyState, ...toRefs(route) }
},
})
app.use(router)
Expand Down
Loading

0 comments on commit ac1c96f

Please sign in to comment.