]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: support loose queries with numbers
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 5 Feb 2020 11:36:17 +0000 (12:36 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 5 Feb 2020 13:08:29 +0000 (14:08 +0100)
__tests__/stringifyQuery.spec.ts
src/history/common.ts
src/router.ts
src/types/index.ts
src/utils/encoding.ts

index 8ea5a46acd377aaae30d2f94928af5272b0cf93f..a03341e6ce22f0158775019df60246dc34d0eca4 100644 (file)
@@ -8,6 +8,21 @@ describe('stringifyQuery', () => {
     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')
   })
index c1492ff764c29162750cc8992aa50f11d1129a61..8cc83b43c302baed79fd8f8e6a713ee4170207e2 100644 (file)
@@ -2,7 +2,16 @@ import { ListenerRemover } from '../types'
 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
@@ -13,7 +22,7 @@ export type RawHistoryLocation = HistoryLocation | string
 export type HistoryLocationNormalized = Pick<HistoryLocation, 'fullPath'>
 export interface LocationPartial {
   path: string
-  query?: HistoryQuery
+  query?: RawHistoryQuery
   hash?: string
 }
 export interface LocationNormalized {
@@ -142,7 +151,7 @@ export function parseURL(
  * @param location
  */
 export function stringifyURL(
-  stringifyQuery: (query: HistoryQuery) => string,
+  stringifyQuery: (query: RawHistoryQuery) => string,
   location: LocationPartial
 ): string {
   let query: string = location.query ? stringifyQuery(location.query) : ''
@@ -165,7 +174,8 @@ export function parseQuery(search: string): HistoryQuery {
   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]
@@ -183,30 +193,55 @@ export function parseQuery(search: string): HistoryQuery {
  * 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
index 7fd6b43c4e15979a7e6659eeeb2a2c7d164c1b35..b7f8409f28834289b962f5156a78027a388325e2 100644 (file)
@@ -18,6 +18,7 @@ import {
   parseURL,
   stringifyQuery,
   stringifyURL,
+  normalizeQuery,
 } from './history/common'
 import {
   ScrollToPosition,
@@ -207,7 +208,7 @@ export function createRouter({
         path: matchedRoute.path,
       }),
       hash: location.hash || '',
-      query: location.query || {},
+      query: normalizeQuery(location.query || {}),
       ...matchedRoute,
       redirectedFrom,
     }
index 4e13f8e634dd132e099d06ef50dd896066c01018..ed70d9aa6d4903b3be380db3cdec6342d7738a4c 100644 (file)
@@ -1,4 +1,4 @@
-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'
@@ -20,7 +20,7 @@ export type ListenerRemover = () => void
 export type RouteParams = Record<string, string | string[]>
 
 export interface RouteQueryAndHash {
-  query?: HistoryQuery
+  query?: RawHistoryQuery
   hash?: string
 }
 export interface LocationAsPath {
@@ -52,7 +52,8 @@ export type RouteLocation =
 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
index ba4e10f104c0b6ea4dcd0f7a6449ab39b0401bbe..939b85fe48ff97044e5fac1c0719b26afaf3c514 100644 (file)
@@ -31,8 +31,8 @@ const ENC_CURLY_OPEN_RE = /%7B/g // {
 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, ']')
@@ -45,7 +45,7 @@ export function encodeHash(text: string): string {
     .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')