Search code examples
vuejs3vuetifyjs3

How to wrap Vuetify's v-row and v-col in one component?


I have Vue 3 app with Vuetify. I want to wrap Vuetify's v-row and v-col in one component.

The code without wrapping looks as following:

<v-row>
    <v-col cols="4">
        <v-text-field
            v-model="person.name_last"
            :rules="[validation.nameLastRules.required, validation.nameLastRules.counter]"
            minlength="4"
            maxlength="30"
        >
            <template #label>Фамилия<span class="text-red">*</span> </template>
        </v-text-field>
    </v-col>
    <v-col cols="4">
        <v-text-field
            v-model="person.name_first"
            :rules="[validation.nameFirstRules.required, validation.nameFirstRules.counter]"
            minlength="4"
            maxlength="30"
        >
            <template #label>Имя<span class="text-red">*</span> </template>
        </v-text-field>
    </v-col>

The validation code:

export const nameFirstRules = {
    required: (value: string) => {
        if (value) return true
        return 'Обязательное поле'
    },
    counter: (value: string) =>
        (value.length >= 4 && value.length <= 30) || 'Допустимое количество символов от 4 до 30',
}

As you can see on the screenshot below everything is fine.

enter image description here

The code with wrapping looks as following:

<v-form-row :cols="3">
    <v-text-field
        v-model="person.name_last"
        :rules="[validation.nameLastRules.required, validation.nameLastRules.counter]"
        minlength="4"
        maxlength="30"
    >
        <template #label>Фамилия<span class="text-red">*</span> </template>
    </v-text-field>
    <v-text-field
        v-model="person.name_first"
        :rules="[validation.nameFirstRules.required, validation.nameFirstRules.counter]"
        minlength="4"
        maxlength="30"
    >
        <template #label>Имя<span class="text-red">*</span> </template>
    </v-text-field>

As you can see on the screenshot below, first text field's length is longer that is not fine. I'm doing validation of string's length that should be longer or equal 4 characters.

enter image description here

The wrapped component looks as following:

<template>
    <v-row :class="columnsClass">
        <slot />
    </v-row>
</template>

<script setup lang="ts">
const props = defineProps<{
    cols?: number
}>()

const columnsClass = computed(() => {
    if (props.cols == null) {
        return null
    }

    const colCount = props.cols != null ? props.cols : 12
    const columnClassname = `md4 pa-${colCount} ga-${colCount + 3}`

    return {
        [columnClassname]: true,
    }
})
</script>

How can I make it work properly?

EDIT 1:

I've tried the code below with different flex settings, but got the same wrong behaviour:

<template>
    <v-row>
        <v-col class="setup" cols="12">
            <slot />
        </v-col>
    </v-row>
</template>

<style lang="css" scoped>
.setup {
    display: flex;
    gap: 25px;
}
</style>

EDIT 2:

The code:

<v-form-row :cols="3">
    <!-- <v-col cols="4"> -->
    <v-text-field
        v-model="person.name_last"
        :rules="[validation.nameLastRules.required, validation.nameLastRules.counter]"
        minlength="4"
        maxlength="30"
        :readonly="readMode"
    >
        <template #label>Фамилия<span class="text-red">*</span> </template>
    </v-text-field>
    <!-- </v-col>
    <v-col cols="4"> -->
    <v-text-field
        v-model="person.name_first"
        :rules="[validation.nameFirstRules.required, validation.nameFirstRules.counter]"
        minlength="4"
        maxlength="30"
        :readonly="readMode"
    >
        <template #label>Имя<span class="text-red">*</span> </template>
    </v-text-field>
    <!-- </v-col>
    <v-col cols="4"> -->
    <v-text-field
        v-model="person.name_middle"
        :rules="[validation.nameMiddleRules.required, validation.nameMiddleRules.counter]"
        minlength="4"
        maxlength="30"
        :readonly="readMode"
    >
        <template #label>Отчество<span class="text-red">*</span> </template>
    </v-text-field>
    <!-- </v-col>
</v-row> -->
</v-form-row>

...

<template>
    <v-row>
        <v-col v-for="colNode in $slots.default()" :cols="cols">
            <component :is="colNode" />
        </v-col>
    </v-row>
</template>

<script setup lang="ts">
defineProps<{
    cols?: number
}>()
</script>

It looks as following:

enter image description here enter image description here enter image description here


Solution

  • As @yoduh said, you have to add the VCol elements to get proper grid formatting. The HTML should look like this:

    <div class="v-container v-locale--is-ltr">
      <div class="v-row">
        <div class="v-col">first name input</div>
        <div class="v-col">last name input</div>
        <div class="v-col">third input</div>
      </div>
    </div>
    

    Otherwise, the inputs won't be restricted in width, and when the validation error message is longer than a third of the width, its container will extend, leading to the input taking up more space than the others.

    However, to add the VCols, you have to know how many items are in the slot, since each of them has to be wrapped individually (in your approach, you add all of them into the same VCol, which gives you the same behavior as before).

    To wrap them individually, build the slot manually and wrap the returned nodes in a v-for:

    <template>
      <v-row>
        <v-col v-for="columnNode in $slots.default()">
          <component :is="columnNode" />
        </v-col>
      </v-row>
    </template>
    

    Note that this will get unfeasible quickly when you want to configure the columns (set different widths for example).

    Here it is in a playground