1

If the submit button is clicked in a form, it should automatically scroll to the first validation error if an error exists. I've read that I can use "scrolltoview" for this, but I don't know exactly how.

I have already tried it with a simple ScrollTo (0.0) to simply scroll up in the event of errors and it works perfectly. However, this is not the solution I would like to have.

< script >
  ...
  let name = 'm-form-user';
export default {
  name: name,
  mixins: [baseForm],
  props: {
    name: {
      type: String,
      default: name
    },
    title: {
      type: String,
      default: ''
    },
    type: {
      type: String,
      default: 'create',
      validator: function(value) {
        return ['edit', 'create'].indexOf(value) !== -1
      }
    },
  },
  data: () => ({
    form: {
      firstName: '',
      lastName: '',
      position: '',
      email: '',
      mobile: '',
      roles: []
    }
  }),
  async created() {
    if (!this.isCreationForm && this.$route.params.id) {
      if (!this.editingUser.length) {
        await this.requestUser({
          id: this.$route.params.id
        });
      }
      Object.assign(this.form, this.editingUser);
      this.form.roles.pop()
    }
  },
  computed: {
    ...mapGetters({
      getUser: "users/read"
    }),

    text() {
      return {
        cancel: this.$t('modules.forms.m-form-user.buttons.cancel'),
        submit: this.$t('modules.forms.m-form-user.buttons.submit')
      }
    },

    editingUser() {
      return this.getUser(this.$route.params.id)
    },
    isCreationForm() {
      return this.type === 'create'
    }
  },
  methods: {
    ...mapActions({
      requestCreateUser: 'users/create',
      requestUpdateUser: 'users/update',
      requestUser: 'users/read'
    }),
    async submit() {
      const validAll = await this.$validator.validateAll();
      const validIdentify = this.validateIdentify();

      if (!validAll || !validIdentify) {


        // ScrolltoView

        return;
      }


      try {
        this.setOrganizationRelation();
        let user = this.isCreationForm ? await this.createUser() : await this.updateUser();
        this.notify.success(this.$t(`notifications.account.userManagement.${ this.isCreationForm ? 'created':'edited'}`, {
          firstName: user.firstName,
          lastName: user.lastName
        }))
        this.redirect(this.nav.dashboard.account.users.view.name, {
          id: user._id
        })
      } catch (e) {
        if (e.response && e.response.status === 400) {
          e.response.data.violations.forEach(violation => {
            if (violation.propertyPath === 'username') return; //TODO temporary workaround, remove it when we get correct response from server
            this.$validator.errors.add({
              id: violation.propertyPath,
              field: violation.propertyPath,
              msg: violation.message
            });


            const field = this.$validator.fields.find({
              name: violation.propertyPath
            });

            if (!field) {
              throw `Field "${violation.propertyPath}" in "${this.$options._componentTag}" component don't have validation on client side!`;
            }

            field.setFlags({
              invalid: true,
              valid: false,
              validated: true
            });
          });
        } else {
          this.notify.processUnhandledError(e);
        }
      }

    },
    async createUser() {
      return await this.requestCreateUser({ ...this.form,
        password: passwordGenerator.generate()
      });
    },
    async updateUser() {
      return await this.requestUpdateUser(this.form)
    },
    cancel() {
      this.goBack();
    },

    validateIdentify() {
      if (!this.form.email && !this.form.mobile) {
        const fields = (({
          email,
          mobile
        }) => ({
          email,
          mobile
        }))(this.$refs);
        Object.keys(fields).forEach((key) => {
          let field = this.$validator.fields.find({
            name: fields[key].name
          });

          this.$validator.errors.add({
            id: field.id,
            field: field.name,
            msg: this.$t('modules.forms.m-form-user.sections.contacts.emptyContacts')
          });

          field.setFlags({
            invalid: true,
            valid: false,
            validated: true
          });

          this.$refs.emailBlock.open();
          this.$refs.mobileBlock.open();
        });
        return false;
      }
      return true;
    },
    setOrganizationRelation() {
      const rel = {
        organization: this.$user.relationships.organization
      };
      setRelations(this.form, rel)
    }
  }
} <
/script>
<m-block-form-fields :required="false">
  <template #title>
                    {{$t('modules.forms.m-form-user.sections.personal.title')}}
                </template>

  <template>
                    <v-layout wrap>
                        <v-flex xs12>
                            <e-input-user-name v-model="form.firstName" rules="required" required-style/>
                        </v-flex>
                        <v-flex xs12>
                            <e-input-user-surname v-model="form.lastName" rules="required" required-style/>
                        </v-flex>
                        <v-flex xs12>
                            <e-input-user-position-function v-model="form.position"/>
                        </v-flex>
                    </v-layout>
                </template>
</m-block-form-fields>
SGG
  • 43
  • 1
  • 6

3 Answers3

7

Try using document.querySelector to locate the first error message like below.

      if (!validAll || !validIdentify) {
        const el = document.querySelector(".v-messages.error--text:first-of-type");
        el.scrollIntoView();
        return;
      }
Eldar
  • 9,781
  • 2
  • 10
  • 35
  • somehow it doesn't work that way, but I don't get an error message either. – SGG Mar 26 '20 at 16:19
  • @SebastianGläßner the classes that error message have may vary. I took the default sample of the library and you may want to delay the execution a little bit with `setTimeout` and adding null checks to guarantee the code execution. – Eldar Mar 27 '20 at 08:51
4

This is based on @Eldar's answer.

Because you're changing the DOM you only want to look for the new element after the DOM has been updated.

I was able to get this to work with nextTick.

if(!validAll || !validIdentify) {
    // Run after the next update cycle
    this.$nextTick(() => {
        const el = this.$el.querySelector(".v-messages.error--text:first-of-type");
        this.$vuetify.goTo(el);
        return;
    });
}
tony19
  • 125,647
  • 18
  • 229
  • 307
2

First, put all your fields inside "v-form" tag

Second, give it a ref="form" as in:

<v-form
        ref="form"
        v-model="valid"
        lazy-validation
        @submit.prevent="() => {}"
> 
   ... all your fields ...
</v-form>

Finally, for the handler of your submit method, do this as a guard clause:

async submit() {
  // if a field is invalid => scroll to it
  if (!this.$refs.form.validate()) {
    const invalidField = this.$refs.form.$children.find((e) => !e.valid)
    if (invalidField)
      invalidField.$el.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
      })

    return
  }

// here, your form is valid and you can continue
}