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

feat: new meta tags API #2958

Merged
merged 5 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,12 @@ export type KnownKeys<T> = {
? U
: never;

export interface ValidationFlags {
untouched: boolean;
export interface FieldMeta {
touched: boolean;
dirty: boolean;
pristine: boolean;
valid: boolean;
invalid: boolean;
passed: boolean;
failed: boolean;
validated: boolean;
pending: boolean;
changed: boolean;
initialValue?: any;
}

export type MaybeReactive<T> = Ref<T> | ComputedRef<T> | T;
Expand All @@ -35,19 +29,6 @@ export type SubmitEvent = Event & { target: HTMLFormElement };

export type GenericValidateFunction = (value: any) => boolean | string | Promise<boolean | string>;

export type Flag =
| 'untouched'
| 'touched'
| 'dirty'
| 'pristine'
| 'valid'
| 'invalid'
| 'passed'
| 'failed'
| 'validated'
| 'pending'
| 'changed';

export interface FormController {
register(field: any): void;
unregister(field: any): void;
Expand Down
95 changes: 28 additions & 67 deletions packages/core/src/useField.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { watch, ref, Ref, isRef, reactive, computed, onMounted, watchEffect, inject, onBeforeUnmount } from 'vue';
import { validate } from './validate';
import {
FormController,
ValidationResult,
MaybeReactive,
GenericValidateFunction,
Flag,
ValidationFlags,
} from './types';
import { validate as validateValue } from './validate';
import { FormController, ValidationResult, MaybeReactive, GenericValidateFunction, FieldMeta } from './types';
import {
normalizeRules,
extractLocators,
Expand All @@ -16,6 +9,7 @@ import {
hasCheckedAttr,
getFromPath,
setInPath,
keysOf,
} from './utils';
import { isCallable } from '../../shared';
import { FormInitialValues, FormSymbol } from './symbols';
Expand Down Expand Up @@ -50,7 +44,7 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie
validateOnValueUpdate,
} = normalizeOptions(name, opts);

const { meta, errors, handleBlur, handleInput, reset, patch, value, checked } = useValidationState({
const { meta, errors, handleBlur, handleInput, reset, setValidationState, value, checked } = useValidationState({
name,
// make sure to unwrap initial value because of possible refs passed in
initValue: unwrap(initialValue),
Expand All @@ -64,32 +58,24 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie
return normalizeRules(nonYupSchemaRules || unwrap(rules));
});

const runValidation = async (): Promise<ValidationResult> => {
const validate = async (): Promise<ValidationResult> => {
meta.pending = true;
let result: ValidationResult;
if (!form || !form.validateSchema) {
const result = await validate(value.value, normalizedRules.value, {
result = await validateValue(value.value, normalizedRules.value, {
name: label,
values: form?.values ?? {},
bails,
});

// Must be updated regardless if a mutation is needed or not
// FIXME: is this needed?
meta.valid = !result.errors.length;
meta.invalid = !!result.errors.length;
meta.pending = false;

return result;
} else {
result = (await form.validateSchema())[name];
}

const results = await form.validateSchema();
meta.pending = false;

return results[name];
return setValidationState(result);
};

const runValidationWithMutation = () => runValidation().then(patch);

// Common input/change event handler
const handleChange = (e: unknown) => {
if (checked && checked.value === (e as any)?.target?.checked) {
Expand All @@ -98,19 +84,14 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie

value.value = normalizeEventValue(e);
meta.dirty = true;
meta.pristine = false;
if (!validateOnValueUpdate) {
return runValidationWithMutation();
return validate();
}
};

onMounted(() => {
runValidation().then(result => {
if (validateOnMount) {
patch(result);
}
});
});
if (validateOnMount) {
onMounted(validate);
}

const errorMessage = computed(() => {
return errors.value[0];
Expand All @@ -128,21 +109,21 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie
checked,
idx: -1,
reset,
validate: runValidationWithMutation,
validate,
handleChange,
handleBlur,
handleInput,
setValidationState: patch,
setValidationState,
};

if (validateOnValueUpdate) {
watch(value, runValidationWithMutation, {
watch(value, validate, {
deep: true,
});
}

if (isRef(rules)) {
watch(rules, runValidationWithMutation, {
watch(rules, validate, {
deep: true,
});
}
Expand Down Expand Up @@ -186,8 +167,8 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie

// For each dependent field, validate it if it was validated before
dependencies.value.forEach(dep => {
if (dep in form.values && meta.validated) {
runValidationWithMutation();
if (dep in form.values && meta.dirty) {
return validate();
}
});
});
Expand Down Expand Up @@ -239,8 +220,8 @@ function useValidationState({
valueProp: any;
}) {
const errors: Ref<string[]> = ref([]);
const { reset: resetFlags, meta } = useMeta();
const initialValue = getFromPath(unwrap(inject(FormInitialValues, {})), name) ?? initValue;
const { reset: resetFlags, meta } = useMeta(initialValue);
const value = useFieldValue(initialValue, name, form);
if (hasCheckedAttr(type) && initialValue) {
value.value = initialValue;
Expand All @@ -265,7 +246,6 @@ function useValidationState({
*/
const handleBlur = () => {
meta.touched = true;
meta.untouched = false;
};

/**
Expand All @@ -279,16 +259,12 @@ function useValidationState({
}

meta.dirty = true;
meta.pristine = false;
};

// Updates the validation state with the validation result
function patch(result: ValidationResult) {
function setValidationState(result: ValidationResult) {
errors.value = result.errors;
meta.changed = initialValue !== value.value;
meta.valid = !result.errors.length;
meta.invalid = !!result.errors.length;
meta.validated = true;

return result;
}
Expand All @@ -302,7 +278,7 @@ function useValidationState({
return {
meta,
errors,
patch,
setValidationState,
reset,
handleBlur,
handleInput,
Expand All @@ -314,39 +290,24 @@ function useValidationState({
/**
* Exposes meta flags state and some associated actions with them.
*/
function useMeta() {
const initialMeta = (): ValidationFlags => ({
untouched: true,
function useMeta(initialValue: any) {
const initialMeta = (): FieldMeta => ({
touched: false,
dirty: false,
pristine: true,
valid: false,
invalid: false,
validated: false,
pending: false,
changed: false,
passed: false,
failed: false,
initialValue,
});

const meta = reactive(initialMeta());
watchEffect(() => {
meta.passed = meta.valid && meta.validated;
meta.failed = meta.invalid && meta.validated;
});

/**
* Resets the flag state
*/
function reset() {
const defaults = initialMeta();
Object.keys(meta).forEach((key: string) => {
// Skip these, since they are computed anyways
if (['passed', 'failed'].includes(key)) {
return;
}

meta[key as Flag] = defaults[key as Flag];
keysOf(meta).forEach(key => {
meta[key] = defaults[key];
});
}

Expand Down
40 changes: 16 additions & 24 deletions packages/core/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, ref, Ref, provide, reactive, onMounted, isRef, watch } from '
import type { ValidationError } from 'yup';
import type { useField } from './useField';
import {
Flag,
FieldMeta,
FormController,
SubmissionHandler,
GenericValidateFunction,
Expand All @@ -11,7 +11,7 @@ import {
MaybeReactive,
} from './types';
import { unwrap } from './utils/refs';
import { getFromPath, isYupValidator, setInPath, unsetPath } from './utils';
import { getFromPath, isYupValidator, keysOf, setInPath, unsetPath } from './utils';
import { FormErrorsSymbol, FormInitialValues, FormSymbol } from './symbols';

interface FormOptions {
Expand Down Expand Up @@ -194,17 +194,14 @@ export function useForm(opts?: FormOptions) {
const errors = computed(() => {
return activeFields.value.reduce((acc: Record<string, string>, field) => {
// Check if its a grouped field (checkbox/radio)
let message: string | undefined;
if (Array.isArray(fieldsById.value[field.name])) {
const group = fieldsById.value[field.name];
const message = unwrap((group.find((f: any) => unwrap(f.checked)) || field).errorMessage);
if (message) {
acc[field.name] = message;
}

return acc;
message = unwrap((group.find((f: any) => unwrap(f.checked)) || field).errorMessage);
} else {
message = unwrap(field.errorMessage);
}

const message = unwrap(field.errorMessage);
if (message) {
acc[field.name] = message;
}
Expand Down Expand Up @@ -255,8 +252,6 @@ export function useForm(opts?: FormOptions) {
}
});

const meta = useFormMeta(fields);

provide(FormSymbol, controller);
provide(FormErrorsSymbol, errors);
const initialValues = computed<Record<string, any>>(() => {
Expand Down Expand Up @@ -294,6 +289,7 @@ export function useForm(opts?: FormOptions) {
}
);

const meta = useFormMeta(fields, initialValues);
// Trigger initial validation
onMounted(() => {
if (opts?.validateOnMount) {
Expand All @@ -318,30 +314,26 @@ export function useForm(opts?: FormOptions) {
};
}

const MERGE_STRATEGIES: Record<Flag, 'every' | 'some'> = {
const MERGE_STRATEGIES: Record<keyof Omit<FieldMeta, 'initialValue'>, 'every' | 'some'> = {
valid: 'every',
invalid: 'some',
dirty: 'some',
pristine: 'every',
touched: 'some',
untouched: 'every',
pending: 'some',
validated: 'every',
changed: 'some',
passed: 'every',
failed: 'some',
};

function useFormMeta(fields: Ref<any[]>) {
const flags: Flag[] = Object.keys(MERGE_STRATEGIES) as Flag[];

function useFormMeta(fields: Ref<any[]>, initialValues: MaybeReactive<Record<string, any>>) {
return computed(() => {
return flags.reduce((acc, flag: Flag) => {
const flags = keysOf(MERGE_STRATEGIES).reduce((acc, flag) => {
const mergeMethod = MERGE_STRATEGIES[flag];
acc[flag] = fields.value[mergeMethod](field => field.meta[flag]);

return acc;
}, {} as Record<string, boolean>);

return {
initialValues: unwrap(initialValues),
...flags,
};
});
}

Expand Down Expand Up @@ -380,7 +372,7 @@ async function validateYupSchema(

result[fieldId] = fieldResult;
const isGroup = Array.isArray(field);
const touched = isGroup ? field.some((f: any) => f.meta.validated) : field.meta.validated;
const touched = isGroup ? field.some((f: any) => f.meta.dirty) : field.meta.dirty;
if (!shouldMutate && !touched) {
return result;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,10 @@ export function unsetPath(object: Record<string, any>, path: string): void {
unset(pathValues[i - 1], keys[i - 1]);
}
}

/**
* A typed version of Object.keys
*/
export function keysOf<TRecord extends Record<string, any>>(record: TRecord): (keyof TRecord)[] {
return Object.keys(record);
}
Loading