Skip to content

Commit

Permalink
feat: nested objects/arrays (#2897)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Sep 16, 2020
1 parent 21dda88 commit 8d161a1
Show file tree
Hide file tree
Showing 12 changed files with 525 additions and 73 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/Form.ts
Expand Up @@ -38,7 +38,7 @@ export const Form = defineComponent({
const children = normalizeChildren(ctx, {
meta: meta.value,
errors: errors.value,
values: values.value,
values: values,
isSubmitting: isSubmitting.value,
validate,
handleSubmit,
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/types.ts
Expand Up @@ -29,6 +29,14 @@ export interface ValidationFlags {
changed: boolean;
}

export type MaybeReactive<T> = Ref<T> | ComputedRef<T> | T;

export type SubmitEvent = Event & { target: HTMLFormElement };

export type SubmissionHandler = (values: Record<string, any>, evt?: SubmitEvent) => any;

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

export type Flag =
| 'untouched'
| 'touched'
Expand All @@ -51,11 +59,3 @@ export interface FormController {
validateSchema?: (shouldMutate?: boolean) => Promise<Record<string, ValidationResult>>;
setFieldValue: (path: string, value: any) => void;
}

export type MaybeReactive<T> = Ref<T> | ComputedRef<T> | T;

export type SubmitEvent = Event & { target: HTMLFormElement };

export type SubmissionHandler = (values: Record<string, any>, evt?: SubmitEvent) => any;

export type GenericValidateFunction = (value: any) => boolean | string | Promise<boolean | string>;
67 changes: 47 additions & 20 deletions packages/core/src/useField.ts
Expand Up @@ -8,7 +8,15 @@ import {
Flag,
ValidationFlags,
} from './types';
import { normalizeRules, extractLocators, normalizeEventValue, unwrap, genFieldErrorId, hasCheckedAttr } from './utils';
import {
normalizeRules,
extractLocators,
normalizeEventValue,
unwrap,
genFieldErrorId,
hasCheckedAttr,
getFromPath,
} from './utils';
import { isCallable } from '../../shared';

interface FieldOptions {
Expand All @@ -28,7 +36,13 @@ type RuleExpression = MaybeReactive<string | Record<string, any> | GenericValida
*/
export function useField(fieldName: MaybeReactive<string>, rules: RuleExpression, opts?: Partial<FieldOptions>) {
const { initialValue, form, immediate, bails, disabled, type, valueProp } = normalizeOptions(opts);
const { meta, errors, onBlur, handleChange, reset, patch, value } = useValidationState(fieldName, initialValue, form);
const { meta, errors, onBlur, handleChange, reset, patch, value, checked } = useValidationState({
fieldName,
initValue: initialValue,
form,
type,
valueProp,
});

const nonYupSchemaRules = extractRuleFromSchema(form?.schema, unwrap(fieldName));
const normalizedRules = computed(() => {
Expand Down Expand Up @@ -75,16 +89,6 @@ export function useField(fieldName: MaybeReactive<string>, rules: RuleExpression

const aria = useAriAttrs(fieldName, meta);

const checked = hasCheckedAttr(type)
? computed(() => {
if (Array.isArray(value.value)) {
return value.value.includes(unwrap(valueProp));
}

return unwrap(valueProp) === value.value;
})
: undefined;

const field = {
name: fieldName,
value: value,
Expand Down Expand Up @@ -190,12 +194,39 @@ function normalizeOptions(opts: Partial<FieldOptions> | undefined): FieldOptions
/**
* Manages the validation state of a field.
*/
function useValidationState(fieldName: MaybeReactive<string>, initValue: any, form?: FormController) {
function useValidationState({
fieldName,
initValue,
form,
type,
valueProp,
}: {
fieldName: MaybeReactive<string>;
initValue?: any;
form?: FormController;
type?: string;
valueProp: any;
}) {
const errors: Ref<string[]> = ref([]);
const { onBlur, reset: resetFlags, meta } = useMeta();
const initialValue = initValue;
const initialValue = initValue ?? getFromPath(inject('$_veeFormInitValues', {}), unwrap(fieldName));
const value = useFieldValue(initialValue, fieldName, form);

const checked = hasCheckedAttr(type)
? computed(() => {
if (Array.isArray(value.value)) {
return value.value.includes(unwrap(valueProp));
}

return unwrap(valueProp) === value.value;
})
: undefined;

if (checked === undefined || checked.value) {
// Set the value without triggering the watcher
value.value = initialValue;
}

// Common input/change event handler
const handleChange = (e: Event) => {
value.value = normalizeEventValue(e);
Expand Down Expand Up @@ -228,6 +259,7 @@ function useValidationState(fieldName: MaybeReactive<string>, initValue: any, fo
onBlur,
handleChange,
value,
checked,
};
}

Expand Down Expand Up @@ -319,17 +351,12 @@ function useFieldValue(initialValue: any, path: MaybeReactive<string>, form?: Fo
// otherwise use a computed setter that triggers the `setFieldValue`
const value = computed({
get() {
return form.values[unwrap(path)];
return getFromPath(form.values, unwrap(path));
},
set(newVal) {
form.setFieldValue(unwrap(path), newVal);
},
});

// Set the value without triggering the setter
if (initialValue !== undefined) {
value.value = initialValue;
}

return value;
}
64 changes: 31 additions & 33 deletions packages/core/src/useForm.ts
Expand Up @@ -10,7 +10,7 @@ import {
ValidationResult,
} from './types';
import { unwrap } from './utils/refs';
import { isYupValidator } from './utils';
import { getFromPath, isYupValidator, setInPath, unsetPath } from './utils';

interface FormOptions {
validationSchema?: Record<string, GenericValidateFunction | string | Record<string, any>>;
Expand Down Expand Up @@ -51,24 +51,9 @@ export function useForm(opts?: FormOptions) {
});

// a private ref for all form values
const _values = reactive<Record<string, any>>({});
// public ref for all active form values
const values = computed(() => {
return activeFields.value.reduce((acc: Record<string, any>, field) => {
acc[field.name] = _values[field.name];

return acc;
}, {});
});

const formValues = reactive<Record<string, any>>({});
const controller: FormController = {
register(field: FieldComposite) {
const name = unwrap(field.name);
// Set the initial value for that field
if (opts?.initialValues?.[name] !== undefined) {
_values[name] = opts?.initialValues[name];
}

fields.value.push(field);
},
unregister(field: FieldComposite) {
Expand All @@ -82,30 +67,32 @@ export function useForm(opts?: FormOptions) {
// in this case, this is a single field not a group (checkbox or radio)
// so remove the field value key immediately
if (field.idx === -1) {
delete _values[fieldName];
unsetPath(formValues, fieldName);
return;
}

// otherwise find the actual value in the current array of values and remove it
const valueIdx: number | undefined = _values[fieldName]?.indexOf?.(unwrap(field.valueProp));
const valueIdx: number | undefined = getFromPath(formValues, fieldName)?.indexOf?.(unwrap(field.valueProp));

if (valueIdx === undefined) {
delete _values[fieldName];
unsetPath(formValues, fieldName);

return;
}

if (valueIdx === -1) {
return;
}

if (Array.isArray(_values[fieldName])) {
_values[fieldName].splice(valueIdx, 1);
if (Array.isArray(formValues[fieldName])) {
unsetPath(formValues, `${fieldName}.${valueIdx}`);
return;
}
delete _values[fieldName];

unsetPath(formValues, fieldName);
},
fields: fieldsById,
values: _values,
values: formValues,
schema: opts?.validationSchema,
validateSchema: isYupValidator(opts?.validationSchema)
? (shouldMutate = false) => {
Expand All @@ -117,40 +104,42 @@ export function useForm(opts?: FormOptions) {

// singular inputs fields
if (!field || (!Array.isArray(field) && field.type !== 'checkbox')) {
_values[path] = value;
setInPath(formValues, path, value);
return;
}

// Radio buttons and other unknown group type inputs
if (Array.isArray(field) && field[0].type !== 'checkbox') {
_values[path] = value;
setInPath(formValues, path, value);
return;
}

// Single Checkbox
if (!Array.isArray(field) && field.type === 'checkbox') {
_values[path] = _values[path] === value ? undefined : value;
const newVal = getFromPath(formValues, path) === value ? undefined : value;
setInPath(formValues, path, newVal);
return;
}

// Multiple Checkboxes but their whole value was updated
if (Array.isArray(value)) {
_values[path] = value;
setInPath(formValues, path, value);
return;
}

// Multiple Checkboxes and a single item is updated
const newVal = Array.isArray(_values[path]) ? [..._values[path]] : [];
const oldVal = getFromPath(formValues, path);
const newVal = Array.isArray(oldVal) ? [...oldVal] : [];
if (newVal.includes(value)) {
const idx = newVal.indexOf(value);
newVal.splice(idx, 1);

_values[path] = newVal;
setInPath(formValues, path, newVal);
return;
}

newVal.push(value);
_values[path] = newVal;
setInPath(formValues, path, newVal);
},
};

Expand Down Expand Up @@ -182,6 +171,14 @@ export function useForm(opts?: FormOptions) {
fields.value.forEach((f: any) => f.reset());
};

const activeFormValues = computed(() => {
return activeFields.value.reduce((formData: Record<string, any>, field) => {
setInPath(formData, field.name, unwrap(field.value));

return formData;
}, {});
});

const handleSubmit = (fn?: SubmissionHandler) => {
return function submissionHandler(e: unknown) {
if (e instanceof Event) {
Expand All @@ -193,7 +190,7 @@ export function useForm(opts?: FormOptions) {
return validate()
.then(result => {
if (result && typeof fn === 'function') {
return fn(values.value, e as SubmitEvent);
return fn(activeFormValues.value, e as SubmitEvent);
}
})
.then(
Expand All @@ -220,12 +217,13 @@ export function useForm(opts?: FormOptions) {

provide('$_veeForm', controller);
provide('$_veeFormErrors', errors);
provide('$_veeFormInitValues', opts?.initialValues || {});

return {
errors,
meta,
form: controller,
values,
values: formValues,
validate,
isSubmitting,
handleReset,
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/utils/assertions.ts
@@ -1,5 +1,5 @@
import { Locator } from '../types';
import { isCallable } from '../../../shared';
import { isCallable, isObject } from '../../../shared';

export function isLocator(value: unknown): value is Locator {
return isCallable(value) && !!(value as any).__locatorRef;
Expand All @@ -21,3 +21,25 @@ export function isYupValidator(value: unknown): value is YupValidator {
export function hasCheckedAttr(type: unknown) {
return type === 'checkbox' || type === 'radio';
}

export function isIndex(value: unknown): value is number {
return Number(value) >= 0;
}

/**
* True if the value is an empty object or array
*/
export function isEmptyContainer(value: unknown): boolean {
if (Array.isArray(value)) {
return value.length === 0;
}

return isObject(value) && Object.keys(value).length === 0;
}

/**
* Checks if the path opted out of nested fields using `[fieldName]` syntax
*/
export function isNotNestedPath(path: string) {
return /^\[.+\]$/i.test(path);
}

0 comments on commit 8d161a1

Please sign in to comment.