Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$each does not work on non-objects #1139

Open
BennieVE opened this issue Dec 1, 2022 · 10 comments
Open

$each does not work on non-objects #1139

BennieVE opened this issue Dec 1, 2022 · 10 comments

Comments

@BennieVE
Copy link

BennieVE commented Dec 1, 2022

Describe the bug
$each is responsible for evaluating collections. The new documentation says to use the helpers.forEach. The example in the docs uses an array of objects, but when I try to apply it to non-objects such as strings / numbers it does not work.

Reproduction URL
The sandbox tries to evaluate the array of strings, but it doesn't pick up that anything is wrong.

Vuelidate 2 - Options API + Vue 3

To Reproduce
Steps to reproduce the behavior:

  1. Add "not an email" to the input field
  2. Click on 'Add'
  3. See that the array is valid

Expected behavior
The array should be invalid.

Additional context
Regarding the code sandbox. i know i can validate the email before it gets added.

@nolde
Copy link

nolde commented Dec 20, 2022

Affecting me as well.

I had to write a custom forEach to handle cases where items are not objects.

For the record, the code below is my own helper, based on vuelidate's own. It can probably be optimised, but I needed something fast to be able to move over to the new version.

/*
 * https://github.com/vuelidate/vuelidate/blob/next/packages/validators/src/utils/forEach.js
 */
import { computed } from 'vue'
import { helpers } from '@vuelidate/validators'

export default function forEach (validators, forceSimple = false) {
  return {
    $validator (collection, ...others) {
      return helpers.unwrap(collection).reduce(
        (previous, collectionItem, index) => {
          if (!forceSimple && typeof collectionItem === 'object') {
            const collectionEntryResult = objectForEach(collectionItem, index, validators, others)
            return {
              $valid: previous.$valid && collectionEntryResult.$valid,
              $data: previous.$data.concat(collectionEntryResult.$data),
              $errors: previous.$errors.concat(collectionEntryResult.$errors)
            }
          }
          const propertyResult = primitiveForEach(collectionItem, index, validators, others)
          return {
            $valid: previous.$valid && propertyResult.$valid,
            $data: previous.$data.concat(propertyResult.$data),
            $errors: previous.$errors.concat(propertyResult.$errors)
          }
        },
        { $valid: true, $data: [], $errors: [] }
      )
    },
    $message: ({ $response }) => {
      if (!$response) return []
      return $response.$errors.map(context => {
        if (context.$model) return context.$message
        return Object.values(context)
          .map(errors => (Array.isArray(errors) ? errors.map(e => e.$message) : errors.$message))
          .reduce((a, b) => a.concat(b), [])
      })
    },
    $params: computed(() => {
      return Object.entries(validators).reduce((map, [key, value]) => {
        map[key] = { ...value.$params }
        return map
      }, {})
    })
  }
}

function primitiveForEach ($model, index, validators, others) {
  const innerValidators = validators || {}
  return Object.entries(innerValidators).reduce(
    (all, [validatorName, currentValidator]) => {
      const validatorFunction = helpers.unwrapNormalizedValidator(currentValidator)
      const $response = validatorFunction.call(this, $model, index, ...others)
      const $valid = helpers.unwrapValidatorResponse($response)
      all.$data[validatorName] = $response
      all.$data.$invalid = !$valid || !!all.$data.$invalid
      all.$data.$error = all.$data.$invalid
      if (!$valid) {
        let $message = currentValidator.$message || ''
        const $params = currentValidator.$params || {}
        if (typeof $message === 'function') {
          $message = $message({
            $pending: false,
            $invalid: !$valid,
            $params,
            $model,
            $response
          })
        }
        all.$errors.push({
          $message,
          $params,
          $response,
          $model,
          $pending: false,
          $validator: validatorName
        })
      }
      return {
        $valid: all.$valid && $valid,
        $data: all.$data,
        $errors: all.$errors
      }
    },
    { $valid: true, $data: {}, $errors: [] }
  )
}

function objectForEach (collectionItem, index, validators, others) {
  return Object.entries(collectionItem).reduce(
    (all, [property, $model]) => {
      const innerValidators = validators[property] || {}
      const propertyResult = Object.entries(innerValidators).reduce(
        // eslint-disable-next-line no-shadow
        (all, [validatorName, currentValidator]) => {
          const validatorFunction = helpers.unwrapNormalizedValidator(currentValidator)
          const $response = validatorFunction.call(this, $model, collectionItem, index, ...others)
          const $valid = helpers.unwrapValidatorResponse($response)
          all.$data[validatorName] = $response
          all.$data.$invalid = !$valid || !!all.$data.$invalid
          all.$data.$error = all.$data.$invalid
          if (!$valid) {
            let $message = currentValidator.$message || ''
            const $params = currentValidator.$params || {}
            if (typeof $message === 'function') {
              $message = $message({
                $pending: false,
                $invalid: !$valid,
                $params,
                $model,
                $response
              })
            }
            all.$errors.push({
              $property: property,
              $message,
              $params,
              $response,
              $model,
              $pending: false,
              $validator: validatorName
            })
          }
          return {
            $valid: all.$valid && $valid,
            $data: all.$data,
            $errors: all.$errors
          }
        },
        { $valid: true, $data: {}, $errors: [] }
      )
      all.$data[property] = propertyResult.$data
      all.$errors[property] = propertyResult.$errors
      return {
        $valid: all.$valid && propertyResult.$valid,
        $data: all.$data,
        $errors: all.$errors
      }
    },
    { $valid: true, $data: {}, $errors: {} }
  )
}

@Ulrich-Mbouna
Copy link

Affecting me as well.

I had to write a custom forEach to handle cases where items are not objects.

For the record, the code below is my own helper, based on vuelidate's own. It can probably be optimised, but I needed something fast to be able to move over to the new version.

/*
 * https://github.com/vuelidate/vuelidate/blob/next/packages/validators/src/utils/forEach.js
 */
import { computed } from 'vue'
import { helpers } from '@vuelidate/validators'

export default function forEach (validators, forceSimple = false) {
  return {
    $validator (collection, ...others) {
      return helpers.unwrap(collection).reduce(
        (previous, collectionItem, index) => {
          if (!forceSimple && typeof collectionItem === 'object') {
            const collectionEntryResult = objectForEach(collectionItem, index, validators, others)
            return {
              $valid: previous.$valid && collectionEntryResult.$valid,
              $data: previous.$data.concat(collectionEntryResult.$data),
              $errors: previous.$errors.concat(collectionEntryResult.$errors)
            }
          }
          const propertyResult = primitiveForEach(collectionItem, index, validators, others)
          return {
            $valid: previous.$valid && propertyResult.$valid,
            $data: previous.$data.concat(propertyResult.$data),
            $errors: previous.$errors.concat(propertyResult.$errors)
          }
        },
        { $valid: true, $data: [], $errors: [] }
      )
    },
    $message: ({ $response }) => {
      if (!$response) return []
      return $response.$errors.map(context => {
        if (context.$model) return context.$message
        return Object.values(context)
          .map(errors => (Array.isArray(errors) ? errors.map(e => e.$message) : errors.$message))
          .reduce((a, b) => a.concat(b), [])
      })
    },
    $params: computed(() => {
      return Object.entries(validators).reduce((map, [key, value]) => {
        map[key] = { ...value.$params }
        return map
      }, {})
    })
  }
}

function primitiveForEach ($model, index, validators, others) {
  const innerValidators = validators || {}
  return Object.entries(innerValidators).reduce(
    (all, [validatorName, currentValidator]) => {
      const validatorFunction = helpers.unwrapNormalizedValidator(currentValidator)
      const $response = validatorFunction.call(this, $model, index, ...others)
      const $valid = helpers.unwrapValidatorResponse($response)
      all.$data[validatorName] = $response
      all.$data.$invalid = !$valid || !!all.$data.$invalid
      all.$data.$error = all.$data.$invalid
      if (!$valid) {
        let $message = currentValidator.$message || ''
        const $params = currentValidator.$params || {}
        if (typeof $message === 'function') {
          $message = $message({
            $pending: false,
            $invalid: !$valid,
            $params,
            $model,
            $response
          })
        }
        all.$errors.push({
          $message,
          $params,
          $response,
          $model,
          $pending: false,
          $validator: validatorName
        })
      }
      return {
        $valid: all.$valid && $valid,
        $data: all.$data,
        $errors: all.$errors
      }
    },
    { $valid: true, $data: {}, $errors: [] }
  )
}

function objectForEach (collectionItem, index, validators, others) {
  return Object.entries(collectionItem).reduce(
    (all, [property, $model]) => {
      const innerValidators = validators[property] || {}
      const propertyResult = Object.entries(innerValidators).reduce(
        // eslint-disable-next-line no-shadow
        (all, [validatorName, currentValidator]) => {
          const validatorFunction = helpers.unwrapNormalizedValidator(currentValidator)
          const $response = validatorFunction.call(this, $model, collectionItem, index, ...others)
          const $valid = helpers.unwrapValidatorResponse($response)
          all.$data[validatorName] = $response
          all.$data.$invalid = !$valid || !!all.$data.$invalid
          all.$data.$error = all.$data.$invalid
          if (!$valid) {
            let $message = currentValidator.$message || ''
            const $params = currentValidator.$params || {}
            if (typeof $message === 'function') {
              $message = $message({
                $pending: false,
                $invalid: !$valid,
                $params,
                $model,
                $response
              })
            }
            all.$errors.push({
              $property: property,
              $message,
              $params,
              $response,
              $model,
              $pending: false,
              $validator: validatorName
            })
          }
          return {
            $valid: all.$valid && $valid,
            $data: all.$data,
            $errors: all.$errors
          }
        },
        { $valid: true, $data: {}, $errors: [] }
      )
      all.$data[property] = propertyResult.$data
      all.$errors[property] = propertyResult.$errors
      return {
        $valid: all.$valid && propertyResult.$valid,
        $data: all.$data,
        $errors: all.$errors
      }
    },
    { $valid: true, $data: {}, $errors: {} }
  )
}

I'm Confused about this answer, using a librairy is suppose to reduce and facilitate the work and absorb the complicated part of implementing functionality.

I thinks i wil find another librairy making this kind of job. 🤔

@nolde
Copy link

nolde commented Mar 16, 2023

Well, that code already exists, I just split it into two different parts, with small modifications. It is code already available in the library.

@ting-dev-coder
Copy link

I still have this problem, any helps?

@wadclapp
Copy link

forEach helper is for an array of objects, should use Array.every() with custom validator (see here)?

Like this?

myEmails: {
  isEmails: (vals) => vals.every(v => email.$validator(v)),
},

Documentation isn't clear on it

@ting-dev-coder
Copy link

ting-dev-coder commented May 24, 2023

@wadclapp
in my case , I used v-for and an array to bind v-model. Instead of knowing error, I need to know which index in the array causes the error and show it. So using Array.every() did not solve my problem and that why I used $each.

@wadclapp
Copy link

wadclapp commented May 24, 2023

If you need index when validating an array you could do this (as suggested in same issue linked above)?

function validateEach(vals, validationObj) {
  return vals.reduce((accVal, currVal, index) => {
    accVal[index] = validationObj

    return accVal
  }, {})
}
validations() {
    return {
      emails: validateEach(this.emails, {
        email,
      }),
    }
  }

Output:

{
  ...
  "$invalid": false,
  ...
  "emails": {
    "0": {
      ...
      "$invalid": false,
      ...
    },
    "1": {

@nolde
Copy link

nolde commented May 25, 2023

@tp27933

The replacement I have published above should work for your use case. You don't have to use it all the time, just when you need primitive support on forEach. It has been working well for my use case.

@ting-dev-coder
Copy link

ting-dev-coder commented May 25, 2023

@wadclapp thanks , but which field should I use to know is there an error? $error or $invaild?
@nolde don't quite understand your answer above. which version of the library cover the code? I already update it to latest version ^2.0.2, but still not working

@nolde
Copy link

nolde commented May 25, 2023

@tp27933

Vuelidate has added a new helper called forEach that works with arrays of objects. Unfortunately, it does not work properly with arrays of primitives, such as strings.

My code above is a modification of the original forEach helper to add support to primitives. It has been working well for my use case.

Just drop it in a file on your project, import it and use it in place of the original one. It should still support object as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants