mobx-react-form
Version:
Reactive MobX Form State Management
328 lines (294 loc) • 8.34 kB
text/typescript
import { each, get, has, isEmpty, isPlainObject, merge, set, transform } from "lodash";
import { FieldPropsEnum, SeparatedPropsMode } from "./models/FieldProps";
import {
$try,
isArrayOfStrings,
isArrayOfObjects,
hasUnifiedProps,
allowNested,
pathToStruct,
isEmptyArray,
} from "./utils";
const defaultValue = ({
type = undefined,
value = undefined,
nullable = undefined,
isEmptyArray = false,
fallbackValueOption = "",
}: any): null | false | 0 | [] | "" => {
if (Array.isArray(value) || isEmptyArray) return [];
if (nullable || value instanceof Date || type === "date" || type === "datetime-local") return null;
if (typeof value === 'number' || type === "number") return 0;
if (typeof value === 'boolean' || type === "checkbox") return false;
if (typeof value === 'string' || type === "file") return "";
return fallbackValueOption;
};
const parsePath = (path: any): string => {
let $path = String(path ?? '');
$path = $path.replace(/\[/g, ".");
$path = $path.replace(/\]/g, "");
return $path;
};
const parseInput = (
input: (...args: any[]) => any,
{ fallbackValueOption = "", type, isEmptyArray, separated, unified, fallback }: any
) =>
input(
$try(
separated,
unified,
fallback,
defaultValue({
fallbackValueOption,
type,
isEmptyArray,
})
)
);
const parseArrayProp = (val: any, prop: string, removeNullishValuesInArrays: boolean): any => {
const values = Object.values(val);
const isValProp: boolean = ([
FieldPropsEnum.value,
FieldPropsEnum.initial,
FieldPropsEnum.default,
] as string[]).includes(prop)
if (removeNullishValuesInArrays && isValProp) {
return values.filter(v => v !== null && v !== undefined && v !== "");
}
return values;
};
const parseCheckArray = (field: any, value: any, prop: string, removeNullishValuesInArrays: boolean) => {
if (field.incremental && value !== null && typeof value === 'object' && isEmpty(value)) return [];
return field.hasIncrementalKeys ? parseArrayProp(value, prop, removeNullishValuesInArrays) : value;
}
const parseCheckOutput = (field: any, prop: string, retrieveNullifiedEmptyStrings = false) => {
if (prop === FieldPropsEnum.value || prop.startsWith("value.")) {
const base = field.$output ? field.$output(field[FieldPropsEnum.value]) : field[FieldPropsEnum.value]
const value = prop.startsWith("value.") ? get(base, prop.substring(6)) : base
if (typeof value === 'string' && isEmpty(value) && retrieveNullifiedEmptyStrings) return null
return value;
}
return field[prop];
}
const defineFieldsFromStruct = (struct: string[], add: boolean = false) =>
struct.reduceRight(
($, name) => {
const obj: any = {};
if (name.endsWith("[]")) {
const val = add ? [$] : [];
obj[name.replace(/\[\]$/, "")] = val;
return obj;
}
// no brakets
const prev = struct[struct.indexOf(name) - 1];
const stop = !!prev && prev.endsWith("[]") && struct[struct.length - 1] === name;
if (!add && stop) return obj;
obj[name] = $;
return obj;
},
{}
);
const handleFieldsArrayOfStrings = ($fields: any, add = false) => {
let fields = $fields;
// handle array with field struct (strings)
if (isArrayOfStrings(fields)) {
fields = transform(
fields,
($obj, $) => {
const pathStruct = $.split(".");
// as array of strings (with empty values)
if (!pathStruct.length) return Object.assign($obj, { [$]: "" });
// define flat or nested fields from pathStruct
return merge($obj, defineFieldsFromStruct(pathStruct, add));
},
{}
);
}
return fields;
};
const handleFieldsArrayOfObjects = ($fields: any) => {
let fields = $fields;
// handle array of objects (with unified props)
if (isArrayOfObjects(fields)) {
fields = transform(
fields,
($obj, field) => {
if (hasUnifiedProps({ fields: { field } }) && !has(field, FieldPropsEnum.name)) return undefined;
return Object.assign($obj, { [field.name]: field });
},
{}
);
}
return fields;
};
const handleFieldsNested = (fields: any, strictProps: boolean = true): any =>
transform(
fields,
(obj, field, key) => {
if (allowNested(field, strictProps)) {
// define nested field
return Object.assign(obj, {
[key]: {
fields: isEmptyArray(field) ? [] : handleFieldsNested(field),
},
});
}
return Object.assign(obj, { [key]: field });
},
{}
);
/* mapNestedValuesToUnifiedValues
FROM:
{
street: '123 Fake St.',
zip: '12345',
}
TO:
[{
name: 'street'
value: '123 Fake St.',
}, {
name: 'zip'
value: '12345',
}]
*/
const mapNestedValuesToUnifiedValues = (data: Record<string, any>): any =>
isPlainObject(data)
? Object.entries(data).map(([name, value]) => ({ value, name }))
: undefined;
/* reduceValuesToUnifiedFields
FROM:
{
name: 'fatty',
address: {
street: '123 Fake St.',
zip: '12345',
},
};
TO:
{
name: {
value: 'fatty',
fields: undefined
},
address: {
value: {
street: '123 Fake St.',
zip: '12345'
},
fields: [ ... ]
},
};
*/
const reduceValuesToUnifiedFields = (values: Record<string, any>): Record<string, any> =>
transform(
values,
(obj, value, key) =>
Object.assign(obj, {
[key]: {
value,
fields: mapNestedValuesToUnifiedValues(value),
},
}),
{}
);
/*
Fallback Unified Props to Separated Mode
*/
const handleFieldsPropsFallback = (
fields: any,
initial: any,
fallback: any
) => {
if (!has(initial, SeparatedPropsMode.values)) return fields;
// if the 'values' object is passed in constructor
// then update the fields definitions
let { values } = initial;
if (hasUnifiedProps({ fields: initial.fields })) {
values = reduceValuesToUnifiedFields(values);
}
return merge(
fields,
transform(
values,
(result: any, v, k) => {
if (Array.isArray(fields[k])) result[k] = v;
if (!(k in fields) && (!isNaN(Number(k)) || fallback)) result[k] = v;
},
{}
)
);
};
const mergeSchemaDefaults = (fields: any, validator: any) => {
if (validator) {
const schema = get(validator.plugins, "svk.config.schema");
if (isEmpty(fields) && schema && !!schema.properties) {
each(schema.properties, (prop, key) => {
set(fields, key, {
value: prop.default,
label: prop.title,
});
});
}
}
return fields;
};
const prepareFieldsData = (
initial: any,
strictProps: boolean = true,
fallback: boolean = true,
) => {
let fields = merge(
handleFieldsArrayOfStrings(initial.fields, false),
handleFieldsArrayOfStrings(initial.struct, false)
);
fields = handleFieldsArrayOfObjects(fields);
fields = handleFieldsPropsFallback(fields, initial, fallback);
fields = handleFieldsNested(fields, strictProps);
return fields;
};
const pathToFieldsTree = (
struct: any,
path: string,
n: number = 0,
add: boolean = false
) => {
const $struct = (Array.isArray(struct) ? struct : Object.values(struct)).filter((item: any) => typeof item === 'string');
const structPath = pathToStruct(path);
const structArray = $struct.filter((item) =>
item.startsWith(structPath)
);
const $tree = handleFieldsArrayOfStrings(structArray, add);
const $structPath = structPath.replace(/\[\]/g, `[${n}]`);
const fields = handleFieldsNested(get($tree, $structPath));
// fix issues #614 & #615
$struct.length && $struct
.filter(s => s.startsWith(path + '[]'))
.map(s => s.substring((path + '[].').length))
.filter(s => s.endsWith('[]'))
.map(s => s.substring(0, s.length - 2))
.forEach(s => {
const ss = s.split('.')
let t = fields[0]?.fields
for (let i = 0; i < ss.length; i++) {
t = t?.[ss[i]]?.[FieldPropsEnum.fields]
if (!t) break;
}
if (t)
delete t[0]
});
return fields;
};
export {
defaultValue,
parseInput,
parsePath,
parseArrayProp,
parseCheckArray,
parseCheckOutput,
mergeSchemaDefaults,
handleFieldsNested,
handleFieldsArrayOfStrings,
prepareFieldsData,
pathToFieldsTree,
};