0.0.14 • Published 4 months ago

@volverjs/form-vue v0.0.14

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

@volverjs/form-vue

form form-field form-wrapper vue3 zod validation

proudly powered by

24/Consulting

Install

# pnpm
pnpm add @volverjs/form-vue

# yarn
yarn add @volverjs/form-vue

# npm
npm install @volverjs/form-vue --save

Usage

@volverjs/form-vue allow you to create a Vue 3 form with @volverjs/ui-vue components from a Zod Object schema. It provides three functions: createForm, useForm and formFactory.

Plugin

createForm defines globally three components VvForm, VvFormWrapper, and VvFormField through a Vue 3 Plugin.

import { createApp } from 'vue'
import { createForm } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
  name: z.string(),
  surname: z.string()
})

const app = createApp(App)
const form = createForm({
  schema
  // lazyLoad: boolean - default false
  // updateThrottle: number - default 500
  // continuosValidation: boolean - default false
  // sideEffects?: (data: any) => void
})

app.use(form)
app.mount('#app')

By default @volverjs/ui-vue components must be defined globally but can be lazy loaded with lazyLoad option. If the schema is omitted, the plugin only share the options to the forms created with the composable.

VvForm

VvForm render a form tag and emit a submit event. Form data are validated on submit. A valid or invalid event is emitted when the form status changes.

<script lang="ts" setup>
  const onSubmit = (formData) => {
    // Do something with the form data
  }
  const onInvalid = (errors) => {
    // Do something with the errors
  }
</script>

<template>
  <VvForm @submit="onSubmit" @invalid="onInvalid">
    <!-- form fields -->
    <button type="submit">Submit</button>
  </VvForm>
</template>

The submit can be triggered programmatically with the submit() method.

<script lang="ts" setup>
  import { ref } from 'vue'
  import type { FormComponent } from '@volverjs/form-vue'

  const formEl = ref<InstanceType<FormComponent>>(null)
  const onSubmit = (formData) => {
    // Do something with the form data
  }
  const submitForm = () => {
    formEl.value.submit()
  }
</script>

<template>
  <VvForm @submit="onSubmit" ref="formEl">
    <!-- form fields -->
  </VvForm>
  <button type="button" @click.stop="submitForm">Submit</button>
</template>

Use the v-model directive (or only :model-value to set the initial value of form data) to bind the form data. The form data two way binding is throttled by default (500ms) to avoid performance issues. The throttle can be changed with the updateThrottle option.

<script lang="ts" setup>
  import { ref } from 'vue'

  const formData = ref({
    name: '',
    surname: ''
  })
</script>

<template>
  <VvForm v-model="formData">
    <!-- form fields -->
  </VvForm>
</template>

The continuosValidation can be passed through options or with VvForm prop. With this field the validation doesn't stop and continue also after a validaton success.

<script lang="ts" setup>
  import { ref } from 'vue'

  const { VvForm, VvFormField } = useForm(MyZodSchema, {
    lazyLoad: true
    // continuosValidation: true
  })

  const formData = ref({
    name: '',
    surname: ''
  })
</script>

<template>
  <VvForm v-model="formData" :continuosValidation="true">
    <!-- form fields -->
  </VvForm>
</template>

VvFormWrapper

VvFormWrapper gives you the validation status of a part of your form. The wrapper status is invalid if at least one of the fields inside it is invalid.

<template>
  <VvForm>
    <VvFormWrapper v-slot="{ invalid }">
      <div class="form-section-1">
        <span v-if="invalid">There is a validation error</span>
        <!-- form fields of section 1 -->
      </div>
    </VvFormWrapper>
    <VvFormWrapper v-slot="{ invalid }">
      <div class="form-section-2">
        <span v-if="invalid">There is a validation error</span>
        <!-- form fields of the section 2 -->
      </div>
    </VvFormWrapper>
  </VvForm>
</template>

VvFormWrapper can be used recursively to create a validation tree. The wrapper status is invalid if at least one of the fields inside it or one of its children is invalid.

<template>
  <VvForm>
    <VvFormWrapper v-slot="{ invalid }">
      <div class="form-section">
        <span v-if="invalid">There is a validation error</span>
        <!-- form fields of section -->
        <VvFormWrapper v-slot="{ invalid: groupInvalid }">
          <div class="form-section__group">
            <span v-if="groupInvalid">There is a validation error</span>
            <!-- form fields of the group -->
          </div>
        </VvFormWrapper>
      </div>
    </VvFormWrapper>
  </VvForm>
</template>

VvFormField

VvFormField allow you to render a @volverjs/ui-vue input component inside a form. It automatically bind the form data through the name attribute.

<template>
  <VvForm>
    <VvFormField type="text" name="name" label="Name" />
    <VvFormField type="text" name="surname" label="Surname" />
  </VvForm>
</template>

For nested objects, use the name attribute with dot notation.

<template>
  <VvForm>
    <VvFormField type="text" name="shipping.address" label="Shipping address" />
  </VvForm>
</template>

The type of input component is defined by the type attribute. All the available input types are listed in the VvFormField documentation.

You can also use the VvFormField component to render a default slot without a type (default type is custom).

<template>
  <VvForm>
    <VvFormField
      v-slot="{
        modelValue,
        invalid,
        invalidLabel,
        formData,
        formErrors,
        errors,
        onUpdate
      }"
      name="surname"
    >
      <label for="surname">Surname</label>
      <input
        id="surname"
        type="text"
        :value="modelValue"
        :aria-invalid="invalid"
        :aria-errormessage="invalid ? 'surname-alert' : undefined"
        @input="onUpdate"
      />
      <small v-if="invalid" role="alert" id="surname-alert">
        {{ invalidLabel }}
      </small>
    </VvFormField>
  </VvForm>
</template>

Or a custom component.

<script lang="ts" setup>
  import MyInput from './MyInput.vue'
</script>

<template>
  <VvForm>
    <VvFormField name="surname" :is="MyInput" />
  </VvForm>
</template>

Nested VvFormField

In some use cases can be usefull nest VvFormField. For example let's assume:

  • a shopping list that is a field of our model (ex: ToDo list)
  • the sum of all products of the shopping list cannot be 0
  • we don't know all the products a priori

So our ToDo model and shopping list are structured like:

const toDo = {
  shoppingList: {
    bread: 0,
    milk: 0,
    tomato: 0,
    potato: 0,
    ...
  }
}

Our Zod schema can be:

const toDoSchema = z.object({
  shoppingList: z
    .object({})
    .default({})
    .superRefine((value, ctx) => {
      const shoppingList = value as Record<string, number>
      if (
        Object.keys(value).length &&
        !Object.keys(value).find((key) => shoppingList[key] > 0)
      ) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: i18n.global.t('atLeastOneProduct')
        })
      }
    })
})

And the Vue component:

<script setup lang="ts">
  const { VvForm, VvFormField } = useForm(toDoSchema, {
    lazyLoad: true,
    continuosValidation: true
  })

  // shopping list data, const or async
  const shoppingList = {
    bread: 0,
    milk: 0,
    tomato: 0,
    potato: 0
  }
</script>

<template>
  <VvForm>
    <VvFormField v-slot="{ invalid, invalidLabel }" name="shoppingList">
      <VvFormField
        v-for="key in Object.keys(shoppingList)"
        :key="key"
        :name="`shoppingList.${key}`"
        :label="$t(key)"
      />
      <small v-if="invalid" class="input-counter__hint">{{
        invalidLabel[0]
      }}</small>
    </VvFormField>
  </VvForm>
</template>

Composable

useForm can be used to create a form programmatically inside a Vue 3 Component. The default settings are inherited from the plugin (if it was defined).

<script lang="ts" setup>
  import { useForm } from '@volverjs/form-vue'
  import { z } from 'zod'

  const schema = z.object({
    name: z.string(),
    surname: z.string()
  })

  const { VvForm, VvFormWrapper, VvFormField } = useForm(schema, {
    // lazyLoad: boolean - default false
    // updateThrottle: number - default 500
    // continuosValidation: true - default false
    // sideEffects?: (formData: any) => void
  })
</script>

<template>
  <VvForm>
    <VvFormField type="text" name="name" label="Name" />
    <VvFormField type="text" name="surname" label="Surname" />
  </VvForm>
</template>

Outside a Vue 3 Component

formFactory can be used to create a form outside a Vue 3 Component. No settings are inherited.

import { formFactory } from '@volverjs/form-vue'
import { z } from 'zod'

const schema = z.object({
  name: z.string(),
  surname: z.string()
})

const { VvForm, VvFormWrapper, VvFormField } = formFactory(schema, {
  // lazyLoad: boolean - default false
  // updateThrottle: number - default 500
  // continuosValidation: true - default false
  // sideEffects?: (data: any) => void
})

export default { VvForm, VvFormWrapper, VvFormField }

Default Object by Zod Object Schema

defaultObjectBySchema creates an object by a Zod Object Schema. It can be useful to create a default object for a form. The default object is created by the default values of the schema and can be merged with an other object passed as parameter.

import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z.object({
  name: z.string().default('John'),
  surname: z.string().default('Doe')
})

const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { name: 'John', surname: 'Doe' }

const defaultObject = defaultObjectBySchema(schema, { name: 'Jane' })
// defaultObject = { name: 'Jane', surname: 'Doe' }

defaultObjectBySchema can be used with nested objects.

import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z.object({
  name: z.string().default('John'),
  surname: z.string().default('Doe'),
  address: z.object({
    street: z.string().default('Main Street'),
    number: z.number().default(1)
  })
})

const defaultObject = defaultObjectBySchema(schema)
// defaultObject = { name: 'John', surname: 'Doe', address: { street: 'Main Street', number: 1 } }

Other Zod methods are also supported: z.nullable(), z.coerce and z.passthrough().

import { z } from 'zod'
import { defaultObjectBySchema } from '@volverjs/form-vue'

const schema = z
  .object({
    name: z.string().default('John'),
    surname: z.string().default('Doe'),
    address: z.object({
      street: z.string().default('Main Street'),
      number: z.number().default(1)
    }),
    age: z.number().nullable().default(null),
    height: z.coerce.number().default(1.8),
    weight: z.number().default(80)
  })
  .passthrough()

const defaultObject = defaultObjectBySchema(schema, {
  height: '1.9',
  email: 'john.doe@test.com'
})
// defaultObject = { name: 'John', surname: 'Doe', address: { street: 'Main Street', number: 1 }, age: null, height: 1.9, weight: 80, email: 'john.doe@test.com' }

License

MIT

1.0.0-beta.34

4 months ago

1.0.0-beta.33

6 months ago

1.0.0-beta.32

7 months ago

1.0.0-beta.28

8 months ago

1.0.0-beta.29

8 months ago

1.0.0-beta.26

8 months ago

1.0.0-beta.27

8 months ago

1.0.0-beta.31

8 months ago

1.0.0-beta.30

8 months ago

1.0.0-beta.23

8 months ago

1.0.0-beta.24

8 months ago

1.0.0-beta.25

8 months ago

1.0.0-beta.22

9 months ago

1.0.0-beta.21

9 months ago

1.0.0-beta.20

9 months ago

1.0.0-beta.19

9 months ago

1.0.0-beta.17

9 months ago

1.0.0-beta.18

9 months ago

1.0.0-beta.15

1 year ago

1.0.0-beta.16

12 months ago

1.0.0-beta.14

1 year ago

1.0.0-beta.13

1 year ago

1.0.0-beta.12

1 year ago

1.0.0-beta.11

1 year ago

1.0.0-beta.10

1 year ago

1.0.0-beta.9

1 year ago

1.0.0-beta.8

1 year ago

0.0.14-beta.1

2 years ago

0.0.14-beta.2

2 years ago

0.0.14-beta.3

2 years ago

0.0.14-beta.4

2 years ago

0.0.13-beta.5

2 years ago

0.0.13

2 years ago

0.0.14

2 years ago

1.0.0-beta.2

2 years ago

1.0.0-beta.3

2 years ago

1.0.0-beta.4

2 years ago

1.0.0-beta.5

2 years ago

1.0.0-beta.1

2 years ago

1.0.0-beta.6

2 years ago

1.0.0-beta.7

2 years ago

0.0.12-beta.1

2 years ago

0.0.13-beta.1

2 years ago

0.0.13-beta.4

2 years ago

0.0.13-beta.3

2 years ago

0.0.12-beta.2

2 years ago

0.0.11-beta.1

2 years ago

0.0.10-beta.13

2 years ago

0.0.10

2 years ago

0.0.11

2 years ago

0.0.12

2 years ago

0.0.10-beta.12

2 years ago

0.0.10-beta.11

2 years ago

0.0.10-beta.10

2 years ago

0.0.10-beta.9

2 years ago

0.0.10-beta.8

2 years ago

0.0.10-beta.7

2 years ago

0.0.10-beta.6

2 years ago

0.0.10-beta.5

2 years ago

0.0.10-beta.4

2 years ago

0.0.10-beta.3

2 years ago

0.0.10-beta.2

2 years ago

0.0.10-beta.1

2 years ago

0.0.9

2 years ago

0.0.9-beta.1

2 years ago

0.0.8

2 years ago

0.0.8-beta.9

2 years ago

0.0.8-beta.2

2 years ago

0.0.8-beta.1

2 years ago

0.0.7

2 years ago

0.0.7-beta.6

2 years ago

0.0.7-beta.5

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago