Vue dynamic class binding with computed props - vue.js

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',
},

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

How to validate only onblur with Vuelidate?

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)"

Vue.js 3 - How can I pass data between Vue components and let both views also update?

I tried the following.
Please note the commented line in parent.vue that doesn't even commit the new state for me.
However maybe someone can guide me to a better solution for a global state shared by multiple components?
main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from 'vuex'
const app = createApp(App);
export const store = createStore({
state: {
textProp: 'test',
count: 1
},
mutations: {
setState(state, newState) {
console.log('setState');
state = newState;
}
},
getters: {
getAll: (state) => () => {
return state;
}
}
});
app.use(store);
app.mount('#app')
parent.vue
<template>
<div class="parent">
<div class="seperator" v-bind:key="item" v-for="item in items">
<child></child>
</div>
<button #click="toonAlert()">{{ btnText }}</button>
<button #click="veranderChild()">Verander child</button>
</div>
</template>
<script>
import child from "./child.vue";
import {store} from '../main';
export default {
name: "parent",
components: {
child,
},
store,
data: function () {
return {
items: [
{
id: 1,
valueText: "",
valueNumber: 0,
},
{
id: 2,
valueText: "",
valueNumber: 0,
},
{
id: 3,
valueText: "",
valueNumber: 0,
},
],
};
},
props: {
btnText: String,
},
methods: {
toonAlert() {
alert(JSON.stringify(this.$store.getters.getAll()));
},
veranderChild() {
console.log('child aan het veranderen (parentEvent)');
this.$store.commit('setState', { // This is especially not working.
textProp: 'gezet via de parent',
count: 99
})
this.$store.commit({type: 'setState'}, {
'textProp': 'gezet via de parent',
'count': 99
});
},
},
};
</script>
<style>
.seperator {
margin-bottom: 20px;
}
.parent {
/* background: lightblue; */
}
</style>
child.vue
<template>
<div class="child">
<div class="inputDiv">
text
<input #change="update" v-model="deText" type="text" name="deText" />
</div>
<div class="inputDiv">
nummer
<input v-model="hetNummer" type="number" name="hetNummer" />
</div>
<button #click="toonState">Toon huidige state</button>
</div>
</template>
<script>
import {store} from '../main';
export default {
name: "child",
store,
data: function() {
return {
'hetNummer': 0
}
},
methods: {
update(e) {
let newState = this.$store.state;
newState.textProp = e.target.value;
// this.$store.commit('setState', newState);
},
toonState()
{
console.log( this.$store.getters.getAll());
}
},
computed: {
deText: function() {
return '';
// return this.$store.getters.getAll().textProp;
}
}
};
</script>
<style>
.inputDiv {
float: right;
margin-bottom: 10px;
}
.child {
max-width: 300px;
height: 30px;
margin-bottom: 20px;
/* background: yellow; */
margin: 10px;
}
</style>
You have a misconception about JavaScript unrelated to Vue/Vuex. This doesn't do what you expect:
state = newState;
Solution (TL;DR)
setState(state, newState) {
Object.assign(state, newState);
}
Instead of setting the state variable, merge the new properties in.
Explanation
Object variables in JavaScript are references. That's why if you have multiple variables referring to the same object, and you change a property on one, they all mutate. They're all just referring to the same object in memory, they're not clones.
The state variable above starts as a reference to Vuex's state object, which you know. Therefore, when you change properties of it, you mutate Vuex's state properties too. That's all good.
But when you change the whole variable-- not just a property-- to something else, it does not mutate the original referred object (i.e. Vuex's state). It just breaks the reference link and creates a new one to the newState object. So Vuex state doesn't change at all. Here's a simpler demo.
Opinion
Avoid this pattern and create an object property on state instead. Then you can just do state.obj = newState.
You should use a spread operator ... to mutate your state as follows :
state = { ...state, ...newState };
LIVE EXAMPLE
but I recommend to make your store more organized in semantic way, each property in your state should have its own setter and action, the getters are the equivalent of computed properties in options API they could be based on multiple state properties.
export const store = createStore({
state: {
count: 1
},
mutations: {
SET_COUNT(state, _count) {
console.log("setState");
state.count=_count
},
INC_COUNT(state) {
state.count++
}
},
getters: {
doubleCount: (state) => () => {
return state.count*2;
}
}
});
**Note : ** no need to import the store from main.js in each child because it's available using this.$store in options api, but if you're working with composition api you could use useStore as follows :
import {useStore} from 'vuex'
setup(){
const store=useStore()// store instead of `$store`
}

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

create a component inside a component, getting error: "Failed to mount component: template or render function not defined."

trying to make a component (tag: simple-div-container) that on a button press will create 2 (tag: simple-div) components, inside each new simple-div there will be a simple-div-container.
so I can create "endless" components inside each other.
I have simple-div-container and when I press the button I get 2 simple-div
but I don't get inside them the NEW simple-div-container.
I get an error:
Failed to mount component: template or render function not defined.
code for tag: simple-div-container
<template>
<div>
<button #click="insert2Div" class="div-controler">insert 2 div</button>
<div v-if="divs" class="horizontal-align">
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
<simplediv v-if="divs" :style="{height: simpleDivHeight + 'px',width: simpleDivWidthPrecent/2 + '%', border: 1 + 'px solid' }"
:height="simpleDivHeight" :widthPrecent="simpleDivWidthPrecent" :isRender="true"></simplediv>
</div>
</div>
</template>
<script>
import SimpleDiv from '../simple-div/simpleDiv.vue';
export default {
props: {
simpleDivHeight: {
require: true,
type: Number
},
simpleDivWidthPrecent: {
require: true,
type: Number
}
},
data() {
return {
divs: false,
}
},
methods: {
insert2Div() {
console.log('insert 2 div')
this.divs = true;
},
},
components: {
simplediv: SimpleDiv
},
}
</script>
<style scoped>
.horizontal-align {
display: flex;
flex-direction: row;
}
</style>
code for tag: simple-div
<template>
<div>
<simple-div-container v-if="isRender" :simpleDivHeight="height" :simpleDivWidthPrecent="widthPrecent/2"></simple-div-container>
</div>
</template>
<script>
import simpleDivContainer from '../simple-div-container/simpleDivContainer.vue';
export default {
props: {
height: {
require: true,
type: Number
},
widthPrecent: {
require: true,
type: Number
},
isRender:{
require: true,
type: Boolean
}
},
data() {
return {
isDivContainer: false
}
},
components: {
'simple-div-container': simpleDivContainer
}
}
</script>
<style scoped>
.div-controler {
position: fixed;
transform: translate(-10%, -320%);
}
</style>
an interesting point: if i add to simple-div data some property(while webpack listens to changes) than it will automatically rerender and the new simpe-div-container will show
You have a circular reference problem. You should check if registering the simple-div component in the beforeCreate lifecycle hook helps. In the simple-div-container:
In the simple-div-container:
beforeCreate () {
this.$options.components.simple-div = require('../simple-div/simpleDiv.vue')
}