Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/2.0 #18

Merged
merged 17 commits into from
Mar 6, 2024
Merged
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"extends": "@antfu",
"ignorePatterns": ["/docs/.vitepress/**/*", "/docs/**/*.svg", "/docs/**/*.css"]
"ignorePatterns": ["/docs/.vitepress/**/*", "/docs/**/*.svg", "/docs/**/*.css"],
"rules": {
"no-mixed-spaces-and-tabs": "off",
"no-tabs": "off"
}
}
41 changes: 0 additions & 41 deletions docs/.vitepress/cache/deps/_metadata.json

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "formango",
"type": "module",
"version": "1.0.3",
"version": "2.0.11",
"packageManager": "[email protected]",
"description": "",
"author": "Wouter Laermans <[email protected]>",
Expand Down
92 changes: 0 additions & 92 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 53 additions & 8 deletions src/lib/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { ComputedRef } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
import { z } from 'zod'
import deepClone from 'deep-clone'
import type { DeepPartial, Field, FieldArray, Form, Path, Register, RegisterArray, Unregister } from '../types'
import { type MaybeRefOrGetter, toValue } from '@vueuse/core'
import type { DeepPartial, Field, FieldArray, Form, MaybePromise, NullableKeys, Path, Register, RegisterArray, Unregister } from '../types'
import { generateId, get, set, unset } from '../utils'

interface UseFormReturnType<TSchema extends z.ZodType> {
Expand All @@ -19,7 +20,7 @@ interface UseFormReturnType<TSchema extends z.ZodType> {

interface UseFormOptions<TSchema extends z.ZodType> {
schema: TSchema
initialState?: z.infer<TSchema>
initialState?: MaybeRefOrGetter<NullableKeys<z.infer<TSchema>>>
}

export function useForm<TSchema extends z.ZodType>(
Expand All @@ -35,14 +36,14 @@ export function useForm<TSchema extends z.ZodType>(
// The errors of the form
const errors = ref<z.ZodFormattedError<TSchema>>({} as z.ZodFormattedError<TSchema>)

let onSubmitCb: UseFormReturnType<TSchema>['onSubmitForm'] | null = null
let onSubmitCb: ((data: z.infer<TSchema>) => MaybePromise<void>) | null = null

const isSubmitting = ref<boolean>(false)
const hasAttemptedToSubmit = ref<boolean>(false)

// The initial state of the form
// This is used to keep track of whether a field has been modified (isDirty)
const initialFormState = ref<DeepPartial<z.infer<TSchema>> | null>(initialState ? deepClone(initialState) : null)
const initialFormState = ref<DeepPartial<z.infer<TSchema>> | null>(initialState ? deepClone(toValue(initialState)) : null)

// Tracks all the registered paths (id, path)
const paths = reactive(new Map<string, string>())
Expand All @@ -57,6 +58,9 @@ export function useForm<TSchema extends z.ZodType>(
const registeredFields = reactive(new Map<string, Field<any, any>>())
const registeredFieldArrays = reactive(new Map<string, FieldArray<any>>())

if (initialState != null)
Object.assign(form, deepClone(toValue(initialState)))

const isDirty = computed<boolean>(() => {
return [
...registeredFields.values(),
Expand All @@ -66,6 +70,15 @@ export function useForm<TSchema extends z.ZodType>(

const isValid = computed<boolean>(() => Object.keys(errors.value).length === 0)

watch(() => initialState, (newInitialState) => {
if (!isDirty.value && newInitialState != null) {
initialFormState.value = deepClone(toValue(newInitialState))
Object.assign(form, deepClone(toValue(newInitialState)))
}
}, {
deep: true,
})

const getIdByPath = (
paths: Map<string, string>,
path: string,
Expand Down Expand Up @@ -119,6 +132,15 @@ export function useForm<TSchema extends z.ZodType>(
}
}

const getChildPaths = (path: string): (Field<any, any> | FieldArray<any>)[] => {
return [
...registeredFields.values(),
...registeredFieldArrays.values(),
].filter((field) => {
return field._path.startsWith(path) && field._path !== path
})
}

const createField = (
id: string,
path: string,
Expand All @@ -128,6 +150,7 @@ export function useForm<TSchema extends z.ZodType>(
'_id': id,
'_path': path,
'isValid': false,
'_isTouched': false,
'isDirty': false,
'isTouched': false,
'isChanged': false,
Expand All @@ -137,7 +160,7 @@ export function useForm<TSchema extends z.ZodType>(
set(form, path, newValue)
},
'onBlur': () => {
field.isTouched = true
field._isTouched = true
},
'onChange': () => {
field.isChanged = true
Expand Down Expand Up @@ -259,6 +282,7 @@ export function useForm<TSchema extends z.ZodType>(
_path: path,
isValid: false,
isDirty: false,
isTouched: false,
modelValue: defaultOrExistingValue,
errors: undefined,
append,
Expand Down Expand Up @@ -288,6 +312,10 @@ export function useForm<TSchema extends z.ZodType>(
return fieldArray
}

const isField = (field: Field<any, any> | FieldArray<any>): field is Field<any, any> => {
return (field as Field<any, any>)._isTouched !== undefined
}

const getFieldWithTrackedDepencies = <TFieldArray extends Field<any, any> | FieldArray<any>>(
field: TFieldArray,
initialValue: unknown,
Expand Down Expand Up @@ -320,6 +348,20 @@ export function useForm<TSchema extends z.ZodType>(
return JSON.stringify(field.modelValue) !== JSON.stringify(initialValue)
}) as unknown as boolean

field.isTouched = computed<boolean>(() => {
const children = getChildPaths(field._path)

const areAnyOfItsChildrenTouched = children.some(child => child.isTouched)

if (areAnyOfItsChildrenTouched)
return true

if (isField(field))
return field._isTouched

return false
}) as unknown as boolean

field.errors = computed<z.ZodFormattedError<TSchema>>(() => {
return get(errors.value, field._path)
}) as unknown as z.ZodFormattedError<TSchema>
Expand Down Expand Up @@ -452,7 +494,7 @@ export function useForm<TSchema extends z.ZodType>(
const id = getIdByPath(paths, path)

if (id === null)
throw new Error(`Could not unregister ${path} because it is not registered`)
throw new Error(`Could not unregister ${path} because it does not exist. This might be because it was never registered or because it was already unregistered.`)

updatePaths(path)
unset(form, path)
Expand All @@ -475,14 +517,17 @@ export function useForm<TSchema extends z.ZodType>(
if (!isValid.value)
return

// We need to keep track of the current form state, because the form might change while submitting
const currentFormState = deepClone(form)

isSubmitting.value = true

if (onSubmitCb == null)
throw new Error('Attempted to submit form but `onSubmitForm` callback is not registered')

await onSubmitCb(schema.parse(form))

initialFormState.value = deepClone(form)
initialFormState.value = deepClone(currentFormState)

isSubmitting.value = false
}
Expand Down Expand Up @@ -546,7 +591,7 @@ export function useForm<TSchema extends z.ZodType>(
setValues,
addErrors,
}),
onSubmitForm: (cb: (data: z.infer<TSchema>) => void) => {
onSubmitForm: (cb: (data: z.infer<TSchema>) => MaybePromise<void>) => {
onSubmitCb = cb
},
}
Expand Down
Loading
Loading