all files / src/ readField.js

98.06% Statements 152/155
96.91% Branches 94/97
88.89% Functions 16/18
98.15% Lines 106/108
6 statements, 1 function, 4 branches Ignored     
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160   373× 373× 336×   373×     49× 49× 49× 20×   29×     186×       522×     522× 522× 522× 522×       522×   96× 96× 96× 96× 96×   351× 277×   351×   277× 96× 81×   81×   81×     81×   96× 45× 45×   96× 96× 96× 95× 22× 22×   95× 95× 95×   95×   95×   27× 27×     96×     96×   426×   49× 49× 49× 49× 49× 49× 49× 49×   49× 38×         49× 49×   377× 377× 377× 199× 199× 199× 199× 199× 199× 199× 199× 199× 186× 186×   186× 186× 186× 186×     199× 199× 199×     377×         377× 377× 377× 377× 377×   377× 377×        
import createOnBlur from './events/createOnBlur';
import createOnChange from './events/createOnChange';
import createOnDragStart from './events/createOnDragStart';
import createOnDrop from './events/createOnDrop';
import createOnFocus from './events/createOnFocus';
import silencePromise from './silencePromise';
import read from './read';
import updateField from './updateField';
import isChecked from './isChecked';
 
function getSuffix(input, closeIndex) {
  let suffix = input.substring(closeIndex + 1);
  if (suffix[0] === '.') {
    suffix = suffix.substring(1);
  }
  return suffix;
}
 
const getNextKey = path => {
  const dotIndex = path.indexOf('.');
  const openIndex = path.indexOf('[');
  if (openIndex > 0 && (dotIndex < 0 || openIndex < dotIndex)) {
    return path.substring(0, openIndex);
  }
  return dotIndex > 0 ? path.substring(0, dotIndex) : path;
};
 
const shouldAsyncValidate = (name, asyncBlurFields) =>
  // remove array indices
  ~asyncBlurFields.indexOf(name.replace(/\[[0-9]+\]/g, '[]'));
 
const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncValidate, isReactNative, props, callback = () => null, prefix = '') => {
  const {asyncBlurFields, autofill, blur, change, focus, form, initialValues, readonly, addArrayValue,
    removeArrayValue, swapArrayValues} = props;
  const dotIndex = fieldName.indexOf('.');
  const openIndex = fieldName.indexOf('[');
  const closeIndex = fieldName.indexOf(']');
  Iif (openIndex > 0 && closeIndex !== openIndex + 1) {
    throw new Error('found [ not followed by ]');
  }
 
  if (openIndex > 0 && (dotIndex < 0 || openIndex < dotIndex)) E{
    // array field
    const key = fieldName.substring(0, openIndex);
    const rest = getSuffix(fieldName, closeIndex);
    const stateArray = state && state[key] || [];
    const fullPrefix = prefix + fieldName.substring(0, closeIndex + 1);
    const subfields = props.fields
      .reduce((accumulator, field) => {
        if (field.indexOf(fullPrefix) === 0) {
          accumulator.push(field);
        }
        return accumulator;
      }, [])
      .map(field => getSuffix(field, prefix.length + closeIndex));
    const addMethods = dest => {
      Object.defineProperty(dest, 'addField', {
        value: (value, index) => addArrayValue(pathToHere + key, value, index, subfields)
      });
      Object.defineProperty(dest, 'removeField', {
        value: index => removeArrayValue(pathToHere + key, index)
      });
      Object.defineProperty(dest, 'swapFields', {
        value: (indexA, indexB) => swapArrayValues(pathToHere + key, indexA, indexB)
      });
      return dest;
    };
    if (!fields[key] || fields[key].length !== stateArray.length) {
      fields[key] = fields[key] ? [...fields[key]] : [];
      addMethods(fields[key]);
    }
    const fieldArray = fields[key];
    let changed = false;
    stateArray.forEach((fieldState, index) => {
      if (rest && !fieldArray[index]) {
        fieldArray[index] = {};
        changed = true;
      }
      const dest = rest ? fieldArray[index] : {};
      const nextPath = `${pathToHere}${key}[${index}]${rest ? '.' : ''}`;
      const nextPrefix = `${prefix}${key}[]${rest ? '.' : ''}`;
 
      const result = readField(fieldState, rest, nextPath, dest, syncErrors,
        asyncValidate, isReactNative, props, callback, nextPrefix);
      if (!rest && fieldArray[index] !== result) {
        // if nothing after [] in field name, assign directly to array
        fieldArray[index] = result;
        changed = true;
      }
    });
    if (fieldArray.length > stateArray.length) {
      // remove extra items that aren't in state array
      fieldArray.splice(stateArray.length, fieldArray.length - stateArray.length);
    }
    return changed ? addMethods([...fieldArray]) : fieldArray;
  }
  if (dotIndex > 0) {
    // subobject field
    const key = fieldName.substring(0, dotIndex);
    const rest = fieldName.substring(dotIndex + 1);
    let subobject = fields[key] || {};
    const nextPath = pathToHere + key + '.';
    const nextKey = getNextKey(rest);
    const nextPrefix = prefix + key + '.';
    const previous = subobject[nextKey];
    const result = readField(state[key] || {}, rest, nextPath, subobject, syncErrors, asyncValidate,
      isReactNative, props, callback, nextPrefix);
    if (result !== previous) {
      subobject = {
        ...subobject,
        [nextKey]: result
      };
    }
    fields[key] = subobject;
    return subobject;
  }
  const name = pathToHere + fieldName;
  const field = fields[fieldName] || {};
  if (field.name !== name) {
    const onChange = createOnChange(name, change, isReactNative);
    const initialFormValue = read(`${name}.initial`, form);
    let initialValue = initialFormValue || read(name, initialValues);
    initialValue = initialValue === undefined ? '' : initialValue;
    field.name = name;
    field.checked = isChecked(initialValue);
    field.value = initialValue;
    field.initialValue = initialValue;
    if (!readonly) {
      field.autofill = value => autofill(name, value);
      field.onBlur = createOnBlur(name, blur, isReactNative,
        shouldAsyncValidate(name, asyncBlurFields) &&
        ((blurName, blurValue) => silencePromise(asyncValidate(blurName, blurValue))));
      field.onChange = onChange;
      field.onDragStart = createOnDragStart(name, () => field.value);
      field.onDrop = createOnDrop(name, change);
      field.onFocus = createOnFocus(name, focus);
      field.onUpdate = onChange; // alias to support belle. https://github.com/nikgraf/belle/issues/58
    }
    field.valid = true;
    field.invalid = false;
    Object.defineProperty(field, '_isField', {value: true});
  }
 
  const defaultFieldState = {
    initial: field.value,
    value: field.value,
  };
 
  const fieldState = (fieldName ? state[fieldName] : state) || defaultFieldState;
  const syncError = read(name, syncErrors);
  const updated = updateField(field, fieldState, name === form._active, syncError);
  Eif (fieldName || fields[fieldName] !== updated) {
    fields[fieldName] = updated;
  }
  callback(updated);
  return updated;
};
 
export default readField;