Created
February 19, 2024 01:28
-
-
Save CasperEngl/8222dd6eb3dff7e77ff77edb27e7ef4c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { debounce } from 'lodash-es' | |
| import type { MaybeRef } from 'vue' | |
| import { computed, reactive, unref, watch } from 'vue' | |
| import { useRoute } from 'vue-router' | |
| import { z } from 'zod' | |
| type PrimitiveZodTypes = | |
| | z.ZodString | |
| | z.ZodNumber | |
| | z.ZodDate | |
| | z.ZodArray<PrimitiveZodTypes> | |
| | z.ZodCatch<PrimitiveZodTypes> | |
| | z.ZodDefault<PrimitiveZodTypes> | |
| | z.ZodOptional<PrimitiveZodTypes> | |
| | z.ZodNullable<PrimitiveZodTypes> | |
| | z.ZodEffects< | |
| PrimitiveZodTypes, | |
| PrimitiveZodTypes | string | number, | |
| PrimitiveZodTypes | unknown | |
| > | |
| | z.ZodUnion<[PrimitiveZodTypes, PrimitiveZodTypes, ...PrimitiveZodTypes[]]> | |
| | z.ZodEnum<[string, string, ...string[]]> | |
| | z.ZodEffects<z.ZodOptional<z.ZodDate>, string, Date> | |
| | z.ZodLiteral<string> | |
| export type ZodObjectSchema = z.ZodObject<Record<string, PrimitiveZodTypes>, z.UnknownKeysParam> | |
| export function zodHasArray(schema: PrimitiveZodTypes): boolean { | |
| if (schema instanceof z.ZodArray) { | |
| return true | |
| } | |
| if (schema instanceof z.ZodObject) { | |
| return Object.values(schema.shape).some((value) => zodHasArray(value as PrimitiveZodTypes)) | |
| } | |
| if (schema instanceof z.ZodUnion) { | |
| return schema.options.some(zodHasArray) | |
| } | |
| if (schema instanceof z.ZodEnum) { | |
| return false | |
| } | |
| if (schema instanceof z.ZodTuple) { | |
| return schema.items.some(zodHasArray) | |
| } | |
| if (schema instanceof z.ZodIntersection) { | |
| if ('left' in schema && 'right' in schema) { | |
| return schema.left | |
| ? zodHasArray(schema.left as PrimitiveZodTypes) | |
| : zodHasArray(schema.right as PrimitiveZodTypes) | |
| } | |
| } | |
| if (schema instanceof z.ZodEffects) { | |
| return zodHasArray(schema._def.schema) | |
| } | |
| if (schema && '_def' in schema && typeof schema._def === 'object' && 'innerType' in schema._def) { | |
| return zodHasArray(schema._def.innerType) | |
| } | |
| return false | |
| } | |
| function zodDefaultValue(schema: z.ZodType<unknown, unknown>) { | |
| if (schema instanceof z.ZodDefault) { | |
| return schema._def.defaultValue() | |
| } | |
| if (schema && '_def' in schema && typeof schema._def === 'object' && 'innerType' in schema._def) { | |
| return zodDefaultValue(schema._def.innerType as z.ZodType<unknown, unknown>) | |
| } | |
| return undefined | |
| } | |
| function zodCatchValue(schema: z.ZodType<unknown, unknown>) { | |
| if (schema instanceof z.ZodCatch) { | |
| return schema._def.catchValue({ | |
| error: new z.ZodError([]), | |
| input: undefined, | |
| }) | |
| } | |
| if (schema && '_def' in schema && typeof schema._def === 'object' && 'innerType' in schema._def) { | |
| return zodCatchValue(schema._def.innerType as z.ZodType<unknown, unknown>) | |
| } | |
| return undefined | |
| } | |
| type UseZodParamsOptions<Schema extends ZodObjectSchema> = { | |
| schema: Schema | |
| enabled?: MaybeRef<boolean> | |
| type: 'params' | 'query' | |
| debounceMs?: number | |
| } | |
| // Outside your export function, define a map to hold debounced functions | |
| const debouncedSetters = new Map() | |
| export function useZodParams<Schema extends ZodObjectSchema>(options: UseZodParamsOptions<Schema>) { | |
| const route = useRoute() | |
| const enabled = options.enabled ?? true | |
| const type = options.type ?? 'query' | |
| const debouncedMs = options.debounceMs ?? 300 | |
| const mountedMatched = route.matched | |
| const hasMatchedRoute = computed(() => { | |
| return mountedMatched.some((m) => m.path === route.path) | |
| }) | |
| const proxy = new Proxy(reactive({} as z.infer<Schema>), { | |
| get(target, key) { | |
| if (!unref(enabled)) return null | |
| if (!unref(hasMatchedRoute)) return null | |
| const schemaPath = options.schema.shape[key.toString()] | |
| /** | |
| * Vue Router returns a single value if there's only one array item. | |
| * For consistent parameter handling, convert it to an array if the | |
| * schema contains an array. | |
| */ | |
| const updatedTarget = Object.fromEntries( | |
| Object.entries(target).map(([key, value]) => [ | |
| key, | |
| zodHasArray(schemaPath) && !Array.isArray(value) ? [value] : value, | |
| ]) | |
| ) | |
| return Reflect.get(options.schema.parse(updatedTarget), key) | |
| }, | |
| set(target, key, value) { | |
| if (!unref(enabled)) return false | |
| if (!unref(hasMatchedRoute)) return false | |
| const schemaPath = options.schema.shape[key.toString()] | |
| // Prepare the new value according to the schema | |
| const newValue = zodHasArray(schemaPath) && !Array.isArray(value) ? [value] : value | |
| // Check if there's already a debounced function for this key | |
| if (!debouncedSetters.has(key)) { | |
| const debouncedSetter = debounce((newValue) => { | |
| const url = new URL(window.location.href) | |
| if (Array.isArray(newValue)) { | |
| url.searchParams.delete(key.toString()) | |
| for (const value of newValue) { | |
| url.searchParams.append(key.toString(), value.toString()) | |
| } | |
| } else { | |
| url.searchParams.set(key.toString(), newValue ?? '') | |
| } | |
| window.history.replaceState(null, '', url.toString()) | |
| // Remove the debounced function from the map after it's called | |
| debouncedSetters.delete(key) | |
| }, debouncedMs) // Adjust debounce time as needed | |
| // Store the debounced function in the map | |
| debouncedSetters.set(key, debouncedSetter) | |
| } | |
| // Retrieve the debounced function and call it with the new value | |
| debouncedSetters.get(key)(newValue) | |
| /** | |
| * Use Reflect.set to update the value of the key in the target object. | |
| * This is necessary to ensure that the changes are propagated to all | |
| * references of the object. It updates the target object immediately without debouncing, | |
| * ensuring the proxy object is up-to-date even though the router update is debounced. | |
| */ | |
| return Reflect.set(target, key, newValue) | |
| }, | |
| }) | |
| // Watch for changes in the route and update the proxy accordingly | |
| watch(route, (newRoute) => { | |
| if (!unref(enabled)) return | |
| if (!unref(hasMatchedRoute)) return | |
| const existingKeys = Object.keys(proxy) | |
| const newKeys = Object.keys(newRoute[type]) | |
| // Handle keys present in the proxy but not in the newRoute | |
| for (const key of existingKeys) { | |
| if (!newKeys.includes(key)) { | |
| Reflect.set(proxy, key, undefined) | |
| } | |
| } | |
| // Handle keys present in the newRoute | |
| for (const key of newKeys) { | |
| const schemaPath = options.schema.shape[key] | |
| const value = newRoute[type][key] | |
| const catchValue = zodCatchValue(schemaPath) | |
| const defaultValue = zodDefaultValue(schemaPath) | |
| Reflect.set(proxy, key, value || defaultValue || catchValue) | |
| } | |
| }) | |
| for (const key of Object.keys(options.schema.shape)) { | |
| const schemaPath = options.schema.shape[key] | |
| const value = route[type][key] | |
| const catchValue = zodCatchValue(schemaPath) | |
| const defaultValue = zodDefaultValue(schemaPath) | |
| Reflect.set(proxy, key, value || defaultValue || catchValue) | |
| } | |
| return proxy | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment