Skip to content

Commit

Permalink
fix(url): many bugs with useUrlQuerySync (#467)
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
brc-dd and kiaking committed Feb 13, 2024
1 parent 5782fbc commit 50e34e8
Showing 1 changed file with 100 additions and 87 deletions.
187 changes: 100 additions & 87 deletions lib/composables/Url.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,141 @@
import isEqual from 'lodash-es/isEqual'
import isPlainObject from 'lodash-es/isPlainObject'
import { type MaybeRef, unref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { type MaybeRef, nextTick, unref, watch } from 'vue'
import { type LocationQuery, useRoute, useRouter } from 'vue-router'

export interface UseUrlQuerySyncOptions {
casts?: Record<string, (value: any) => any>
exclude?: string[]
}

/**
* Sync between the given state and the URL query params.
*
* Caveats:
* - Vulnerable to prototype pollution.
* - Does not support objects inside arrays.
*/
export function useUrlQuerySync(
state: MaybeRef<Record<string, any>>,
{ casts = {}, exclude }: UseUrlQuerySyncOptions = {}
{ casts = {}, exclude = [] }: UseUrlQuerySyncOptions = {}
): void {
const router = useRouter()
const route = useRoute()
const router = useRouter()

const flattenInitialState = flattenObject(
JSON.parse(JSON.stringify(unref(state)))
)

setStateFromQuery()

watch(() => unref(state), setQueryFromState, {
deep: true,
immediate: true
})
const flattenedDefaultState = flattenObject(unref(state))

function setStateFromQuery() {
const flattenState = flattenObject(unref(state))
const flattenQuery = flattenObject(route.query)
let isSyncing = false

Object.keys(flattenQuery).forEach((key) => {
if (exclude?.includes(key)) {
return
watch(
() => route.query,
async () => {
if (!isSyncing) {
isSyncing = true
await setState()
isSyncing = false
}
},
{ deep: true, immediate: true }
)

const value = flattenQuery[key]
if (value === undefined) {
return
watch(
() => unref(state),
async () => {
if (!isSyncing) {
isSyncing = true
await setQuery()
isSyncing = false
}
},
{ deep: true }
)

const cast = casts[key]
flattenState[key] = cast ? cast(value) : value
})
async function setState() {
const newState = unflattenObject({ ...flattenedDefaultState, ...normalizeQuery(route.query) })
deepAssign(unref(state), newState)

deepAssign(unref(state), unflattenObject(flattenState))
await nextTick()
await setQuery()
}

async function setQueryFromState() {
const flattenState = flattenObject(unref(state))
const flattenQuery = flattenObject(route.query)
async function setQuery() {
const flattenedState = flattenObject(unref(state))
const newQuery: Record<string, any> = {}

Object.keys(flattenState).forEach((key) => {
if (exclude?.includes(key)) {
return
for (const key in flattenedState) {
if (!exclude.includes(key) && flattenedDefaultState[key] !== flattenedState[key]) {
newQuery[key] = flattenedState[key]
}
}

const value = flattenState[key]
const initialValue = flattenInitialState[key]
const currentQuery = normalizeQuery(route.query)

if (isEqual(value, initialValue)) {
delete flattenQuery[key]
} else {
flattenQuery[key] = value
}
if (!isEqual(newQuery, currentQuery)) {
await router.replace({ query: unflattenObject(newQuery) })
}
}

function normalizeQuery(query: LocationQuery): Record<string, any> {
const flattenedQuery = flattenObject(query)
const result: Record<string, any> = {}

if (flattenQuery[key] === undefined) {
delete flattenQuery[key]
for (const key in flattenedQuery) {
if (!exclude.includes(key)) {
result[key] = casts[key] ? casts[key](flattenedQuery[key]) : flattenedQuery[key]
}
})
}

await router.replace({ query: unflattenObject(flattenQuery) })
return result
}
}

function flattenObject(obj: Record<string, any>, prefix = '') {
return Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? `${prefix}.` : ''
if (isPlainObject(obj[k])) {
Object.assign(acc, flattenObject(obj[k], pre + k))
function flattenObject(obj: Record<string, any>, path: string[] = []): Record<string, any> {
const result: Record<string, any> = {}

for (const key in obj) {
const value = obj[key]

if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.assign(result, flattenObject(value, [...path, key]))
} else {
acc[pre + k] = obj[k]
result[path.concat(key).join('.')] = value
}
return acc
}, {} as Record<string, any>)
}
}

function unflattenObject(obj: Record<string, any>) {
return Object.keys(obj).reduce((acc, k) => {
const keys = k.split('.')
keys.reduce((a, c, i) => {
if (i === keys.length - 1) {
a[c] = obj[k]
} else {
a[c] = a[c] || {}
}
return a[c]
}, acc)
return acc
}, {} as Record<string, any>)
return result
}

function deepAssign(target: Record<string, any>, source: Record<string, any>) {
const dest = target
const src = source

if (isPlainObject(src)) {
Object.keys(src).forEach((key) => deepAssignBase(dest, src, key))
} else if (Array.isArray(src)) {
dest.length = src.length
src.forEach((_, key) => deepAssignBase(dest, src, key))
} else {
throw new TypeError('[deepAssign] src must be an object or array')
function unflattenObject(obj: Record<string, any>): Record<string, any> {
const result: Record<string, any> = {}

for (const key in obj) {
const value = obj[key]

let target = result
const keys = key.split('.')

for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
target = target[k] = target[k] || {}
}

target[keys[keys.length - 1]] = value
}

return result
}

function deepAssignBase(
dest: Record<string, any>,
src: Record<string, any>,
key: string | number
) {
if (typeof src[key] === 'object' && src[key] !== null) {
deepAssign(dest[key], src[key])
} else {
dest[key] = src[key]
function deepAssign(target: Record<string, any>, source: Record<string, any>) {
for (const key in source) {
const value = source[key]

if (Array.isArray(value)) {
target[key].splice(0, target[key].length, ...value)
} else if (value && typeof value === 'object') {
target[key] = deepAssign(target[key] || {}, value)
} else {
target[key] = value
}
}

return target
}

0 comments on commit 50e34e8

Please sign in to comment.