How to validate only onblur with Vuelidate? - vue.js

From my parent component I'm calling my custom input child component this way:
<custom-input
v-model="$v.form.userName.$model"
:v="$v.form.userName"
type="text"
/>
And here's my custom input component:
<template>
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="{ error: v && v.$error }"
>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
}
},
computed: {
inputListeners () {
const vm = this
return Object.assign({},
this.$listeners,
{
input (event) {
vm.$emit('blur', event.target.value)
}
}
)
}
}
}
</script>
This triggers validation errors from the very first character entered in the input field (which is arguably poor UX, so I really don't understand why this is default behavior).
Anyway, how to trigger such errors only on blur event?

This is not default behavior - it's your code!
Vuelidate validates (and raise errors) only after field is marked as dirty by calling $touch method. But when you are using $model property ($v.form.userName.$model) for v-model, it calls $touch automatically - docs
So either do not use $model for binding and call $touch by yourself on blur event (or whenever you want)
Alternatively you can try to use .lazy modifier on v-model but that is supported only on native input elements (support for custom components is long time request)
Example below shows how to implement it yourself....
Vue.use(window.vuelidate.default)
Vue.component('custom-input', {
template: `
<input
v-bind="$attrs"
:value="value"
v-on="inputListeners"
:class="status(v)"
></input>
`,
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
v: {
type: Object,
default: null
},
lazy: {
type: Boolean,
default: false
}
},
computed: {
inputListeners() {
const listeners = { ...this.$listeners }
const vm = this
const eventName = this.lazy ? 'change' : 'input'
delete listeners.input
listeners[eventName] = function(event) {
vm.$emit('input', event.target.value)
}
return listeners
}
},
methods: {
status(validation) {
return {
error: validation.$error,
dirty: validation.$dirty
}
}
}
})
const { required, minLength } = window.validators
new Vue({
el: "#app",
data: {
userName: ''
},
validations: {
userName: {
required,
minLength: minLength(5)
}
}
})
input {
border: 1px solid silver;
border-radius: 4px;
background: white;
padding: 5px 10px;
}
.dirty {
border-color: #5A5;
background: #EFE;
}
.dirty:focus {
outline-color: #8E8;
}
.error {
border-color: red;
background: #FDD;
}
.error:focus {
outline-color: #F99;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuelidate/dist/vuelidate.min.js"></script>
<script src="https://unpkg.com/vuelidate/dist/validators.min.js"></script>
<div id="app">
<custom-input v-model="$v.userName.$model" :v="$v.userName" type="text" lazy></custom-input>
<pre>Model: {{ userName }}</pre>
<pre>{{ $v }}</pre>
</div>

Try to emit the input event from the handler of blur event so :
instead of :
v-on="inputListeners"
set
#blur="$emit('input', $event.target.value)"

Related

How to fix this error: "v-model cannot be used on a prop, because local prop bindings are not writable?"

I'm trying to make a dropdown sort and I get this error:
VueCompilerError: v-model cannot be used on a prop, because local prop bindings are not writable. Use a v-bind binding combined with a v-on listener that emits update:x event instead.
Here are 2 components App and MySelect:
<template>
<!-- App Component -->
<div class="app">
<h1>Страница с постами</h1>
<div class="app__btns">
<my-button #click="showDialog">Cоздать пост</my-button>
<my-select v-model="selectedSort" :options="sortOptions" />
</div>
<my-dialog v-model:show="dialogVisible">
<post-form #create="createPost" />
</my-dialog>
<post-list :posts="posts" #remove="removePost" v-if="!isPostsLoading" />
<div v-else>Идет загрузка...</div>
</div>
</template>
<script>
import axios from 'axios'
import PostForm from './components/PostForm.vue'
import PostList from './components/PostList.vue'
export default {
components: { PostList, PostForm },
data() {
return {
posts: [],
dialogVisible: false,
isPostsLoading: false,
selectedSort: '',
sortOptions: [
{ value: 'title', name: 'По названию' },
{ value: 'body', name: 'По содержанию' },
],
}
},
methods: {
createPost(post) {
this.posts.push(post)
this.dialogVisible = false
},
removePost(post) {
this.posts = this.posts.filter((p) => p.id !== post.id)
},
showDialog() {
this.dialogVisible = true
},
async fetchPosts() {
try {
this.isPostsLoading = true
const res = await axios.get(
'https://jsonplaceholder.typicode.com/posts?_limit=10'
)
this.posts = res.data
} catch (error) {
alert('ошибка')
} finally {
this.isPostsLoading = false
}
},
},
mounted() {
this.fetchPosts()
},
}
</script>
<!-- флаг scoped - значит, что стили будут применяться только к этому комопненту -->
<style>
.app {
padding: 20px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.app__btns {
margin: 15px 0;
display: flex;
justify-content: space-between;
}
</style>
<template>
<!-- MySelect component -->
<select v-model="modelValue" #change="changeOption">
<option disabled value="">Выберите из списка</option>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.name }}
</option>
</select>
</template>
<script>
export default {
name: 'my-select',
props: {
modelValue: {
type: String,
},
options: {
type: Array,
default: () => [],
},
},
methods: {
changeOption(event) {
this.$emit('update:modelValue', event.target.value)
},
},
}
</script>
<style lang="scss" scoped></style>
I need to update modelValue, so I tried to add
:value="modelValue"
instead of
v-model="modelValue"
and it works, but I'm not sure if this is the correct solution.
If anyone else is encountering this issue when updating their vue version. Please note that this error started to appear on version 3.2.45.
For the implementation pattern, as noted on the documentation, props should be considered readonly within the component. Vue did not enforce it enough prior to version 3.2.45.
Documentation with links to good implementation patterns : https://vuejs.org/guide/components/props.html#one-way-data-flow

Styling component nested in opened new window

I'm trying to style component mounted in new window but it seems like it's not readable. However inline styling is working. It is possible to style with style section in component?
Code explain:
When component is mounted i'm opening new window with this component as argument.
<template>
<!-- Inline styling works -->
<div style="background-color: red" class="window-wrapper" v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=800,height=600,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
//here i'm opening new window with this compomnent
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeUnmounted() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
<style scoped>
.body {
box-sizing: border-box;
}
/* here styling is not working */
.window-wrapper {
background-color: red;
}
</style>
Also components passed as slot cannot read styles from styles section.

Vue dynamic class binding with computed props

I am trying to bind a class from a parent component to a child component via a computed switch case to an slot.
Parent:
<template>
<mcTooltip :elementType="'text'"><p>Test</p></mcTooltip>
</template>
<script>
import mcTooltip from '#/components/mcTooltip/index.vue';
export default {
components: {
mcTooltip
}
};
</script>
Child:
<template>
<div>
<slot :class="[elementClass]" />
</div>
</template>
<script>
export default {
props: {
elementType: {
type: String,
required: true,
// must have one of these elements
validator: (value) => {
return ['text', 'icon', 'button'].includes(value);
}
}
},
data() {
return {};
},
computed: {
elementClass: () => {
// return this.elementType ? 'tooltip--text' : 'tooltip--text';
// calls prop value for verification
switch (this.elementType) {
case 'text':
return 'tooltip--text';
case 'icon':
return 'tooltip--icon';
case 'button':
return 'tooltip--button';
default:
return 'tooltip--text';
}
}
},
};
</script>
<style lang="scss" scoped>
.tooltip--text {
text-decoration: underline dotted;
cursor: pointer;
&:hover {
background: $gray_220;
}
}
</style>
Whatever I try I dont seem to make it work in any way. Thats my latest attempt. The vue devtools say to my computed prop "(error during evaluation)".
I found a solution, the way I did it is as following:
<div
v-show="showTooltip"
ref="mcTooltipChild"
:class="['tooltip__' + elementType]"
></div>
elementType: {
type: String,
default: 'small',
},

Invalid prop: type check failed error in VueJS

I am trying to use a component named CardRenderer.vue which renders card using array of Objects. I am using the same component again & again to render the data. I am having this error "[Vue warn]: Invalid prop: type check failed for prop "renderData" when I tried passing prop from component.
I tried passing different values and different types but it did'nt work.
Here is the code:
CardRenderer.vue
<template lang="html">
<div>
<b-container class="bv-example-row">
<b-row v-for="(row, i) of rows" v-bind:key="i">
<b-col v-for="(item, j) of row" v-bind:key="j" >
<!-- you card -->
<b-card
:title="item.title"
img-src="item.icon"
img-alt="Image"
img-top
tag="article"
style="max-width: 20rem;"
class="mb-2"
>
<b-card-text>
<h1>{{item.name}}</h1>
<pre>{{item.description}}</pre>
</b-card-text>
<b-button :href="'/dashboard/'+item.name" variant="primary">More</b-button>
</b-card>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script lang="js">
export default {
name: 'CardRenderer',
props: {
renderData: []
},
data() {
return {
rows: null
}
},
mounted() {
const itemsPerRow = 3
let rowss = []
let arr = this.renderData
// eslint-disable-next-line
// console.log(this.renderData)
for (let i = 0; i < arr.length; i += itemsPerRow){
let row = []
for (let z = 0; z < itemsPerRow; z++) {
row.push(arr[z])
}
rowss.push(row)
}
this.rows = rowss
// eslint-disable-next-line
console.log(this.rows)
},
methods: {
},
computed: {
// rows() {
// }
}
}
</script>
<style scoped>
</style>
CardGrouper.vue:
<template lang="html">
<div class = "full" >
<div class="h-50" style=" background-color: #C8544F">
<h1 align="center">{{$store.getters.responseAPI.title}} </h1>
<CardRenderer :renderData=this.$store.getters.responseAPI.apps />
</div>
</div>
</template>
<script>
import CardRenderer from "./CardRenderer.vue"
/* eslint-disable */
export default {
name: 'CardGrouper',
components: {
CardRenderer
},
props: [],
mounted() {
},
data() {
return {
}
},
methods: {
},
computed: {
}
}
</script>
<style scoped >
.full{
width: 100vw;
height: 90vh;
background: linear-gradient(to bottom, Red 30%, white 50%);
}
</style>
Something.vue
<template lang="html">
<!-- <h1>Something</h1> -->
<CardRenderer :renderData=valObj />
</template>
<script lang="js">
import CardRenderer from './CardRenderer'
export default {
name: 'something',
components: {
CardRenderer
},
props: [],
data() {
return {
valObj: []
}
},
mounted() {
let key = this.findUrl()
let value = this.$store.getters.responseAPI.apps.filter((elem) => {
if(elem.name == key) return elem.apps
})
if (value.length > 0)
this.valObj = value[0].apps
//eslint-disable-next-line
console.log(this.valObj)
},
methods: {
findUrl() {
let url = window.location.pathname.split("/").slice(-1)[0];
return url
}
},
computed: {
}
}
</script>
<style scoped >
.something {
}
</style>
I am having this error.
It looks like this.
This is the data I am passing as a prop from Something.vue
This is how value looks like
Error is being generated somewhere from Something.vue.
I am passing array of objects as a prop.
How do i rectify this error, to make it work.
Set the renderData type as Array and default value to []:
props: {
renderData: {
type: Array,
deafult: () => []
}
}
It appears that you are defining your renderData prop as an array [] but probably are passing an object to it or something.
Either simplify it and do...
props: ['renderData']
Or if you are passing an object to it do..
props: {
renderData: {
type: Object,
}
}
If it is an array of objects do..
props: {
renderData: {
type: Array,
default: () => [{}];
}
just for doc.
// Object with a default value
propE: {
type: Object,
// Object or array defaults must be returned from
// a factory function
default: function () {
return { message: 'hello' }
}
},
This is in vue prop documentation

Cannot read property '$i18n' of undefined when using Vue Test Utils

I'm trying to test a BaseDialog component that uses VueI18n for translations with vue-test-utils. I cannot get the test to run do the the following error:
TypeError: Cannot read property '$i18n' of undefined
at VueComponent.default (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/BaseDialog.vue:2671:220)
at getPropDefaultValue (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1662:11)
at validateProp (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:1619:13)
at loop (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4612:17)
at initProps (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4643:33)
at initState (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4586:21)
at VueComponent.Vue._init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4948:5)
at new VueComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5095:12)
at createComponentInstanceForVnode (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3270:10)
at init (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3101:45)
at createComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5919:9)
at createElm (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:5866:9)
at VueComponent.patch [as __patch__] (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:6416:7)
at VueComponent.Vue._update (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:3904:19)
at VueComponent.updateComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4025:10)
at Watcher.get (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4426:25)
at new Watcher (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4415:12)
at mountComponent (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:4032:3)
at VueComponent.Object.<anonymous>.Vue.$mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/vue/dist/vue.runtime.common.dev.js:8350:10)
at mount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/#vue/test-utils/dist/vue-test-utils.js:8649:21)
at shallowMount (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/#vue/test-utils/dist/vue-test-utils.js:8677:10)
at Object.it (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/src/components/BaseDialog/__tests__/BaseDialog.spec.js:22:21)
at Object.asyncJestTest (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/jasmine_async.js:108:37)
at resolve (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:56:12)
at new Promise (<anonymous>)
at mapper (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:43:19)
at promise.then (/Users/mmelv/Workspace/Projects/Vue/vue-pux-port/node_modules/jest-jasmine2/build/queue_runner.js:87:41)
at process.internalTickCallback (internal/process/next_tick.js:77:7)
I've tried every solution listed here with no result.
Here is the relevant code:
// BaseDialog.spec.js
import { shallowMount, createLocalVue } from '#vue/test-utils'
import BaseDialog from '#/components/BaseDialog/BaseDialog'
import VueI18n from 'vue-i18n'
describe('BaseDialog', () => {
it('is called', () => {
let localVue = createLocalVue()
localVue.use(VueI18n)
const messages = {
gb: {
'ui.universal.label.ok': 'OK',
'ui.universal.label.cancel': 'Cancel'
}
}
const i18n = new VueI18n({
locale: 'gb',
fallbackLocale: 'gb',
messages
})
const wrapper = shallowMount(BaseDialog, {
i18n,
localVue
})
expect(wrapper.name()).toBe('BaseDialog')
expect(wrapper.isVueInstance()).toBeTruthy()
})
})
// BaseDialog.vue
<template>
<transition :name="animation">
<div v-if="isActive" class="dialog modal is-active" :class="size">
<div class="modal-background" #click="cancel('outside')" />
<div class="modal-card animation-content">
<header v-if="title" class="modal-card-head">
<p class="modal-card-title">{{ title }}</p>
</header>
<section
class="modal-card-body"
:class="{ 'is-titleless': !title, 'is-flex': hasIcon }"
>
<div class="media">
<div v-if="hasIcon" class="media-left">
<b-icon
:icon="icon ? icon : iconByType"
:pack="iconPack"
:type="type"
:both="!icon"
size="is-large"
/>
</div>
<div class="media-content">
<p v-html="message" />
<div v-if="hasInput" class="field">
<div class="control">
<input
ref="input"
v-model="prompt"
class="input"
:class="{ 'is-danger': validationMessage }"
v-bind="inputAttrs"
#keyup.enter="confirm"
/>
</div>
<p class="help is-danger">{{ validationMessage }}</p>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button
v-if="showCancel"
ref="cancelButton"
class="button"
#click="cancel('button')"
>
{{ cancelText }}
</button>
<button
ref="confirmButton"
class="button"
:class="type"
#click="confirm"
>
{{ confirmText }}
</button>
</footer>
</div>
</div>
</transition>
</template>
<script>
import Modal from '../BaseModal/BaseModal'
import config from '../../utils/config'
import { removeElement } from '../../utils/helpers'
export default {
name: 'BaseDialog',
extends: Modal,
props: {
title: {
type: String,
default: null
},
message: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
iconPack: {
type: String,
default: null
},
hasIcon: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'is-primary'
},
size: {
type: String,
default: null
},
confirmText: {
type: String,
default: () => {
return config.defaultDialogConfirmText
? config.defaultDialogConfirmText
: this.$i18n('ui.universal.label.ok')
}
},
cancelText: {
type: String,
default: () => {
return config.defaultDialogCancelText
? config.defaultDialogCancelText
: this.$i18n('ui.universal.label.cancel')
}
},
hasInput: Boolean, // Used internally to know if it's prompt
inputAttrs: {
type: Object,
default: () => ({})
},
onConfirm: {
type: Function,
default: () => {}
},
focusOn: {
type: String,
default: 'confirm'
}
},
data() {
const prompt = this.hasInput ? this.inputAttrs.value || '' : ''
return {
prompt,
isActive: false,
validationMessage: ''
}
},
computed: {
/**
* Icon name (MDI) based on the type.
*/
iconByType() {
switch (this.type) {
case 'is-info':
return 'information'
case 'is-success':
return 'check-circle'
case 'is-warning':
return 'alert'
case 'is-danger':
return 'alert-circle'
default:
return null
}
},
showCancel() {
return this.cancelOptions.indexOf('button') >= 0
}
},
beforeMount() {
// Insert the Dialog component in body tag
this.$nextTick(() => {
document.body.appendChild(this.$el)
})
},
mounted() {
this.isActive = true
if (typeof this.inputAttrs.required === 'undefined') {
this.$set(this.inputAttrs, 'required', true)
}
this.$nextTick(() => {
// Handle which element receives focus
if (this.hasInput) {
this.$refs.input.focus()
} else if (this.focusOn === 'cancel' && this.showCancel) {
this.$refs.cancelButton.focus()
} else {
this.$refs.confirmButton.focus()
}
})
},
methods: {
/**
* If it's a prompt Dialog, validate the input.
* Call the onConfirm prop (function) and close the Dialog.
*/
confirm() {
if (this.$refs.input !== undefined) {
if (!this.$refs.input.checkValidity()) {
this.validationMessage = this.$refs.input.validationMessage
this.$nextTick(() => this.$refs.input.select())
return
}
}
this.onConfirm(this.prompt)
this.close()
},
/**
* Close the Dialog.
*/
close() {
this.isActive = false
// Timeout for the animation complete before destroying
setTimeout(() => {
this.$destroy()
removeElement(this.$el)
}, 150)
}
}
}
</script>
<style lang="scss">
.dialog {
.modal-card {
max-width: 460px;
width: auto;
.modal-card-head {
font-size: $size-5;
font-weight: $weight-semibold;
}
.modal-card-body {
.field {
margin-top: 16px;
}
}
.modal-card-foot {
justify-content: flex-end;
.button {
display: inline; // Fix Safari centering
min-width: 5em;
font-weight: $weight-semibold;
}
}
#include tablet {
min-width: 320px;
}
}
&.is-small {
.modal-card,
.input,
.button {
#include control-small;
}
}
&.is-medium {
.modal-card,
.input,
.button {
#include control-medium;
}
}
&.is-large {
.modal-card,
.input,
.button {
#include control-large;
}
}
}
</style>
I don't really know what else to try here. This is the beginning of a project where I must support 9 languages with over 500 keys a piece, so I've got to get this working. Any help is very much appreciated.
The problem was I was referencing this in the props. Props are processed before the component is instantiated and therefore I had no access to this. It's always the little things that make you bash your head into the wall hahaha.