vue-context-storage 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.d.ts +411 -14
  2. package/dist/index.js +84 -71
  3. package/package.json +73 -83
  4. package/dist/collection.d.cts +0 -22
  5. package/dist/collection.d.ts +0 -22
  6. package/dist/handlers/query/helpers.d.cts +0 -45
  7. package/dist/handlers/query/helpers.d.ts +0 -45
  8. package/dist/handlers/query/index.d.cts +0 -31
  9. package/dist/handlers/query/index.d.ts +0 -31
  10. package/dist/handlers/query/transform-helpers.d.cts +0 -123
  11. package/dist/handlers/query/transform-helpers.d.ts +0 -123
  12. package/dist/handlers/query/types.d.cts +0 -103
  13. package/dist/handlers/query/types.d.ts +0 -103
  14. package/dist/handlers.d.cts +0 -15
  15. package/dist/handlers.d.ts +0 -15
  16. package/dist/index.cjs +0 -571
  17. package/dist/index.cjs.map +0 -1
  18. package/dist/index.d.cts +0 -14
  19. package/dist/index.js.map +0 -1
  20. package/dist/injectionSymbols.d.cts +0 -7
  21. package/dist/injectionSymbols.d.ts +0 -7
  22. package/dist/symbols.d.cts +0 -4
  23. package/dist/symbols.d.ts +0 -4
  24. package/src/collection.ts +0 -71
  25. package/src/components/ContextStorage.vue +0 -23
  26. package/src/components/ContextStorageActivator.vue +0 -20
  27. package/src/components/ContextStorageCollection.vue +0 -92
  28. package/src/components/ContextStorageProvider.vue +0 -38
  29. package/src/components.ts +0 -5
  30. package/src/handlers/query/helpers.ts +0 -134
  31. package/src/handlers/query/index.ts +0 -355
  32. package/src/handlers/query/transform-helpers.ts +0 -309
  33. package/src/handlers/query/types.ts +0 -125
  34. package/src/handlers.ts +0 -18
  35. package/src/index.ts +0 -44
  36. package/src/injectionSymbols.ts +0 -15
  37. package/src/plugin.ts +0 -16
  38. package/src/shims-vue.d.ts +0 -5
  39. package/src/symbols.ts +0 -4
@@ -1,355 +0,0 @@
1
- import { ContextStorageHandlerConstructor } from '../../handlers.ts'
2
- import { deserializeParams, serializeParams } from './helpers.ts'
3
- import { contextStorageQueryHandler } from '../../symbols.ts'
4
- import { cloneDeep, isEqual, merge, pick } from 'lodash'
5
- import { getCurrentInstance, inject, MaybeRefOrGetter, onBeforeUnmount, toValue, watch } from 'vue'
6
- import { LocationQuery, useRoute, useRouter } from 'vue-router'
7
- import {
8
- ContextStorageQueryRegisteredItem,
9
- IContextStorageQueryHandler,
10
- QueryHandlerBaseOptions,
11
- RegisterQueryHandlerBaseOptions,
12
- RegisterQueryHandlerOptions,
13
- } from './types.ts'
14
-
15
- export function useContextStorageQueryHandler<T extends Record<string, unknown>>(
16
- data: MaybeRefOrGetter<T>,
17
- options?: RegisterQueryHandlerBaseOptions<T>,
18
- ): void {
19
- const handler = inject<InstanceType<typeof ContextStorageQueryHandler>>(
20
- contextStorageQueryHandler,
21
- )
22
-
23
- if (!handler) {
24
- throw new Error('[ContextStorage] ContextStorageQueryHandler is not provided')
25
- }
26
-
27
- const currentInstance = getCurrentInstance()
28
- const uid = currentInstance?.uid || 0
29
-
30
- const causer = new Error().stack?.split('\n')[2]?.trimStart() || 'unknown'
31
-
32
- const stop = handler.register(data, { causer, uid, ...options })
33
- onBeforeUnmount(() => {
34
- stop()
35
- })
36
- }
37
-
38
- function sortQueryByReference(query: LocationQuery, ...references: LocationQuery[]): LocationQuery {
39
- const sorted: LocationQuery = {}
40
-
41
- const referenceKeys = new Set<string>()
42
-
43
- references.forEach((reference) => {
44
- Object.keys(reference).forEach((key) => {
45
- referenceKeys.add(key)
46
- })
47
- })
48
-
49
- referenceKeys.forEach((key) => {
50
- if (key in query && !(key in sorted)) {
51
- sorted[key] = query[key]
52
- }
53
- })
54
-
55
- Object.keys(query).forEach((key) => {
56
- if (!(key in sorted)) {
57
- sorted[key] = query[key]
58
- }
59
- })
60
-
61
- return sorted
62
- }
63
-
64
- export class ContextStorageQueryHandler implements IContextStorageQueryHandler {
65
- private enabled = false
66
- private registered: ContextStorageQueryRegisteredItem<any>[] = []
67
- private currentQuery: LocationQuery | undefined = undefined
68
- private readonly route: ReturnType<typeof useRoute>
69
- private router: ReturnType<typeof useRouter>
70
- private initialState?: Record<string, unknown>
71
- private hasAnyRegistered = false
72
- private preventSyncRegisteredToQueryByAfterEachRoute = false
73
- private preventAfterEachRouteCallsWhileCallingRouter = false
74
-
75
- static customQueryHandlerOptions: QueryHandlerBaseOptions = {}
76
-
77
- private readonly options: Required<QueryHandlerBaseOptions> = {
78
- mode: 'replace',
79
- emptyPlaceholder: '_',
80
- mergeOnlyExistingKeysWithoutTransform: true,
81
- preserveUnusedKeys: false,
82
- preserveEmptyState: false,
83
- }
84
-
85
- // noinspection JSUnusedGlobalSymbols
86
- static configure(options: QueryHandlerBaseOptions): ContextStorageHandlerConstructor {
87
- ContextStorageQueryHandler.customQueryHandlerOptions = options
88
-
89
- return ContextStorageQueryHandler
90
- }
91
-
92
- constructor() {
93
- this.route = useRoute()
94
- this.router = useRouter()
95
-
96
- this.options = {
97
- ...this.options,
98
- ...ContextStorageQueryHandler.customQueryHandlerOptions,
99
- }
100
-
101
- const stopAfterEach = this.router.afterEach(() => {
102
- this.afterEachRoute()
103
- })
104
-
105
- onBeforeUnmount(() => {
106
- stopAfterEach()
107
- })
108
- }
109
-
110
- getInjectionKey(): typeof contextStorageQueryHandler {
111
- return contextStorageQueryHandler
112
- }
113
-
114
- setInitialState(state: Record<string, unknown> | undefined): void {
115
- this.initialState = state
116
- }
117
-
118
- static getInitialStateResolver(): () => LocationQuery {
119
- const route = useRoute()
120
-
121
- return () => route.query
122
- }
123
-
124
- setEnabled(state: boolean, initial: boolean): void {
125
- const prevState = this.enabled
126
- this.enabled = state
127
-
128
- if (this.hasAnyRegistered) {
129
- if (initial) {
130
- this.syncInitialStateToRegistered()
131
- }
132
-
133
- if ((state && !prevState) || !initial) {
134
- this.syncRegisteredToQuery()
135
- }
136
- }
137
- }
138
-
139
- async syncRegisteredToQuery(): Promise<void> {
140
- if (!this.enabled) {
141
- return
142
- }
143
-
144
- if (this.preventSyncRegisteredToQueryByAfterEachRoute) {
145
- return
146
- }
147
-
148
- const { newQuery, newQueryRaw } = this.#buildQueryFromRegistered()
149
-
150
- this.currentQuery = newQueryRaw
151
-
152
- if (isEqual(newQuery, this.route.query)) {
153
- return
154
- }
155
-
156
- this.preventAfterEachRouteCallsWhileCallingRouter = true
157
- try {
158
- if (this.options.mode === 'replace') {
159
- await this.router.replace({ ...this.route, query: newQuery })
160
- } else {
161
- await this.router.push({ ...this.route, query: newQuery })
162
- }
163
- } catch (e) {
164
- console.error('[ContextStorage] Got error while routing', e)
165
- }
166
- this.preventAfterEachRouteCallsWhileCallingRouter = false
167
- }
168
-
169
- afterEachRoute(): void {
170
- if (!this.enabled) {
171
- return
172
- }
173
-
174
- if (this.preventAfterEachRouteCallsWhileCallingRouter) {
175
- return
176
- }
177
-
178
- this.setInitialState(this.route.query)
179
-
180
- this.preventSyncRegisteredToQueryByAfterEachRoute = true
181
- queueMicrotask(() => {
182
- this.preventSyncRegisteredToQueryByAfterEachRoute = false
183
-
184
- this.syncInitialStateToRegistered()
185
- this.syncRegisteredToQuery()
186
- })
187
-
188
- setTimeout(() => {
189
- this.syncInitialStateToRegistered()
190
- this.syncRegisteredToQuery()
191
- })
192
- }
193
-
194
- syncInitialStateToRegisteredItem<T extends Record<string, unknown>>(
195
- item: ContextStorageQueryRegisteredItem<T>,
196
- ): void {
197
- if (this.initialState === undefined) {
198
- return
199
- }
200
-
201
- let deserialized = deserializeParams(this.initialState)
202
-
203
- const {
204
- prefix,
205
- mergeOnlyExistingKeysWithoutTransform = this.options.mergeOnlyExistingKeysWithoutTransform,
206
- } = item.options || {}
207
-
208
- if (typeof prefix === 'string' && prefix.length > 0) {
209
- deserialized = deserialized[prefix]
210
- }
211
-
212
- if (deserialized === undefined) {
213
- return
214
- }
215
-
216
- const itemData = toValue(item.data)
217
-
218
- /**
219
- * null can be if query parameter only has a name without a value sign
220
- */
221
- if (deserialized !== null) {
222
- const deserializedKeys = Object.keys(deserialized)
223
-
224
- /**
225
- * If the data is empty, return the initial value.
226
- *
227
- * This can happen when directly navigating to a route, for example through a menu item.
228
- */
229
- if (!deserializedKeys.length) {
230
- merge(itemData, item.initialData)
231
- return
232
- }
233
-
234
- if (deserializedKeys.length === 1 && deserialized[this.options.emptyPlaceholder] === null) {
235
- delete deserialized[this.options.emptyPlaceholder]
236
- }
237
- }
238
-
239
- if (item.options?.transform) {
240
- deserialized = item.options.transform(deserialized, item.initialData)
241
- } else {
242
- if (mergeOnlyExistingKeysWithoutTransform) {
243
- deserialized = pick(deserialized, Object.keys(item.initialData))
244
- }
245
- }
246
-
247
- if (isEqual(itemData, deserialized)) {
248
- return
249
- }
250
-
251
- merge(itemData, deserialized)
252
- }
253
-
254
- syncInitialStateToRegistered(): void {
255
- this.registered.forEach((item) => this.syncInitialStateToRegisteredItem(item))
256
- }
257
-
258
- register<T extends Record<string, unknown>>(
259
- data: MaybeRefOrGetter<T>,
260
- options: RegisterQueryHandlerOptions<T>,
261
- ): () => void {
262
- this.hasAnyRegistered = true
263
-
264
- const watchHandle = watch(data, () => this.syncRegisteredToQuery(), {
265
- deep: true,
266
- })
267
-
268
- const item: ContextStorageQueryRegisteredItem<T> = {
269
- data,
270
- initialData: cloneDeep(toValue(data)) as T,
271
- options,
272
- watchHandle,
273
- }
274
- this.registered.push(item)
275
-
276
- const syncCallback = (): void => {
277
- this.syncInitialStateToRegisteredItem(item)
278
- this.syncRegisteredToQuery()
279
- }
280
-
281
- if (this.preventAfterEachRouteCallsWhileCallingRouter) {
282
- /**
283
- * Macrotask solves syncing issues when syncRegisteredToQuery called after HMR
284
- */
285
- setTimeout(syncCallback)
286
- } else {
287
- queueMicrotask(syncCallback)
288
- }
289
-
290
- return (): void => {
291
- this.registered.splice(this.registered.indexOf(item), 1)
292
- this.syncRegisteredToQuery()
293
- }
294
- }
295
-
296
- #buildQueryFromRegistered(): { newQuery: LocationQuery; newQueryRaw: LocationQuery } {
297
- const newQueryRaw: LocationQuery = {}
298
-
299
- this.registered.forEach((item) => {
300
- const { prefix, preserveEmptyState = this.options.preserveEmptyState } = item.options || {}
301
- const patch = serializeParams(toValue(item.data), {
302
- prefix,
303
- })
304
-
305
- const patchKeys = Object.keys(patch)
306
-
307
- // If there are key intersections between the query and the patch, a warning is issued.
308
- // Patches should not overwrite each other, otherwise, upon reload, an incorrect value will be restored.
309
- patchKeys.forEach((key) => {
310
- if (newQueryRaw.hasOwnProperty(key)) {
311
- console.warn(
312
- `[ContextStorage] Key ${key} is already present, overriding ` +
313
- (item.options?.causer || ''),
314
- )
315
- }
316
- })
317
-
318
- if (!patchKeys.length && preserveEmptyState) {
319
- patch[prefix || this.options.emptyPlaceholder] = null
320
- }
321
-
322
- Object.assign(newQueryRaw, patch)
323
- })
324
-
325
- let newQuery = { ...newQueryRaw }
326
-
327
- /*
328
- * It will not delete from the query the keys that are not used in the patch.
329
- *
330
- * It will only work if the registered item has a transform, otherwise without
331
- * it - all keys are dumped into item.data during the initial fill from initialState
332
- */
333
- if (this.options.preserveUnusedKeys) {
334
- newQuery = { ...this.route.query, ...newQuery }
335
- }
336
-
337
- if (this.currentQuery !== undefined) {
338
- //Perform a diff of keys between currentQuery and newQueryRaw, and remove the keys that are in currentQuery but not in newQueryRaw.
339
- //This is necessary to ensure that the query string does not contain keys that are no longer used.
340
- Object.keys(this.currentQuery).forEach((key) => {
341
- if (!newQueryRaw.hasOwnProperty(key)) {
342
- delete newQuery[key]
343
- }
344
- })
345
- }
346
-
347
- if (Object.keys(newQuery).length > 1 && newQuery[this.options.emptyPlaceholder] === null) {
348
- delete newQuery[this.options.emptyPlaceholder]
349
- }
350
-
351
- newQuery = sortQueryByReference(newQuery, newQueryRaw)
352
-
353
- return { newQuery, newQueryRaw }
354
- }
355
- }
@@ -1,309 +0,0 @@
1
- import { QueryValue } from './types.ts'
2
-
3
- interface AsNumberOptions {
4
- nullable?: boolean
5
- missable?: boolean
6
- fallbackValue?: number
7
- }
8
-
9
- export function asNumber(value: QueryValue | number | undefined): number
10
- export function asNumber(
11
- value: QueryValue | number | undefined,
12
- options: { nullable: true; missable: true; fallbackValue?: number },
13
- ): number | null | undefined
14
- export function asNumber(
15
- value: QueryValue | number | undefined,
16
- options: { nullable: true; missable?: false; fallbackValue?: number },
17
- ): number | null
18
- export function asNumber(
19
- value: QueryValue | number | undefined,
20
- options: { nullable?: false; missable: true; fallbackValue?: number },
21
- ): number | undefined
22
- export function asNumber(
23
- value: QueryValue | number | undefined,
24
- options: { nullable?: false; missable?: false; fallbackValue?: number },
25
- ): number
26
- export function asNumber(
27
- value: QueryValue | number | undefined,
28
- options?: AsNumberOptions,
29
- ): number | null | undefined {
30
- const {
31
- nullable = false,
32
- missable = false,
33
- fallbackValue = missable ? undefined : nullable ? null : 0,
34
- } = options || {}
35
-
36
- if (value === null && nullable) {
37
- return null
38
- }
39
-
40
- if (value === undefined && missable) {
41
- return undefined
42
- }
43
-
44
- value = Number(value)
45
-
46
- return isNaN(value) ? fallbackValue : value
47
- }
48
-
49
- interface AsStringOptions<T extends readonly string[] = string[]> {
50
- nullable?: boolean
51
- missable?: boolean
52
- fallbackValue?: T extends readonly string[] ? T[number] : string
53
- allowedValues?: T
54
- }
55
-
56
- export function asString(value: QueryValue | undefined): string
57
- export function asString<T extends readonly string[]>(
58
- value: QueryValue | undefined,
59
- options: {
60
- nullable: true
61
- missable: true
62
- fallbackValue?: T[number]
63
- allowedValues: T
64
- },
65
- ): T[number] | null | undefined
66
- export function asString<T extends readonly string[]>(
67
- value: QueryValue | undefined,
68
- options: {
69
- nullable: true
70
- missable?: false
71
- fallbackValue?: T[number]
72
- allowedValues: T
73
- },
74
- ): T[number] | null
75
- export function asString<T extends readonly string[]>(
76
- value: QueryValue | undefined,
77
- options: {
78
- nullable?: false
79
- missable: true
80
- fallbackValue?: T[number]
81
- allowedValues: T
82
- },
83
- ): T[number] | undefined
84
- export function asString<T extends readonly string[]>(
85
- value: QueryValue | undefined,
86
- options: {
87
- nullable?: false
88
- missable?: false
89
- fallbackValue?: T[number]
90
- allowedValues: T
91
- },
92
- ): T[number]
93
- export function asString(
94
- value: QueryValue | undefined,
95
- options: { nullable: true; missable: true; fallbackValue?: string },
96
- ): string | null | undefined
97
- export function asString(
98
- value: QueryValue | undefined,
99
- options: { nullable: true; missable?: false; fallbackValue?: string },
100
- ): string | null
101
- export function asString(
102
- value: QueryValue | undefined,
103
- options: { nullable?: false; missable: true; fallbackValue?: string },
104
- ): string | undefined
105
- export function asString(
106
- value: QueryValue | undefined,
107
- options: { nullable?: false; missable?: false; fallbackValue?: string },
108
- ): string
109
- export function asString(
110
- value: QueryValue | undefined,
111
- options?: AsStringOptions,
112
- ): QueryValue | undefined {
113
- const {
114
- nullable = false,
115
- missable = false,
116
- fallbackValue = missable ? undefined : nullable ? null : '',
117
- allowedValues,
118
- } = options || {}
119
-
120
- if (value === null && nullable) {
121
- return null
122
- }
123
-
124
- if (value === undefined && missable) {
125
- return undefined
126
- }
127
-
128
- const stringValue = value ?? fallbackValue
129
-
130
- if (allowedValues && typeof stringValue === 'string' && !allowedValues.includes(stringValue)) {
131
- return fallbackValue
132
- }
133
-
134
- return stringValue
135
- }
136
-
137
- interface AsNumberArrayOptions {
138
- nullable?: boolean
139
- }
140
-
141
- export function asNumberArray(value: QueryValue | undefined): number[]
142
- export function asNumberArray(
143
- value: QueryValue | undefined,
144
- options: { nullable: true },
145
- ): number[] | null
146
- export function asNumberArray(
147
- value: QueryValue | undefined,
148
- options: { nullable?: false },
149
- ): number[]
150
- export function asNumberArray(
151
- value: QueryValue | undefined,
152
- options?: AsNumberArrayOptions,
153
- ): number[] | null {
154
- const { nullable = false } = options || {}
155
-
156
- if (value === null && nullable) {
157
- return null
158
- }
159
-
160
- if (value === undefined) {
161
- return nullable ? null : []
162
- }
163
-
164
- let arrayValue: (string | null)[]
165
-
166
- if (Array.isArray(value)) {
167
- arrayValue = value
168
- } else if (typeof value === 'string') {
169
- arrayValue = [value]
170
- } else {
171
- arrayValue = []
172
- }
173
-
174
- return arrayValue.map((item) => {
175
- if (item === null) {
176
- return 0
177
- }
178
- const num = Number(item)
179
- return isNaN(num) ? 0 : num
180
- })
181
- }
182
-
183
- interface AsArrayOptions<T> {
184
- nullable?: boolean
185
- missable?: boolean
186
- transform?: (value: QueryValue) => T
187
- }
188
-
189
- export function asArray<T>(value: QueryValue | undefined): T[]
190
- export function asArray<T>(
191
- value: QueryValue | undefined,
192
- options: { nullable: true; missable: true; transform?: (value: QueryValue) => T },
193
- ): T[] | null | undefined
194
- export function asArray<T>(
195
- value: QueryValue | undefined,
196
- options: { nullable: true; missable?: false; transform?: (value: QueryValue) => T },
197
- ): T[] | null
198
- export function asArray<T>(
199
- value: QueryValue | undefined,
200
- options: { nullable?: false; missable: true; transform?: (value: QueryValue) => T },
201
- ): T[] | undefined
202
- export function asArray<T>(
203
- value: QueryValue | undefined,
204
- options: { nullable?: false; missable?: false; transform?: (value: QueryValue) => T },
205
- ): T[]
206
- export function asArray<T>(
207
- value: QueryValue | undefined,
208
- options?: AsArrayOptions<T>,
209
- ): T[] | null | undefined {
210
- const { nullable = false, missable = false, transform } = options || {}
211
-
212
- if (value === null && nullable) {
213
- return null
214
- }
215
-
216
- if (value === undefined && missable) {
217
- return undefined
218
- }
219
-
220
- if (value === undefined) {
221
- return nullable ? null : []
222
- }
223
-
224
- let arrayValue: QueryValue[]
225
-
226
- if (Array.isArray(value)) {
227
- arrayValue = value
228
- } else {
229
- arrayValue = [value]
230
- }
231
-
232
- if (transform) {
233
- return arrayValue.map((item) => transform(item))
234
- }
235
-
236
- return arrayValue as T[]
237
- }
238
-
239
- interface AsBooleanOptions {
240
- nullable?: boolean
241
- missable?: boolean
242
- fallbackValue?: boolean
243
- }
244
-
245
- export function asBoolean(value: QueryValue | undefined): boolean
246
- export function asBoolean(
247
- value: QueryValue | undefined,
248
- options: { nullable: true; missable: true; fallbackValue?: boolean },
249
- ): boolean | null | undefined
250
- export function asBoolean(
251
- value: QueryValue | undefined,
252
- options: { nullable: true; missable?: false; fallbackValue?: boolean },
253
- ): boolean | null
254
- export function asBoolean(
255
- value: QueryValue | undefined,
256
- options: { nullable?: false; missable: true; fallbackValue?: boolean },
257
- ): boolean | undefined
258
- export function asBoolean(
259
- value: QueryValue | undefined,
260
- options: { nullable?: false; missable?: false; fallbackValue?: boolean },
261
- ): boolean
262
- export function asBoolean(
263
- value: QueryValue | undefined,
264
- options?: AsBooleanOptions,
265
- ): boolean | null | undefined {
266
- const {
267
- nullable = false,
268
- missable = false,
269
- fallbackValue = missable ? undefined : nullable ? null : false,
270
- } = options || {}
271
-
272
- if (value === null && nullable) {
273
- return null
274
- }
275
-
276
- if (value === undefined && missable) {
277
- return undefined
278
- }
279
-
280
- if (value === undefined || value === null) {
281
- return fallbackValue
282
- }
283
-
284
- if (typeof value === 'string') {
285
- const lowerValue = value.toLowerCase()
286
- if (lowerValue === 'true' || lowerValue === '1') {
287
- return true
288
- }
289
- if (lowerValue === 'false' || lowerValue === '0') {
290
- return false
291
- }
292
- }
293
-
294
- return fallbackValue
295
- }
296
-
297
- export const transform: {
298
- asString: typeof asString
299
- asNumber: typeof asNumber
300
- asArray: typeof asArray
301
- asNumberArray: typeof asNumberArray
302
- asBoolean: typeof asBoolean
303
- } = {
304
- asString,
305
- asNumber,
306
- asArray,
307
- asNumberArray,
308
- asBoolean,
309
- }