finishes trail create
This commit is contained in:
255
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
Normal file
255
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
import {derived, writable, get} from 'svelte/store';
|
||||
import {util} from './util';
|
||||
|
||||
const NO_ERROR = '';
|
||||
const IS_TOUCHED = true;
|
||||
|
||||
function isCheckbox(element) {
|
||||
return element.getAttribute && element.getAttribute('type') === 'checkbox';
|
||||
}
|
||||
|
||||
function isFileInput(element) {
|
||||
return element.getAttribute && element.getAttribute('type') === 'file';
|
||||
}
|
||||
|
||||
function resolveValue(element) {
|
||||
if (isFileInput(element)) {
|
||||
return element.files;
|
||||
} else if (isCheckbox(element)) {
|
||||
return element.checked;
|
||||
} else {
|
||||
return element.value;
|
||||
}
|
||||
}
|
||||
|
||||
export const createForm = (config) => {
|
||||
let initialValues = config.initialValues || {};
|
||||
|
||||
const validationSchema = config.validationSchema;
|
||||
const validateFunction = config.validate;
|
||||
const onSubmit = config.onSubmit;
|
||||
|
||||
const getInitial = {
|
||||
values: () => util.cloneDeep(initialValues),
|
||||
errors: () =>
|
||||
validationSchema
|
||||
? util.getErrorsFromSchema(initialValues, validationSchema.fields)
|
||||
: util.assignDeep(initialValues, NO_ERROR),
|
||||
touched: () => util.assignDeep(initialValues, !IS_TOUCHED),
|
||||
};
|
||||
|
||||
const form = writable(getInitial.values());
|
||||
const errors = writable(getInitial.errors());
|
||||
const touched = writable(getInitial.touched());
|
||||
|
||||
const isSubmitting = writable(false);
|
||||
const isValidating = writable(false);
|
||||
|
||||
const isValid = derived(errors, ($errors) => {
|
||||
const noErrors = util
|
||||
.getValues($errors)
|
||||
.every((field) => field === NO_ERROR);
|
||||
return noErrors;
|
||||
});
|
||||
|
||||
const modified = derived(form, ($form) => {
|
||||
const object = util.assignDeep($form, false);
|
||||
|
||||
for (let key in $form) {
|
||||
object[key] = !util.deepEqual($form[key], initialValues[key]);
|
||||
}
|
||||
|
||||
return object;
|
||||
});
|
||||
|
||||
const isModified = derived(modified, ($modified) => {
|
||||
return util.getValues($modified).includes(true);
|
||||
});
|
||||
|
||||
function validateField(field) {
|
||||
return util
|
||||
.subscribeOnce(form)
|
||||
.then((values) => validateFieldValue(field, values[field]));
|
||||
}
|
||||
|
||||
function validateFieldValue(field, value) {
|
||||
updateTouched(field, true);
|
||||
|
||||
if (validationSchema) {
|
||||
isValidating.set(true);
|
||||
|
||||
return validationSchema
|
||||
.validateAt(field, get(form))
|
||||
.then(() => util.update(errors, field, ''))
|
||||
.catch((error) => util.update(errors, field, error.message))
|
||||
.finally(() => {
|
||||
isValidating.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (validateFunction) {
|
||||
isValidating.set(true);
|
||||
return Promise.resolve()
|
||||
.then(() => validateFunction({[field]: value}))
|
||||
.then((errs) =>
|
||||
util.update(errors, field, !util.isNullish(errs) ? errs[field] : ''),
|
||||
)
|
||||
.finally(() => {
|
||||
isValidating.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function updateValidateField(field, value) {
|
||||
updateField(field, value);
|
||||
return validateFieldValue(field, value);
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
const element = event.target;
|
||||
const field = element.name || element.id;
|
||||
const value = resolveValue(element);
|
||||
|
||||
return updateValidateField(field, value);
|
||||
}
|
||||
|
||||
function handleSubmit(event) {
|
||||
if (event && event.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
isSubmitting.set(true);
|
||||
|
||||
return util.subscribeOnce(form).then((values) => {
|
||||
if (typeof validateFunction === 'function') {
|
||||
isValidating.set(true);
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => validateFunction(values))
|
||||
.then((error) => {
|
||||
if (util.isNullish(error) || util.getValues(error).length === 0) {
|
||||
return clearErrorsAndSubmit(values);
|
||||
} else {
|
||||
errors.set(error);
|
||||
isSubmitting.set(false);
|
||||
}
|
||||
})
|
||||
.finally(() => isValidating.set(false));
|
||||
}
|
||||
|
||||
if (validationSchema) {
|
||||
isValidating.set(true);
|
||||
|
||||
return (
|
||||
validationSchema
|
||||
.validate(values, {abortEarly: false})
|
||||
.then(() => clearErrorsAndSubmit(values))
|
||||
// eslint-disable-next-line unicorn/catch-error-name
|
||||
.catch((yupErrors) => {
|
||||
if (yupErrors && yupErrors.inner) {
|
||||
const updatedErrors = getInitial.errors();
|
||||
|
||||
yupErrors.inner.map((error) =>
|
||||
util.set(updatedErrors, error.path, error.message),
|
||||
);
|
||||
|
||||
errors.set(updatedErrors);
|
||||
}
|
||||
isSubmitting.set(false);
|
||||
})
|
||||
.finally(() => isValidating.set(false))
|
||||
);
|
||||
}
|
||||
|
||||
return clearErrorsAndSubmit(values);
|
||||
});
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
form.set(getInitial.values());
|
||||
errors.set(getInitial.errors());
|
||||
touched.set(getInitial.touched());
|
||||
}
|
||||
|
||||
function clearErrorsAndSubmit(values) {
|
||||
return Promise.resolve()
|
||||
.then(() => errors.set(getInitial.errors()))
|
||||
.then(() => onSubmit(values, form, errors))
|
||||
.finally(() => isSubmitting.set(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to imperatively update the value of a form field
|
||||
*/
|
||||
function updateField(field, value) {
|
||||
util.update(form, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler to imperatively update the touched value of a form field
|
||||
*/
|
||||
function updateTouched(field, value) {
|
||||
util.update(touched, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the initial values and reset form. Used to dynamically display new form values
|
||||
*/
|
||||
function updateInitialValues(newValues) {
|
||||
initialValues = newValues;
|
||||
|
||||
handleReset();
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
errors,
|
||||
touched,
|
||||
modified,
|
||||
isValid,
|
||||
isSubmitting,
|
||||
isValidating,
|
||||
isModified,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
handleReset,
|
||||
updateField,
|
||||
updateValidateField,
|
||||
updateTouched,
|
||||
validateField,
|
||||
updateInitialValues,
|
||||
state: derived(
|
||||
[
|
||||
form,
|
||||
errors,
|
||||
touched,
|
||||
modified,
|
||||
isValid,
|
||||
isValidating,
|
||||
isSubmitting,
|
||||
isModified,
|
||||
],
|
||||
([
|
||||
$form,
|
||||
$errors,
|
||||
$touched,
|
||||
$modified,
|
||||
$isValid,
|
||||
$isValidating,
|
||||
$isSubmitting,
|
||||
$isModified,
|
||||
]) => ({
|
||||
form: $form,
|
||||
errors: $errors,
|
||||
touched: $touched,
|
||||
modified: $modified,
|
||||
isValid: $isValid,
|
||||
isSubmitting: $isSubmitting,
|
||||
isValidating: $isValidating,
|
||||
isModified: $isModified,
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
102
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
Normal file
102
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
/// <reference lib="svelte2tsx" />
|
||||
import type {SvelteComponentTyped} from 'svelte';
|
||||
import type {Readable, Writable} from 'svelte/store';
|
||||
import type {ObjectSchema} from 'yup';
|
||||
|
||||
export type FormProps<Inf = Record<string, unknown>> = {
|
||||
context?: FormState;
|
||||
initialValues?: Inf;
|
||||
onSubmit?: ((values: Inf) => any) | ((values: Inf) => Promise<any>);
|
||||
validate?: (values: Inf) => any | undefined;
|
||||
validationSchema?: ObjectSchema<any>;
|
||||
} & svelte.JSX.HTMLAttributes<HTMLFormElement>;
|
||||
|
||||
type FieldProperties = {
|
||||
name: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLInputElement>;
|
||||
|
||||
type SelectProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLSelectElement>;
|
||||
|
||||
type ErrorProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLDivElement>;
|
||||
|
||||
type TextareaProperties = {
|
||||
name: string;
|
||||
} & svelte.JSX.HTMLProps<HTMLTextAreaElement>;
|
||||
|
||||
type FormState<Inf = Record<string, any>> = {
|
||||
form: Writable<Inf>;
|
||||
errors: Writable<Record<keyof Inf, string>>;
|
||||
touched: Writable<Record<keyof Inf, boolean>>;
|
||||
modified: Readable<Record<keyof Inf, boolean>>;
|
||||
isValid: Readable<boolean>;
|
||||
isSubmitting: Writable<boolean>;
|
||||
isValidating: Writable<boolean>;
|
||||
isModified: Readable<boolean>;
|
||||
updateField: (field: keyof Inf, value: any) => void;
|
||||
updateValidateField: (field: keyof Inf, value: any) => void;
|
||||
updateTouched: (field: keyof Inf, value: any) => void;
|
||||
validateField: (field: keyof Inf) => Promise<any>;
|
||||
updateInitialValues: (newValues: Inf) => void;
|
||||
handleReset: () => void;
|
||||
state: Readable<{
|
||||
form: Inf;
|
||||
errors: Record<keyof Inf, string>;
|
||||
touched: Record<keyof Inf, boolean>;
|
||||
modified: Record<keyof Inf, boolean>;
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
isValidating: boolean;
|
||||
isModified: boolean;
|
||||
}>;
|
||||
handleChange: (event: Event) => any;
|
||||
handleSubmit: (event: Event) => any;
|
||||
};
|
||||
|
||||
declare function createForm<Inf = Record<string, any>>(formProperties: {
|
||||
initialValues: Inf;
|
||||
onSubmit: (values: Inf) => any | Promise<any>;
|
||||
validate?: (values: Inf) => any | undefined;
|
||||
validationSchema?: ObjectSchema<any>;
|
||||
}): FormState<Inf>;
|
||||
|
||||
declare class Form extends SvelteComponentTyped<
|
||||
FormProps,
|
||||
Record<string, unknown>,
|
||||
{
|
||||
default: FormState;
|
||||
}
|
||||
> {}
|
||||
|
||||
declare class Field extends SvelteComponentTyped<
|
||||
FieldProperties,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>
|
||||
> {}
|
||||
|
||||
declare class Textarea extends SvelteComponentTyped<
|
||||
TextareaProperties,
|
||||
Record<string, unknown>,
|
||||
Record<string, unknown>
|
||||
> {}
|
||||
|
||||
declare class Select extends SvelteComponentTyped<
|
||||
SelectProperties,
|
||||
Record<string, unknown>,
|
||||
{default: any}
|
||||
> {}
|
||||
|
||||
declare class ErrorMessage extends SvelteComponentTyped<
|
||||
ErrorProperties,
|
||||
Record<string, unknown>,
|
||||
{default: any}
|
||||
> {}
|
||||
|
||||
declare const key: {};
|
||||
|
||||
export {createForm, key, Form, Field, Select, ErrorMessage, Textarea};
|
||||
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
Normal file
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {createForm} from './create-form';
|
||||
135
web/src/lib/vendor/svelte-form-lib/util.js
vendored
Normal file
135
web/src/lib/vendor/svelte-form-lib/util.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
import { dequal as isEqual } from 'dequal/lite';
|
||||
|
||||
function subscribeOnce(observable) {
|
||||
return new Promise((resolve) => {
|
||||
observable.subscribe(resolve)(); // immediately invoke to unsubscribe
|
||||
});
|
||||
}
|
||||
|
||||
function update(object, path, value) {
|
||||
object.update((o) => {
|
||||
set(o, path, value);
|
||||
return o;
|
||||
});
|
||||
}
|
||||
|
||||
function cloneDeep(object) {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
|
||||
} catch (e) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
function isNullish(value) {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
function isEmpty(object) {
|
||||
return isNullish(object) || Object.keys(object).length <= 0;
|
||||
}
|
||||
|
||||
function getValues(object) {
|
||||
let results = [];
|
||||
|
||||
for (const [, value] of Object.entries(object)) {
|
||||
const values = typeof value === 'object' ? getValues(value) : [value];
|
||||
results = [...results, ...values];
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// TODO: refactor this so as not to rely directly on yup's API
|
||||
// This should use dependency injection, with a default callback which may assume
|
||||
// yup as the validation schema
|
||||
function getErrorsFromSchema(initialValues, schema, errors = {}) {
|
||||
for (const key in schema) {
|
||||
switch (true) {
|
||||
case schema[key].type === 'object' && !isEmpty(schema[key].fields): {
|
||||
errors[key] = getErrorsFromSchema(
|
||||
initialValues[key],
|
||||
schema[key].fields,
|
||||
{ ...errors[key] },
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case schema[key].type === 'array': {
|
||||
const values =
|
||||
initialValues && initialValues[key] ? initialValues[key] : [];
|
||||
errors[key] = values.map((value) => {
|
||||
const innerError = getErrorsFromSchema(
|
||||
value,
|
||||
schema[key].innerType.fields,
|
||||
{ ...errors[key] },
|
||||
);
|
||||
|
||||
return Object.keys(innerError).length > 0 ? innerError : '';
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
errors[key] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const deepEqual = isEqual;
|
||||
|
||||
function assignDeep(object, value) {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map((o) => assignDeep(o, value));
|
||||
}
|
||||
const copy = {};
|
||||
for (const key in object) {
|
||||
copy[key] =
|
||||
typeof object[key] === 'object' && !isNullish(object[key]) ? assignDeep(object[key], value) : value;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
function set(object, path, value) {
|
||||
if (new Object(object) !== object) return object;
|
||||
|
||||
if (!Array.isArray(path)) {
|
||||
path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
}
|
||||
|
||||
const result = path
|
||||
.slice(0, -1)
|
||||
// TODO: replace this reduce with something more readable
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce(
|
||||
(accumulator, key, index) =>
|
||||
new Object(accumulator[key]) === accumulator[key]
|
||||
? accumulator[key]
|
||||
: (accumulator[key] =
|
||||
Math.trunc(Math.abs(path[index + 1])) === +path[index + 1]
|
||||
? []
|
||||
: {}),
|
||||
object,
|
||||
);
|
||||
|
||||
result[path[path.length - 1]] = value;
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
export const util = {
|
||||
assignDeep,
|
||||
cloneDeep,
|
||||
deepEqual,
|
||||
getErrorsFromSchema,
|
||||
getValues,
|
||||
isEmpty,
|
||||
isNullish,
|
||||
set,
|
||||
subscribeOnce,
|
||||
update,
|
||||
};
|
||||
Reference in New Issue
Block a user