1

I have this onChange method with its related function calls below for React-Jsonschema-Form. The problem I am facing is, newFormData when get passed to computeValueByFormula does not pass the async value.

onChange = ({ schema, formData }) => {
    const { properties } = schema

    this.updateTotalCell(properties)

    lookUpValue(properties, formData).then(newFormData => {
        const newFormData2 = computeValueByFormula(properties, newFormData)
        this.setState({ formData: newFormData2 })
    })
}

// recursive function to do math calculation on string formula input
// use case: mathCalculation("1 * 2 + 4 / 2 - 6")
export function mathCalculation (formula) {
  const plusOperator = '+'
  const minusOperator = '-'
  const multiplyOperator = '*'
  const divideOperator = '/'

  if (formula.indexOf(plusOperator) > 0) {
    const operands = formula.split(plusOperator)
    let total = 0

    operands.forEach(operand => {
      total = total + mathCalculation(operand)
    })

    return total
  }

  else if (formula.indexOf(minusOperator) > 0) {
    const operands = formula.split(minusOperator)
    let total = 0

    operands.forEach((operand, index) => {
      if (index === 0) {
        total = mathCalculation(operand)
      } 
      else {
        total = total - mathCalculation(operand)
      }
    })

    return total
  }

  else if (formula.indexOf(multiplyOperator) > 0) {
    const operands = formula.split(multiplyOperator)
    let total = 1

    operands.forEach(operand => {
      total = total * mathCalculation(operand)
    })

    return total
  }

  else if (formula.indexOf(divideOperator) > 0) {
    const operands = formula.split(divideOperator)
    let total = 1

    operands.forEach((operand, index) => {
      if (index === 0) {
        total = mathCalculation(operand)
      }
      else {
        total = total / mathCalculation(operand)
      }
    })

    return total
  }

  return Number(formula)
}

// compute field value based on value of other fields
export function computeValueByFormula (properties, formData) {
  let newFormData = {...formData}

  Object.keys(properties).forEach(key => {
    if (properties[key].formula) {
      const formula = properties[key].formula

      let operands = formula.replace(/\+|-|\*|\//g, ' ').split(' ')
      operands = operands.map(operand => formData[operand])

      if (properties[key].type === 'number') {
        const operators = formula.replace(/\w/g, '').split('')
        const updatedFormula = operands.map(operand => operators.length > 0 ? operand + operators.shift() : operand).join('')
        newFormData[key] = mathCalculation(updatedFormula)
      }
      else if (properties[key].type === 'string'){
        newFormData[key] = operands.join(' ')
      }
    }
    else if (properties[key].type === 'array') {
      if (formData[key] !== undefined) {
        newFormData[key].forEach((item, childKey) => {
          newFormData[key][childKey] = computeValueByFormula(properties[key].items.properties, formData[key][childKey])
        })
      }
    }
  })

  return newFormData
}

// lookup value based on value of other field
export function lookUpValue (properties, formData, parentFieldName, parentFormData) {
  let newFormData = {...formData}

  Object.keys(properties).forEach(async (key) => {
    if (properties[key].lookup) {
      const { collection, field, parameterField } = properties[key].lookup

      if (parentFormData !== undefined) { // pattern is in array field item
        if (parameterField.indexOf(':') > 0) { // parsing array field item
          const arrayRef = parameterField.split(':')
          const arrayField = arrayRef[0]
          const itemField = arrayRef[1]

          if (arrayField === parentFieldName) {
            const lookupValue = formData[itemField]
            newFormData[key] = await axios.get(`${API_URL}/record-lookup?collection_id=${collection}&lookup_field=${itemField}&lookup_value=${lookupValue}&lookup_target_field=${field}`)
              .then(res => res.data.data)
          }
        } else {
          const lookupValue = parentFormData[parameterField]
          newFormData[key] = await axios.get(`${API_URL}/record-lookup?collection_id=${collection}&lookup_field=${parameterField}&lookup_value=${lookupValue}&lookup_target_field=${field}`)
            .then(res => res.data.data)
        }
      } else {
        const lookupValue = formData[parameterField]
        newFormData[key] = await axios.get(`${API_URL}/record-lookup?collection_id=${collection}&lookup_field=${parameterField}&lookup_value=${lookupValue}&lookup_target_field=${field}`)
          .then(res => res.data.data)
      }
    }
    else if (properties[key].type === 'array') {
      if (formData[key] !== undefined) {
        newFormData[key].forEach(async (item, childKey) => {
            newFormData[key][childKey] = await lookUpValue(properties[key].items.properties, formData[key][childKey], key, formData).then(data => data)
        })
      }
    }
  })

  return Promise.resolve(newFormData)
}

What I want to accomplish is to look up a value based on user's input on the form. Let's say I have currency table on the database, whenever a user change a dropdown select of currency name, another field which supposed to hold the currency rate will do server call to fetch related currency data from database.

This field will have a lookup field on the schema. The same as field which has math calculation on it, will have formula field on the schema.

Somehow after the server call, it will update the formData on the other places, but formData that's passed to computeValueByFormula is not updated. Even if I pass it directly without the spread operator.

This give me a blank field on the form while on the formData I have the value.

Note: The recursive form of the functions needed to evaluate the array field items.

1
  • lookUpValue is not declared as an async function. No try..catch blocks or error handlers within .catch() appear at the code at the question. Commented Feb 12, 2019 at 0:29

1 Answer 1

0

You are resolving the lookUpValue function right away with newFormData by returning Promise.resolve(newFormData). This will not wait for any asynchronous callbacks being done inside your Object.keys(...).forEach(...)-loop. As lookUpValue has some asynchronous calculations, it should wait for them to finish.

If lookUpValue was marked as async, you could leave out the async keyword in the Object.keys(...).forEach(key => {...}) loop and it should wait for the await calls.

There is one caveat though: It will run the for each await calls sequentially, so it might take some time. It might be a better idea in this case to create multiple Promises and wait for them in parallel through Promise.all.

Using the parallel approach might yield performance boosts, but it may also introduce bugs due to race conditions. Especially since you are going through your state recursively there might be additional complexity involved.

Sign up to request clarification or add additional context in comments.

5 Comments

it doesn't seem to solve the issue. If I don't put async in array.forEach it will trigger another error stating that await should be used only within async function. I really need for the function to stop executing until the async value is resolved. Not really care of the performance issue currently.
You need to mark lookUpValue with async as well: async function lookUpValue(properties, formData, parentFieldName, parentFormData) { for that to work
Ah, I see, you mean in the Object.keys - you could use reduce instead of forEach to return a promise and wait for it: await Object.keys(...).reduce(async (lastPromise, key) => { await lastPromise; ... }, Promise.resolve())
I tried using array.reduce, it gives the same behavior. When I debug step by step, the this.setState is called before the async value resolved. I can't make the execution sequentially until the async value resolved.
It should not resolve, if you properly await each step... a basic example would be: [1, 2, 3].reduce(async (lastPromise, n) => { const sum = await lastPromise; await new Promise(resolve => setTimeout(resolve, 1000)); return sum + n }, Promise.resolve(0)).then(sum => console.log(sum)); - maybe you could provide a codesandbox, jsfiddle or make some test code?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.