Skip to content

Commit

Permalink
feat: added useField and useForm hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Jan 24, 2020
1 parent 0a7dd9c commit c1e9007
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ export { extend } from './extend';
export { configure } from './config';
export { setInteractionMode } from './modes';
export { localize } from './localize';
export { localeChanged } from './localeChanged';
export { ValidationProvider, ValidationObserver, withValidation } from './components';
export { normalizeRules } from './utils/rules';
export { useField } from './useField';
export { useForm } from './useForm';

const version = '__VERSION__';

Expand Down
167 changes: 167 additions & 0 deletions packages/core/src/useField.ts
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 ?? {})
};
}
104 changes: 104 additions & 0 deletions packages/core/src/useForm.ts
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();
}
});
}
};
}

0 comments on commit c1e9007

Please sign in to comment.