expect(stringifyQuery({ e: 'a', b: 'c' })).toEqual('e=a&b=c')
})
+ it('stringifies null values', () => {
+ expect(stringifyQuery({ e: null })).toEqual('e')
+ expect(stringifyQuery({ e: null, b: null })).toEqual('e&b')
+ })
+
+ it('stringifies numbers', () => {
+ expect(stringifyQuery({ e: 2 })).toEqual('e=2')
+ expect(stringifyQuery({ e: [2, 'b'] })).toEqual('e=2&e=b')
+ })
+
+ it('ignores undefined values', () => {
+ expect(stringifyQuery({ e: undefined })).toEqual('')
+ expect(stringifyQuery({ e: undefined, b: 'a' })).toEqual('b=a')
+ })
+
it('stringifies arrays', () => {
expect(stringifyQuery({ e: ['b', 'a'] })).toEqual('e=b&e=a')
})
import { decode, encodeQueryProperty } from '../utils/encoding'
// import { encodeQueryProperty, encodeHash } from '../utils/encoding'
-export type HistoryQuery = Record<string, string | string[]>
+type HistoryQueryValue = string | null
+type RawHistoryQueryValue = HistoryQueryValue | number | undefined
+export type HistoryQuery = Record<
+ string,
+ HistoryQueryValue | HistoryQueryValue[]
+>
+export type RawHistoryQuery = Record<
+ string | number,
+ RawHistoryQueryValue | RawHistoryQueryValue[]
+>
interface HistoryLocation {
fullPath: string
export type HistoryLocationNormalized = Pick<HistoryLocation, 'fullPath'>
export interface LocationPartial {
path: string
- query?: HistoryQuery
+ query?: RawHistoryQuery
hash?: string
}
export interface LocationNormalized {
* @param location
*/
export function stringifyURL(
- stringifyQuery: (query: HistoryQuery) => string,
+ stringifyQuery: (query: RawHistoryQuery) => string,
location: LocationPartial
): string {
let query: string = location.query ? stringifyQuery(location.query) : ''
for (let i = 0; i < searchParams.length; ++i) {
let [key, value] = searchParams[i].split('=')
key = decode(key)
- value = decode(value)
+ // avoid decoding null
+ value = value && decode(value)
if (key in query) {
// an extra variable for ts types
let currentValue = query[key]
* Stringify an object query. Works like URLSearchParams. Doesn't prepend a `?`
* @param query
*/
-export function stringifyQuery(query: HistoryQuery): string {
+export function stringifyQuery(query: RawHistoryQuery): string {
let search = ''
for (let key in query) {
- if (search.length > 1) search += '&'
+ if (search.length) search += '&'
const value = query[key]
key = encodeQueryProperty(key)
- if (value === null) {
- // TODO: should we just add the empty string value?
- search += key
+ if (value == null) {
+ // only null adds the value
+ if (value !== undefined) search += key
continue
}
- // const encodedKey = encodeQueryProperty(key)
- let values: string[] = Array.isArray(value) ? value : [value]
- // const encodedValues = values.map(encodeQueryProperty)
+ // keep null values
+ let values: RawHistoryQueryValue[] = Array.isArray(value)
+ ? value.map(v => v && encodeQueryProperty(v))
+ : [value && encodeQueryProperty(value)]
- search += `${key}=${encodeQueryProperty(values[0])}`
- for (let i = 1; i < values.length; i++) {
- search += `&${key}=${encodeQueryProperty(values[i])}`
+ for (let i = 0; i < values.length; i++) {
+ // only append & with i > 0
+ search += (i ? '&' : '') + key
+ if (values[i] != null) search += ('=' + values[i]) as string
}
}
return search
}
+/**
+ * Transforms a RawQuery intoe a NormalizedQuery by casting numbers into
+ * strings, removing keys with an undefined value and replacing undefined with
+ * null in arrays
+ * @param query
+ */
+export function normalizeQuery(query: RawHistoryQuery): HistoryQuery {
+ const normalizedQuery: HistoryQuery = {}
+
+ for (let key in query) {
+ let value = query[key]
+ if (value !== undefined) {
+ normalizedQuery[key] = Array.isArray(value)
+ ? value.map(v => (v == null ? null : '' + v))
+ : value == null
+ ? value
+ : '' + value
+ }
+ }
+
+ return normalizedQuery
+}
+
/**
* Strips off the base from the beginning of a location.pathname
* @param pathname location.pathname
-import { HistoryQuery } from '../history/common'
+import { HistoryQuery, RawHistoryQuery } from '../history/common'
import { PathParserOptions } from '../matcher/path-parser-ranker'
import { markNonReactive } from 'vue'
import { RouteRecordMatched } from '../matcher/types'
export type RouteParams = Record<string, string | string[]>
export interface RouteQueryAndHash {
- query?: HistoryQuery
+ query?: RawHistoryQuery
hash?: string
}
export interface LocationAsPath {
export interface RouteLocationNormalized
extends Required<RouteQueryAndHash & LocationAsRelative & LocationAsPath> {
fullPath: string
- query: HistoryQuery // the normalized version cannot have numbers
+ // the normalized version cannot have numbers or undefined
+ query: HistoryQuery
// TODO: do the same for params
name: string | null | undefined
matched: RouteRecordMatched[] // non-enumerable
const ENC_PIPE_RE = /%7C/g // |
const ENC_CURLY_CLOSE_RE = /%7D/g // }
-function commonEncode(text: string): string {
- return encodeURI(text)
+function commonEncode(text: string | number): string {
+ return encodeURI('' + text)
.replace(ENC_PIPE_RE, '|')
.replace(ENC_BRACKET_OPEN_RE, '[')
.replace(ENC_BRACKET_CLOSE_RE, ']')
.replace(ENC_CARET_RE, '^')
}
-export function encodeQueryProperty(text: string): string {
+export function encodeQueryProperty(text: string | number): string {
return commonEncode(text)
.replace(HASH_RE, '%23')
.replace(AMPERSAND_RE, '%26')