mobx-react-form
Version:
Reactive MobX Form State Management
218 lines (188 loc) • 6.35 kB
text/typescript
import { has, isEmpty, isPlainObject } from "lodash";
import { values as mobxValues, keys as mobxKeys } from "mobx";
import { FieldInterface } from "./models/FieldInterface";
import { AllowedFieldPropsTypes, FieldPropsEnum, FieldPropsOccurrence } from "./models/FieldProps";
import { props } from "./props";
const getObservableMapValues = (fields: any):
ReadonlyArray<FieldInterface> => {
// ArrayMap duck-typing
if (fields && fields._isArrayMap) {
const result: FieldInterface[] = [];
fields.forEach((value: FieldInterface) => result.push(value));
return result;
}
return mobxValues(fields);
};
const getObservableMapKeys = (fields: any):
ReadonlyArray<any> => {
// ArrayMap duck-typing
if (fields && fields._isArrayMap) {
return Array.from(fields.keys());
}
return mobxKeys(fields);
};
const checkObserveItem =
(change: any) =>
({ key, to, type, exec }: any) =>
change.type === type &&
change.name === key &&
change.newValue === to &&
exec.apply(change, [change]);
const checkObserve = (collection: Record<string, any>[]) => (change: any) =>
collection.map(checkObserveItem(change));
const checkPropOccurrence = ({ type, data }: any): boolean => {
let $check: any;
switch (type) {
case FieldPropsOccurrence.some: $check = ($data: object) => ($data as any[]).some(Boolean); break;
case FieldPropsOccurrence.every: $check = ($data: object) => ($data as any[]).every(Boolean); break;
default: throw new Error('Occurrence not found for specified prop');
}
return $check(data);
};
const hasProps = ($type: string, $data: any): boolean => {
let $props: string[] | null;
switch ($type) {
case AllowedFieldPropsTypes.computed:
$props = props.computed;
break;
case AllowedFieldPropsTypes.observable:
$props = [
FieldPropsEnum.fields,
...props.computed,
...props.editable,
];
break;
case AllowedFieldPropsTypes.editable:
$props = [
...props.editable,
...props.validation,
...props.functions,
...props.handlers,
];
break;
case AllowedFieldPropsTypes.all:
$props = [
FieldPropsEnum.id,
FieldPropsEnum.key,
FieldPropsEnum.name,
FieldPropsEnum.path,
...props.computed,
...props.editable,
...props.validation,
...props.functions,
...props.handlers,
];
break;
default:
$props = null;
}
return $data.filter((x: string) => $props!.includes(x)).length > 0;
};
/**
Check Allowed Properties
*/
const allowedProps = (type: string, data: string[]): void => {
if (hasProps(type, data)) return;
const $msg = "The selected property is not allowed";
throw new Error(`${$msg} (${JSON.stringify(data)})`);
};
/**
Throw Error if undefined Fields
*/
const throwError = (path: string, fields: any, msg: null | string = null): void => {
if (fields != null) return;
const $msg = msg == null ? "The selected field is not defined" : msg;
throw new Error(`${$msg} (${path})`);
};
const pathToStruct = (path: string): string => {
let struct;
struct = path.replace(/\.\d+($|\.)/g, "[].");
struct = struct.replace("..", ".");
struct = struct.replace(/^\.+|\.+$/g, "");
return struct;
};
const isArrayFromStruct = (struct: string[], structPath: string): boolean => {
if (isArrayOfStrings(struct)) return !!struct
.filter((s) => s.startsWith(structPath))
.find((s) => s.substring(structPath.length) === "[]")
|| (struct?.find((e) => e === structPath)?.endsWith('[]') ?? false);
else return false;
};
const hasSome = (obj: any, keys: any): boolean =>
keys.some((key: string) => has(obj, key));
const isEmptyArray = (field: any): boolean =>
isEmpty(field) && Array.isArray(field);
const isArrayOfStrings = (struct: any): boolean =>
Array.isArray(struct) && struct.every((s: any) => typeof s === 'string');
const isArrayOfObjects = (fields: any): boolean =>
Array.isArray(fields) && fields.every((f: any) => isPlainObject(f));
const getKeys = (fields: any) =>
fields ? [...new Set(Object.values(fields).flatMap((values) => values ? Object.keys(values) : []))] : [];
const hasUnifiedProps = ({ fields }: any) =>
!isArrayOfStrings({ fields }) && hasProps(AllowedFieldPropsTypes.editable, getKeys(fields));
const hasSeparatedProps = (initial: any): boolean =>
hasSome(initial, props.separated) || hasSome(initial, props.validation);
const allowNested = (field: any, strictProps: boolean): boolean =>
field !== null && typeof field === 'object' &&
!(field instanceof Date) &&
!has(field, FieldPropsEnum.fields) &&
!has(field, FieldPropsEnum.class) &&
(!hasSome(field, [
...props.editable,
...props.handlers,
...props.validation,
...props.functions,
]) || strictProps);
const parseIntKeys = (fields: any) =>
Array.from(getObservableMapKeys(fields)).map(Number);
const hasIntKeys = (fields: any): boolean =>
parseIntKeys(fields).every((x: any) => Number.isInteger(x));
const maxKey = (fields: any): number => {
const keys = parseIntKeys(fields);
const maxVal = keys.length ? Math.max(...keys) : undefined;
return maxVal === void 0 ? 0 : maxVal + 1;
};
let _idCounter = 0;
const _localUniqueId = (prefix: string): string => `${prefix}${++_idCounter}`;
const uniqueId = (field: any): string =>
_localUniqueId([field.path.replace(/\./g, "-"), "--"].join(""));
const isEvent = (obj: any): boolean => {
if (obj == null || typeof Event === "undefined") return false;
return obj instanceof Event || obj.target != null;
};
const hasFiles = ($: any): boolean =>
$.target.files && $.target.files.length !== 0;
const isBool = ($: any, val: any): boolean =>
typeof val === 'boolean' && typeof $.target.checked === 'boolean';
const $try = (...args: any[]) => {
for (const val of args) {
if (val !== undefined) return val;
}
return undefined;
};
export {
props,
checkObserve,
checkPropOccurrence,
hasProps,
allowedProps,
throwError,
isArrayOfStrings,
isEmptyArray,
isArrayOfObjects,
pathToStruct,
isArrayFromStruct,
hasUnifiedProps,
hasSeparatedProps,
allowNested,
parseIntKeys,
hasIntKeys,
maxKey,
uniqueId,
isEvent,
hasFiles,
isBool,
$try,
getObservableMapKeys,
getObservableMapValues,
};