react-formstate-fp
Version:
An API to build, query, update, and bind formstate in React. Develop forms DRYly in React.
210 lines (171 loc) • 5.64 kB
JavaScript
export function generateQuickGuid() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
export function exists(v) {
return v !== undefined && v !== null;
}
export function isContainer(v) {
return (typeof(v) === 'object' && v !== null) || typeof(v) === 'function';
}
export function isObject(v) {
return typeof(v) === 'object' && v !== null && !Array.isArray(v);
}
export function hasKey(container, name) {
return isContainer(container) && Object.keys(container).some(k => k === name);
}
export function hasProp(props, name) {
return isObject(props) && hasKey(props, name);
}
export function isNonEmptyString(v) {
return typeof(v) === 'string' && v.trim() !== '';
}
export function capitalize(s) {
return isNonEmptyString(s) ? (s.charAt(0).toUpperCase() + s.slice(1)) : s;
}
export function uncapitalize(s) {
return isNonEmptyString(s) ? (s.charAt(0).toLowerCase() + s.slice(1)) : s;
}
export function splitOnUpperCaseLetter(s) {
return String(s).split(/(?=[A-Z])/);
}
export function humanizeCamelCaseString(s) {
const a = splitOnUpperCaseLetter(s);
return a.map((s, i) => (i === 0 ? capitalize(s) : uncapitalize(s))).join(' ');
}
export function addScope(currentScope, scope) {
if (currentScope && scope) {return currentScope + '.' + scope;}
if (currentScope) {return currentScope;}
if (scope) {return scope;}
return '';
}
export function inScope(rootModelKey, scope) {
return rootModelKey.substring(0, scope.length) === scope;
}
export function parseParentScope(modelKey) {
const i = modelKey.lastIndexOf('.');
if (i === -1) {
return ['', modelKey];
}
return [modelKey.slice(0, i), modelKey.slice(i + 1)];
}
export function parseRootScope(modelKey) {
const i = modelKey.indexOf('.');
if (i === -1) {
return [modelKey, null];
}
return [modelKey.slice(0, i), modelKey.slice(i + 1)];
}
export function scopeLength(scopeKey) {
if (scopeKey === '') {return -1;}
return scopeKey.split('.').length - 1;
}
export function sortKeysByScopeLength(keysByIdToSort, rootModelKeysById) {
return Object.keys(keysByIdToSort).map(id => rootModelKeysById[id]).sort((k1, k2) => scopeLength(k2) - scopeLength(k1));
}
export function normalizeModelKey(modelKey) {
// convert 'contacts[0].address.line1' to 'contacts.0.address.line1'
let k = String(modelKey).replace(/\[(\w+)\]/g, '.$1');
return (k[0] !== '.') ? k : k.slice(1);
// If I go to lengths to clean things up here, I will have to do it in buildLookup as well. Not sure that's best.
// while (k[0] === '.') {k = k.slice(1);}
// while (k[k.length - 1] === '.') {k = k.slice(0, -1);}
// while (k.indexOf('..') !== -1) {k = k.replace('..', '.');}
// return k;
}
export function parseKeys(modelKey) {
return modelKey.split('.');
}
export function fieldName(modelKey) {
return parseKeys(modelKey).slice(-1);
}
// function modelHasKey(model, modelKey) {
// parseKeys(modelKey).forEach((key) => {
// if (hasKey(model, key)) {
// model = model[key];
// }
// else {
// return false;
// }
// });
// return true;
// }
export function modelGet(model, modelKey, throwError = true) {
if (modelKey === '') {
return model;
}
let curr = model, notFound = false;
parseKeys(modelKey).forEach((key) => {
if (hasKey(curr, key)) {
curr = curr[key];
}
else {
notFound = true;
}
});
if (notFound) {
if (throwError) {
throw new Error(`Unable to get modelKey "${modelKey}" from model ${JSON.stringify(model)}.`);
}
return undefined;
}
return curr;
}
function throwModelAssignError(rootModel, modelKey, action) {
let preposition = 'in';
if (action === 'add') {preposition = 'to';}
if (action === 'delete') {preposition = 'from';}
throw new Error(`Unable to ${action} modelKey "${modelKey}" ${preposition} model ${JSON.stringify(rootModel)}.`);
}
export function _modelAssign(currentModel, keys, value, rootModel, modelKey, action) {
if (!isContainer(currentModel)) {
throwModelAssignError(rootModel, modelKey, action);
}
const key = keys[0];
if ((keys.length !== 1 || action !== 'add') && !hasKey(currentModel, key)) {
throwModelAssignError(rootModel, modelKey, action);
}
if (keys.length === 1 && action === 'add') {
if (hasKey(currentModel, key)) {
throwModelAssignError(rootModel, modelKey, action);
}
if (Array.isArray(currentModel) && Number(key) !== currentModel.length) {
throwModelAssignError(rootModel, modelKey, action);
}
}
let newModel = shallowCopy(currentModel);
if (keys.length === 1) {
if (action == 'delete') {
if (Array.isArray(currentModel)) {
newModel = newModel.slice(0, Number(key)).concat(newModel.slice(Number(key) + 1));
}
else {
delete newModel[key];
}
}
else {
newModel[key] = value;
}
}
else {
newModel[key] = _modelAssign(currentModel[key], keys.slice(1), value, rootModel, modelKey, action);
}
return newModel;
}
export function modelAssign(model, modelKey, value) {
return _modelAssign(model, parseKeys(modelKey), value, model, modelKey, 'update');
}
export function modelAdd(model, modelKey, value) {
return _modelAssign(model, parseKeys(modelKey), value, model, modelKey, 'add');
}
export function modelDelete(model, modelKey) {
return _modelAssign(model, parseKeys(modelKey), null, model, modelKey, 'delete');
}
export function shallowCopy(value) {
if (Array.isArray(value)) {
return [...value];
}
if (isContainer(value)) {
return {...value};
}
return value;
}