How to watch an array props value changes? - javascript

Here is my main component.
I have an array and ı want to send it to my child component to fill necessary fields. I sent it with named infos prop.
<template>
<list-form :infos="infos"/>
</template>
<script>
export default {
data(){
return(){
infos:[
{name:'',surname:'',date:new Date().toISOString().substr(0, 10),menu:false},
{name:'',surname:'',date:new Date().toISOString().substr(0, 10),menu:false},
{name:'',surname:'',date:new Date().toISOString().substr(0, 10),menu:false},
{name:'',surname:'',date:new Date().toISOString().substr(0, 10),menu:false}
]
}
}
}
</script>
Here is my child component
I get my prop in here and i used inside v-for loop.
<template>
<v-row v-for="(info,i) in infos" :key="'list-item-'+i">
<v-col cols="6"><v-text-field v-model="info.name"/></v-col>
<v-col cols="6" > <v-text-field v-model="info.surName"/></v-col>
<v-col cols="6">
<v-menu v-model="info.menu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
max-width="290px"
min-width="auto">
<template v-slot:activator="{ on, attrs }">
<v-text-field v-model="info.dateFormatted"
label="date"
placeholder="GG/AA/YYYY"
v-bind="attrs"
v-on="on"/>
</template>
<v-date-picker
v-model="info.date"
no-title
#input="info.menu = false"
/>
</v-menu>
</v-col>
</v-row>
</template>
export default {
props:{
infos:{
type:Array
}
},
watch: {
**//the problem is in here**
date() {
this.dateFormatted = this.formatDate(date)
},
},
methods: {
formatDate(date) {
if (!date) return null
const [year, month, day] = date.split('-')
return `${month}/${day}/${year}`
}
}
}
</script>
I want to take date changes. If date changed i want to change its format . but I think it has a different way to catch changes in props. I tried some ways for this stuation but ı could not find true way.
Normally ı can get changes inside watch but i cant take changes with this way

You don't need to watch prop changes in your child component. Just watch changes in your parent component by using watch (if you really need) and write some conditions in your child component.
When you change data in parent component Vue will update your child component so you don't need to watch it inside.

Related

Vue.js & vuex handling SMS by server-side-events

I have a app, which is basically a Call centrum. You can receive calls, call to someone, receive sms and send sms etc.. I have problem with showing my SMS on screen, when I receive event from backend, I am showing that data on screen using vuex and V-for for specific component. Problem is that when I receive another event from backend with different number, I would like to show it under that first sms, but it will overwrite first sms and show only that new sms. I was trying multiple approaches, but nothing worked for me so I hope someone will be able to show me my mistake.
Here is photo of screen with one sms (red box is where second sms should be with own informations like number...)..
Here is code where I receive events.
export default function setupStream(){
let evtSource = new EventSource('/hzs/events.sse');
evtSource.addEventListener('receive_sms', event => {
let sms_data = JSON.parse(event.data);
store.dispatch('receiveSMS', sms_data);
}, false)
}
Here is my vuex code
const state = {
sms: [],
};
const getters = {
getSMS: (state) => state.sms,
};
const actions = {
receiveSMS({ commit }, sms_data) {
commit('setSMS', sms_data);
},
};
const mutations = {
setSMS: (state, sms) => (state.sms = sms),
};
export default {
state,
getters,
actions,
mutations
}
And here is component.
<template>
<v-card>
<v-card-title class="primary white--text">
{{ $t("Communication") }}
</v-card-title>
<v-card d-flex flex-column height="100%" class="card-outter scroll">
<v-col>
<div v-for="sms in getSMS" :key="sms.id">
<v-card-actions>
<v-row>
<v-btn #click="openChat" icon class="mt-4"
><v-img
max-width="30px"
max-height="30px"
class="mt-2"
src="#/assets/icons/icon-sms.svg"
alt="icon-sms"
/></v-btn>
<v-col>
<span>{{sms.date_time}}</span> <br />
<h4>{{sms.sender}}</h4>
<!-- Dialog for Adding new Note -->
<v-dialog
v-model="showEditor"
max-width="400px"
persistent
scrollable
>
<template v-slot:activator="{ on, attrs }">
<v-btn
#click="showEditor = true"
depressed
small
v-bind="attrs"
v-on="on"
>{{$t("Add Note")}}</v-btn
>
</template>
<AddNoteDialog v-on:close-card="showEditor = false"
/></v-dialog>
</v-col>
<v-spacer></v-spacer>
<v-btn class="mt-5" icon #click="deleteCommunication"
><v-img
max-width="20px"
src="#/assets/icons/icon-delete.svg"
alt="icon-delete"
/></v-btn>
</v-row>
</v-card-actions>
<v-divider></v-divider>
</div>
<v-spacer></v-spacer>
<v-divider></v-divider>
<v-card-actions class="card-actions">
<v-row>
<v-text-field
class="ml-4"
color="primary white--text"
required
:label="$t('Mobile number')"
clearable
></v-text-field>
<v-dialog
v-model="showEditor1"
max-width="450px"
persistent
scrollable
>
<template v-slot:activator="{ on, attrs }">
<v-btn
#click="showEditor1 = true"
class="mt-5 mr-4"
depressed
icon
v-bind="attrs"
v-on="on"
><v-icon>mdi-plus-circle</v-icon></v-btn
>
</template>
<AddNummberDialog v-on:close-card="showEditor1 = false"
/></v-dialog>
</v-row>
</v-card-actions>
</v-col>
</v-card>
</v-card>
</template>
<script>
import AddNoteDialog from "#/components/UI/AddNoteDialog";
import AddNummberDialog from "#/components/UI/AddNummberDialog";
import { mapGetters, mapActions } from 'vuex';
export default {
name: "Communication",
data() {
return {
dialog: false,
showEditor: false,
showEditor1: false,
note: '',
chat: this.switchChat,
};
},
computed: {
...mapGetters(['getSMS']),
},
components: { AddNoteDialog, AddNummberDialog },
props: ["switchChat"],
methods: {
...mapActions(['setupEvents']),
openChat() {
this.$emit('openChat')
},
async deleteCommunication() {
alert("Deleted");
},
},
};
</script>
<style>
.scroll {
overflow-y: scroll;
}
.card-outter {
padding-bottom: 50px;
}
.card-actions {
position: absolute;
bottom: 0;
width: 100%;
}
</style>
I think that solution is creating new array, where I will store every single SMS that I receive. Problem is that I don't know how and where to do it.
You already have your vue state array called sms which is a good start. You'll need to update your Vuex to have an additional mutation called "addNewSMS" or something:
const mutations = {
setSMS: (state, sms) => (state.sms = sms),
addNewSMS: (state, newSMS) => state.sms.push(newSMS),
};
This will update your state.sms array to include more than one element, which you should be able to loop through using a v-for loop in your template.
Of course, you'll also need to update your actions like this:
const actions = {
receiveSMS({ commit }, sms_data) {
commit('addNewSMS', sms_data);
},
};
As a sidenote, I'd personally change the sms variable name to messages so its clearer to you and other coders that it contains multiple objects.

TextArea Avoid mutating a prop directly since the value will be overwritten

I know that a similar question has already been dealt with on stackoverflow. But I could not put together a solution from the proposed one. I am very ashamed.
The essence is this: I have a component and another one inside it.
The child component-VClipboardTextField is a ready-made configured text-area. I couldn't get the input out of there and I don't get an error when I try to enter it.
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"message"
code tabs-item.vue
<template>
<v-container fluid>
<v-row align="center">
<v-col cols="9">
<v-card flat>
<v-card-text>
<h1>Request</h1>
<v-container>
<v-textarea v-model="message"
placeholder="Placeholder"
label="Request"
auto-grow
clear-icon="mdi-close-circle-outline"
clearable
rows="10"
row-height="5"
#click:clear="clearMessage"
></v-textarea>
<v-textarea v-model="response"
placeholder="Placeholder"
label="Request2"
auto-grow
counter
rows="10"
row-height="5"
color="success"
></v-textarea>
<VClipboardTextField ></VClipboardTextField>
<VClipboardTextField isReadOnly></VClipboardTextField>
</v-container>
<v-row>
<v-btn
dark
color="primary"
elevation="12"
justify="end"
float-right
#click="sendRequest"
>
Send Request
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="3">
<schema-selector #changeSchema="onChangeSchema"></schema-selector>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'tabs-item',
props: ['response'],
data() {
return {
schema: String,
message: '',
}
},
methods: {
sendRequest() {
const message = {
message: this.message,
method: this.schema.state
}
this.$emit('sendRequest', message)
},
clearMessage() {
this.message = ''
},
onChangeSchema(selectS) {
console.log("get schema: ", selectS.state)
this.schema = selectS
}
},
}
</script>
and child VClipboardTextField.vue
<template>
<v-container>
<v-tooltip bottom
v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: 'VClipboardTextField',
props: {
isReadOnly: Boolean,
message : { type :String, default: "msg"}
},
data() {
return {
show: false,
// messageLocal: 'Response!',
iconIndex: 0,
}
},
methods: {
copyToBuffer() {
console.log("this: ", this)
navigator.clipboard.writeText(this.message);
this.toolTipChange()
setTimeout(() => this.toolTipChange(), 1000)
},
clearMessage() {
this.message = ''
},
toolTipChange() {
if (this.show)
this.show = false
}
}
}
</script>
I will be glad to see an example of the code that will explain how to correctly solve this problem without crutches!
Thanks.
you cannot modify props in components, if the initial value of message is needed as placeholder (meaning the input might not be empty at the begining), you can store the data in message prop to another data variable and use that as v-model to the textarea.
if there is no initial value for message, just use another data variable for textarea and emit an update for message in a watcher.
in code tabs-item.vue
<VClipboardTextField :message.sync="message"></VClipboardTextField>
and child VClipboardTextField.vue
<template>
<v-container>
<v-tooltip bottom
v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message_slug"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: 'VClipboardTextField',
props: {
isReadOnly: Boolean,
message : { type :String, default: "msg"}
},
data() {
return {
show: false,
// messageLocal: 'Response!',
iconIndex: 0,
message_slug: '',
}
},
watch: {
message_slug(x) {
this.$emit('update:message', x)
}
},
}
</script>
It will bind the value to message_slug and update the message on parent component when it's value changes.
Instead of watching for change every time, you can only emit when there are changes.
In your tabs-item.vue
<VClipboardTextField v-model="message"></VClipboardTextField>
In your VClipboardTextField.vue component, you can receive the v-model input prop as a "VALUE" prop and assign it to a local data property. That way u can manipulate the local property and emit only when in it is changed!
<template>
<v-container>
<v-tooltip bottom v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
v-on="on"
v-bind="attrs"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
#change="$emit('input', v)"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: "VClipboardTextField",
props: {
isReadOnly: { type: Boolean },
value: {
type: String,
},
},
data() {
return {
show: false,
message: this.value,
};
},
};
</script>

How to use controlled components in Vue to set values in parent component object

I know how to use controlled componenents to emit the value that is selected. For example,
// app-select.vue
<v-select :items="[1,2,3]" #change="$emit('input', $event)"></v-select>
// parent-component.vue
<app-select v-model="selectedValue" />
So changing the value of v-select should change selectedValue on the parent component.
But what if I have an object I'd like to have updated by a controlled component with several selectors:
parent-comp.vue
<template>
<filter-comp> v-model="filterObj"</filter-comp>
</template>
<script>
import FilterComp from './filtercomp'
export default {
components: {
FilterComp
},
data () {
return {
filterObj: {}
}
}
}
</script>
and a child with several inputs capable of emiting on input:
<template>
<v-select :items="filterOneItems" #change="$emit('input', $event)">></v-select>
<v-select :items="filterTwoItems" #change="$emit('input', $event)">></v-select>
</template>
And let's say it would be my goal to make it so that when v-select input is given, it is updated on the parent component like so:
filterObj: {
filterOne: 'value 1',
filterTwo: 'value 2'
}
Is there a way to get this to work?
I can think of two solutions. First solution, just use prop not two-way binding:
For parent-comp.vue
<filter-comp :obj="filterObj"></filter-comp>
And for filter-comp.vue
<template>
<div>
<v-select :items="items" v-model="obj.filterOne"></v-select>
<v-select :items="items" v-model="obj.filterTwo"></v-select>
</div>
</template>
...
props: {
obj: {
type: Object,
default: {}
}
}
...
JSFiddle
Second solution, use v-model as you described:
For filter-comp.vue
<template>
<div>
<v-select :items="items" #change="change('filterOne', $event)"></v-select>
<v-select :items="items" #change="change('filterTwo', $event)"></v-select>
</div>
</template>
...
methods: {
change(name, value) {
this.$emit('input', {
...this.value,
[name]: value
})
}
}
...
JSFiddle

How to render a v-for based on changing props?

Maybe I have a missconcept based on React.js, but I am working wih Vue.js
I Just create a component:
<ResultCards v-bind:cards="cards"/>
cards is updated when an event is triggered by other component:
methods: {
fillResultCards(cards){
this.cards = cards;
}
}
This is my ResultCard Component
<template>
<div>
<v-card
v-for="card in cards"
v-bind:key="card.id"
>
// ..card detail here
</v-card>
</div>
</template>
<script>
export default {
name: 'ResultCards.vue',
props: {
cards: Array,
},
};
</script>
But when I update the "cards" props send me the message:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "cards"
I tried to copy cards prop to a internal dat
data() {
return { internalCards: this.cards };
}
but it doesn't works.
Okay, it works, maibe I had a typo, but this is the FULL flow.
I have external Component (a search bar)
<SearchGit v-on:item-selected="fillResultCards"/>
when search concept fill the input, it emit the event item-selected passing results (from API) as args.
this.$emit('item-selected', this.cache.filter(item => item.full_name === textSearch));
In Container I set my data with the function
data: () => ({
cards: [],
}),
methods: {
fillResultCards(cards){
this.cards = cards;
}
}
then this "new version" of cards pass as prop to the render component
<ResultCards v-bind:cards="cards"/>
And the render component only need to render data, and thats all:
<v-card
v-for="card in cards"
v-bind:key="card.id"
>
<v-list-item three-line>
<v-list-item-content>
<div class="overline mb-4">{{ card.name }}</div>
<v-list-item-title class="headline mb-1">{{
card.full_name
}}</v-list-item-title>
<v-list-item-subtitle>{{ card.description }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-avatar tile size="80" color="grey">
<img v-bind:src="card.owner.avatar_url" alt="avatar" />
</v-list-item-avatar>
</v-list-item>
</v-card>
I don't change any of the code, but maybe a cache or something keeps old changes, it works now.

Simple toggle function in Vue trigger: You may have an infinite update loop

I have read a few answers about infinite update loop and still don't understand the issue.
I still keep getting this error message:
[Vue warn]: You may have an infinite update loop in a component render function.
What is the correct Vue way to write simple toggle function? Looks like my approach is just wrong.
<template>
<v-content>
<v-container fluid fill-height>
<v-layout align-center justify-center>
<v-btn
color="normal"
:click="toggleLogin()"
>
{{login ? "Register" : "Login"}}
</v-btn>
</v-layout>
</v-container>
</v-content>
</template>
<script>
export default {
data: () => ({
login: true
}),
methods: {
toggleLogin: function() {
console.log(this.login)
this.login = !this.login
}
}
}
</script>
You should change data binding
:click="toggleLogin()"
to event handling:
#click="toggleLogin()"

Categories

Resources