-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added useField and useForm hooks
- Loading branch information
Showing
3 changed files
with
273 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { watch, ref, Ref, isRef, reactive, computed, onMounted, toRefs } from 'vue'; | ||
import { validate } from './validate'; | ||
import { Flag, FormController, ValidationResult, MaybeReactive } from './types'; | ||
import { createFlags, normalizeRules, extractLocators } from './utils'; | ||
|
||
interface FieldOptions { | ||
value: Ref<any>; | ||
immediate: boolean; | ||
form?: FormController; | ||
} | ||
|
||
export function useFlags() { | ||
const flags = reactive(createFlags()); | ||
const passed = computed(() => { | ||
return flags.valid && flags.validated; | ||
}); | ||
|
||
const failed = computed(() => { | ||
return flags.invalid && flags.validated; | ||
}); | ||
|
||
const onBlur = () => { | ||
flags.touched = true; | ||
flags.untouched = false; | ||
}; | ||
|
||
const onInput = () => { | ||
flags.dirty = true; | ||
flags.pristine = false; | ||
}; | ||
|
||
return { | ||
...toRefs(flags), | ||
passed, | ||
failed, | ||
onInput, | ||
onBlur | ||
}; | ||
} | ||
|
||
export function useField(fieldName: string, rules: MaybeReactive<string | Record<string, any>>, opts?: FieldOptions) { | ||
const errors: Ref<string[]> = ref([]); | ||
const failedRules: Ref<Record<string, string>> = ref({}); | ||
const { value, form, immediate } = normalizeOptions(opts); | ||
const initialValue = value.value; | ||
const { onBlur, onInput, ...flags } = useFlags(); | ||
|
||
function commitResult(result: ValidationResult) { | ||
errors.value = result.errors; | ||
flags.changed.value = initialValue !== value.value; | ||
flags.valid.value = result.valid; | ||
flags.invalid.value = !result.valid; | ||
flags.validated.value = true; | ||
flags.pending.value = false; | ||
failedRules.value = result.failedRules; | ||
} | ||
|
||
const normalizedRules = computed(() => { | ||
return normalizeRules(isRef(rules) ? rules.value : rules); | ||
}); | ||
|
||
const validateField = async (): Promise<ValidationResult> => { | ||
flags.pending.value = true; | ||
const result = await validate(value.value, normalizedRules.value, { | ||
name: fieldName, | ||
values: form?.values.value ?? {}, | ||
names: form?.names.value ?? {} | ||
}); | ||
|
||
commitResult(result); | ||
|
||
return result; | ||
}; | ||
|
||
watch(value, validateField, { | ||
lazy: true, | ||
deep: true | ||
}); | ||
|
||
if (isRef(rules)) { | ||
watch(rules, validateField, { | ||
lazy: true, | ||
deep: true | ||
}); | ||
} | ||
|
||
const reset = () => { | ||
errors.value = []; | ||
const defaults = createFlags(); | ||
failedRules.value = {}; | ||
Object.keys(flags).forEach((key: string) => { | ||
// Skip these, since they are computed anyways. | ||
if (key === 'passed' || key === 'failed') { | ||
return; | ||
} | ||
|
||
(flags[key as Flag] as Ref<boolean>).value = defaults[key as Flag]; | ||
}); | ||
}; | ||
|
||
onMounted(() => { | ||
validate(initialValue, isRef(rules) ? rules.value : rules).then(result => { | ||
if (immediate) { | ||
commitResult(result); | ||
return; | ||
} | ||
|
||
// Initial silent validation. | ||
flags.valid.value = result.valid; | ||
flags.invalid.value = !result.valid; | ||
}); | ||
}); | ||
|
||
const field = { | ||
vid: fieldName, | ||
value: value, | ||
...flags, | ||
errors, | ||
reset, | ||
validate: validateField, | ||
onInput, | ||
onBlur | ||
}; | ||
|
||
// eslint-disable-next-line no-unused-expressions | ||
form?.register(field); | ||
|
||
if (form) { | ||
const dependencies = computed(() => { | ||
return Object.keys(normalizedRules.value).reduce((acc: string[], rule: string) => { | ||
const deps = extractLocators(normalizedRules.value[rule]).map((dep: any) => dep.__locatorRef); | ||
acc.push(...deps); | ||
|
||
return acc; | ||
}, []); | ||
}); | ||
|
||
watch(dependencies, val => { | ||
val.forEach(dep => { | ||
watch(form.fields[dep].value, () => { | ||
if (flags.validated.value) { | ||
validateField(); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
return field; | ||
} | ||
|
||
function normalizeOptions(opts: FieldOptions | undefined): FieldOptions { | ||
const defaults = () => ({ | ||
value: ref(null), | ||
immediate: false, | ||
rules: '' | ||
}); | ||
|
||
if (!opts) { | ||
return defaults(); | ||
} | ||
|
||
return { | ||
...defaults(), | ||
...(opts ?? {}) | ||
}; | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { computed, ref, Ref } from 'vue'; | ||
import { Flag, FormController } from './types'; | ||
|
||
const mergeStrategies: Record<Flag, 'every' | 'some'> = { | ||
valid: 'every', | ||
invalid: 'some', | ||
dirty: 'some', | ||
pristine: 'every', | ||
touched: 'some', | ||
untouched: 'every', | ||
pending: 'some', | ||
validated: 'every', | ||
changed: 'some', | ||
passed: 'every', | ||
failed: 'some', | ||
required: 'some' | ||
}; | ||
|
||
function computeFlags(fields: Ref<any[]>) { | ||
const flags: Flag[] = Object.keys(mergeStrategies) as Flag[]; | ||
|
||
return flags.reduce((acc, flag: Flag) => { | ||
acc[flag] = computed(() => { | ||
return fields.value[mergeStrategies[flag]](field => field[flag]); | ||
}); | ||
|
||
return acc; | ||
}, {} as Record<Flag, Ref<boolean>>); | ||
} | ||
|
||
interface FormComposite { | ||
form: FormController; | ||
errors: Ref<Record<string, string[]>>; | ||
reset: () => void; | ||
handleSubmit: (fn: Function) => Promise<any>; | ||
validate: () => Promise<boolean>; | ||
} | ||
|
||
export function useForm(): FormComposite { | ||
const fields: Ref<any[]> = ref([]); | ||
const fieldsById: Record<string, any> = {}; | ||
const values = computed(() => { | ||
return fields.value.reduce((acc: any, field: any) => { | ||
acc[field.vid] = field.value; | ||
|
||
return acc; | ||
}, {}); | ||
}); | ||
|
||
const names = computed(() => { | ||
return fields.value.reduce((acc: any, field: any) => { | ||
acc[field.vid] = field.vid; | ||
|
||
return acc; | ||
}, {}); | ||
}); | ||
|
||
const controller: FormController = { | ||
register(field) { | ||
fields.value.push(field); | ||
fieldsById[field.vid] = field; | ||
}, | ||
fields: fieldsById, | ||
values, | ||
names | ||
}; | ||
|
||
const validate = async () => { | ||
const results = await Promise.all( | ||
fields.value.map((f: any) => { | ||
return f.validate(); | ||
}) | ||
); | ||
|
||
return results.every(r => r.valid); | ||
}; | ||
|
||
const errors = computed(() => { | ||
return fields.value.reduce((acc: Record<string, string[]>, field) => { | ||
acc[field.vid] = field.errors.value; | ||
|
||
return acc; | ||
}, {}); | ||
}); | ||
|
||
const reset = () => { | ||
fields.value.forEach((f: any) => f.reset()); | ||
}; | ||
|
||
return { | ||
errors, | ||
...computeFlags(fields), | ||
form: controller, | ||
validate, | ||
reset, | ||
handleSubmit: (fn: Function) => { | ||
return validate().then(result => { | ||
if (result && typeof fn === 'function') { | ||
return fn(); | ||
} | ||
}); | ||
} | ||
}; | ||
} |