When an event is triggered, I call a function that fills a variable and opens a modal from a child component. But, there, my new variable is empty, and if I close/re-open modal I have the data, so the data is loaded after.
I tried to load that child component after I have data, but no good till now.
Parent
<p class="open-modal-button" #click="openUpdatesModal">
<i class="fas fa-sync"></i>
Check for updates
</p>
<check-for-updates-modal
v-if="check_for_updates_values.account_name != ''"
:modalUpdatesOpen="modalUpdatesOpen"
:check_for_updates_values="check_for_updates_values"
/>
data() {
return {
//code....
check_for_updates_values: [],
modalUpdatesOpen: false,
};
}
openUpdatesModal() {
this.temporaryChecker();
},
temporaryChecker() {
this.check_for_updates_values.account_name = this.account_name;
this.check_for_updates_values.company_registration_number = this.company_registration_number;
this.check_for_updates_values.registered_address = this.registered_address;
this.modalUpdatesOpen = !this.modalUpdatesOpen;
},
Child
<b-col>{{check_for_updates_values.account_name}}</b-col>
props: [
"modalUpdatesOpen",
"check_for_updates_values",
],
watch: {
modalUpdatesOpen() {
this.checkForChanges();
this.$bvModal.show("modal-check-for-updates");
},
},
If you really initialize check_for_updates_values as an array, this is the problem. According to your usage, it should be an object.
Also, be careful to explicitly initialize every existing key on the object, or Vue won't be able to register them for reactivity watchers! That means if you have a data object empty foo: {}, any time you add a property, it won't refresh the vue foo.label = 'test' // won't render properly.
data() {
return {
check_for_updates_values: {
account_name: null,
company_registration_number: null,
registered_address: null,
},
};
}
Related
I'm trying to make a function where users can added multiple resume posts (from child component) to an array (in parent).
The problem is, every object I push to parent array stays reactive with the child form/object. So if I for example clear the form in child component, the Object I pushed to the parent array gets all it's values cleared as well. How to I emit and push the post-object to parent array and stop it from being reactive, so I can add new/more resume posts?
CreateProfile.vue
<template>
<ResumePostInput :resume_posts="form.resume_posts" #resumeHandler="handleResume"/>
</template>
<script>
export default {
data() {
form: {
resume_posts: []
}
}
methods: {
handleResume(post) {
this.form.resume_posts.push(post)
}
}
}
</script>
ResumePostInput.vue
<template
-- Input fields binded to post object --
</template>
<script>
export default {
emits: ["resumeHandler"],
props: {
resume_posts: Array
},
data() {
return {
post: {
title: '',
sub_title: '',
text: '',
year_from: '',
year_to: '',
type: ''
}
}
},
methods: {
addResume() {
this.$emit("resumeHandler", this.post)
}
}
}
</script>
you emit unknown property, it's a post, not posts
and learn about JS object, there are copy by reference & value
maybe you just need to update your addResume method like this
addResume() {
const post = JSON.parse(JSON.stringify(this.post))
this.$emit("resumeHandler", post)
}
It's not the problem that the object is reactive but that it's the same object because objects are passed by reference in JavaScript. If it's modified in one place, it's modified everywhere.
In order to avoid this, the object needs to be explicitly copied. For shallow object this can be done with object spread:
this.$emit("resumeHandler", {...this.post})
For deeply nested objects, multiple spreads or third-party clone function can be used.
On my main Vue instance I have a data attribute that is an array. It has 4 items:
var app = new Vue({
el: '#app',
efeitosAos: ['fade', 'flip-up', 'slide-up', 'zoom-in'],
aos: 'fade-in'
}
As you can see, it has names of effects from the AOS library. My idea is to get a random value from it and pass it to one of my components.
Each component is called inside a v-for:
<post v-for="post, index in posts" :key="post.id" :post="post" :efeito="aos">
{{randomizaAos()}}
</post>
This is my component:
const post = {
data: function(){
return {
duration: 1000,
delay: 50,
}
},
props: {
efeito: '',
post: {
id: '',
titulo: '',
resumo: '',
imagem: '',
},
},
template: '\
<section class="guarda-post" :data-aos="efeito" :data-aos-duration="duration" :data-aos-delay="delay">\
<img :src="post.imagem" class="img-fluid">\
<h4 class="titulo-post">{{post.titulo}}</h4>\
<p>{{post.resumo}}</p>\
</section>'
};
On my component, I have a prop called efeito. It should receive the value of my data aos, that comes from my main Vue instance. My idea is to use a method to change this aos data. So far, I got this:
methods:{
randomizaAos: function(){
var efeitoAleatorio = Math.floor(Math.random() * this.efeitosAos.length);
this.aos = this.efeitosAos[efeitoAleatorio];
console.log(this.aos);
}
}
The problem is that when I run the randomizaAos method, I get an infinite loop, and I don't know why. It's weird, because if I leave only the console.log inside the method, I get 4 messages, that is precisely the size of my posts array. But when I use the Math stuff and give to my aos data the value found, I get the infinite loop. Why is that? Any help?
Set the value of the efeito prop in the mounted method, so it won't be re-renderized everytime
Here's a part of my Vue template:
<ul>
<li v-for="friend in user.friends">
<span v-if="checkIfNew(friend.id)"> ... </span>
</li>
</ul>
Basically, friends is an array of objects, and I want to display the span element, if we have new messages from any of them. That's what checkIfNew() does. It checks whether the friend's id is in the unreadIds array (it contains ids of friends, who sent a message)
This array is being updated in a different method, but, here's the problem: v-if doesn't react to the changes.
Here's a part of the script section:
data(){
return {
unreadIds: []
}
},
methods:{
checkIfNew(id){
if(id in this.unreadIds) return true
else return false
}
},
computed:{
user(){
return this.$store.getters.user;
}
}
Does anyone have any ideas what am I doing wrong?
id in this.unreadIds doesn't do what you think it does. See the docs for the in operator. It will return true if the object has the value as a property. So if this.unreadIds had 3 items and you had an id of 1, then the in operator will return true because 1 is a property of the array (this.unreadIds[1] exists).
Instead, you should use includes.
Try rewriting your method like this:
checkIfNew(id) {
return this.unreadIds.includes(id);
}
Here's a working version of the component that updates the list without the Vuex store code:
<ul>
<li v-for="friend in user.friends" :key="friend.ids">
<span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
</li>
</ul>
export default {
data() {
return {
unreadIds: [5],
user: {
friends: [
{ id: 1, name: 'joe' },
{ id: 5, name: 'nancy' },
{ id: 9, name: 'sue' },
],
},
};
},
created() {
setTimeout(() => this.unreadIds.push(9), 2000);
},
methods: {
checkIfNew(id) {
return this.unreadIds.includes(id);
},
},
};
Just to prove here that David was correct all along, I put this code in a runable snippet, and cannot find any fault...
Vue.config.productionTip = false
new Vue({
el: '#app',
data() {
return {
unreadIds: [],
};
},
created() {
setTimeout(() => this.unreadIds.push(9), 2000);
},
methods: {
checkIfNew(id) {
// if(id in this.unreadIds) return true
// else return false
return this.unreadIds.includes(id);
},
},
computed: {
user(){
return {
friends: [
{ id: 1, name: 'joe' },
{ id: 5, name: 'nancy' },
{ id: 9, name: 'sue' }
]
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
<ul>
<li v-for="friend in user.friends" >
<span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
</li>
</ul>
</div>
The sample above is a bit closer to the original question
user is a computed not data
unreadIds is initially empty
Upvote from me!
You want to leverage Vue's reactivity system as the previous answers do not.. they will eventually open you up to inexplicable problems that aren't easily debuggable.
Rather than invoking a method in a v-for (which I guarantee will become problematic for you in the future), you should declare a computed list that contains (or does not contain) the items you want rendered, something like this:
data(){
return {
unreadIds: []
}
},
computed:{
user(){
return this.$store.getters.user;
},
NewFriends() {
return this.user.friends.filter(friend => this.unreadIds.includes(friend.id));
}
}
Your markup would then just be:
<ul>
<li v-for="friend in NewFriends">
<span > ... </span>
</li>
</ul>
And Vue's reactivity system would handle any changes to data dependencies for NewFriends.
You don't want to ever use method calls in a template because method calls are only guaranteed to be invoked once (the same applies to functions that come from computed methods...).
If you find yourself trying to trigger re-renders and dependency checks manually, you will want to re-think your design.
ETA: The only time you will ever want to invoke a function of any kind in a template is to respond to events.
I'll have a stab at it - someone posted this earlier and deleted it. You need checkIfNew() to be a computed not a method for reactivity in the template.
Since you need to pass in the id, the computed needs to return a function.
data(){
return {
unreadIds: []
}
},
computed:{
user(){
return this.$store.getters.user;
},
checkIfNew(){
return (id) => {
return this.unreadIds.includes(id);
}
}
}
As David Weldon says, you should ideally change the array immutably - and probably why ohgodwhy asked the original question.
let's say I have following tree:
[
{
name: 'asd',
is_whatever: true,
children: [
{
name: 'asd',
is_whatever: false,
children: [],
},
],
},
],
The tree is stored in a module via Vuex under key 'tree' and looped through with following recursive component called 'recursive-item':
<li class="recursive-item" v-for="item in tree">
{{ item.name }}
<div v-if="item.is_whatever">on</div>
<div v-else>off</div>
<ul v-if="tree.children.length">
<recursive-item :tree="item.children"></recursive-item>
</ul>
</li>
Now i want to toggle item's property 'is_whatever', so i attach a listener
<div v-if="item.is_whatever"
#click="item.is_whatever = !item.is_whatever">on</div>
<div v-else>off</div>
When i click it, it works, but emits following
"Error: [vuex] Do not mutate vuex store state outside mutation handlers."
[vuex] Do not mutate vuex store state outside mutation handlers.
How am I supposed to implement it without this error? I can see no way how to dispatch an action or emit event to the top of the tree because it's nested and recursive, so I haven't got a path to the specific item, right?
After consulting with some other devs later that evening we came with few ways how to achieve it. Because the data are nested in a tree and I access the nodes in recursive manner, I need to either get the path to the specific node, so for example pass the index of a node as a property, then add the child index while repeating that in every node recursively, or pass just the id of a node and then run the recursive loop in the action in order to toggle its properties.
More optimal solution could be flattening the data structure, hence avoiding the need for a recursion. The node would be then accessible directly via an id.
Right now you're changing the state object directly by calling item.is_whatever = !item.is_whatever, what you need to do is create a mutation function that will execute that operation for you to guarantee proper reactivity:
const store = new Vuex.Store({
state: { /* Your state */ },
mutations: {
changeWhatever (state, item) {
const itemInState = findItemInState(state, item); // You'll need to implement this function
itemInState.is_whatever = !item.is_whatever
}
}
})
Then you need to expose this.$store.commit('changeWhatever', item) as an action in your view that'll be trigger by the click.
There is a debatable solution, but I'll just leave it here.
State:
state: {
nestedObject: {
foo: {
bar: 0
}
}
}
There is Vuex mutation:
mutateNestedObject(state, payload) {
const { callback } = payload;
callback(state.nestedObject);
},
And this is an example of use in a component:
this.$store.commit('mutateNestedObject', {
callback: (nestedObject) => {
nestedObject.foo.bar = 1;
},
});
I am trying to map data so that elements only get re-rendered when values have actually changed.
{
Apps : [
{
"Categories" : [{
"Name" : "#Some,#More,#Tags,#For,#Measure"
}
],
"Concentrator" : "",
"Health" : 1,
"Id" : 2648,
"Ip" : "1.1.1.1",
"IsDisabled" : true,
"IsObsolete" : false,
"Name" : "",
"Path" : "...",
"SvcUrl" : "http://1.1.1.1",
"TimeStamp" : "\/Date(1463015444163)\/",
"Type" : "...",
"Version" : "1.0.0.0"
}
...
]
...
}
var ViewModel = function() {
self.Apps = ko.observableArray([]);
}
var myModel = new ViewModel();
var map = {
'Apps': {
create: function (options) {
return new AppModel(options.data);
},
key: function(data) { return ko.utils.unwrapObservable(data.Id); }
}
}
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(',');
ko.mapping.fromJS(data, { }, this);
return this;
}
function UpdateViewModel() {
return api.getDashboard().done(function (data) {
ko.mapping.fromJS(data, map, myModel);
});
}
loopMe(UpdateViewModel, 5000);
function loopMe(func, time) {
//Immediate run, once finished we set a timeout and run loopMe again
func().always(function () {
setTimeout(function () { loopMe(func, time); }, time);
});
}
<script type="tmpl" id="App-template">
<div>
<!-- ko foreach: Categories -->
<span class="btn btn-default btn-xs" data-bind="text:$data"></span>
<!-- /ko -->
</div>
</script>
On the first run of UpdateViewModel I will see 5 spans as expected. On the second call, receiving the same data, it gets updated to a single span that says [Object object] which is because it still thinks Categories is an array of objects instead of an array of strings.
Everything seems fixed if I change 'create' to 'update' in my map, however it seems that the spans are then re-rendered every time regardless if data changed or not.
Can anyone lend me a hand in the direction I need to go so that I can
adjust the Categories array from objects to strings
Only re-render/render changed/new items
Here is a Fiddle showing the behavior
The problem is with these lines:
var AppModel = function(data){
data.Categories = data.Categories[0].Name.split(','); // <-- mainly this one
ko.mapping.fromJS(data, { }, this);
return this;
}
There's two problems:
You mutate the data object which (at least in our repro) mutates the original object that data references to. So first time one of the fakeData objects is passed in, that one is mutated in place, and will forever be "fixed".
You mutate it in the AppModel constructor function, which is only called the first time. According to your key function, the second time the constructor should not be called, but instead ko-mapping should leave the original object and mutate it in place. But it will do so with a "wrongly" formatted data.Categories property.
The correct fix seems to me to be in your data layer, which we have mocked in the repro, so it makes little sense for my answer to show you how.
Another more hacky way to do this would be to have an update method in your mapping like so:
update: function(options) {
if (!!options.data.Categories[0].Name) {
options.data.Categories = options.data.Categories[0].Name.split(',');
}
return options.data;
},
When it encounters an "unmodified" data object it'll do the same mutation. See this jsfiddle for that solution in action.