I know there is many similar questions about data not being updated in VueJS component but I still could not find the answer. I have the following component:
<template>
<div class="mt-5">
[...]
<div v-for="dataSource in dataSources" v-bind:key="dataSource.id">
{{ dataSource}}
</div>
[...]
</div>
</template>
<script>
import { chain, find, merge, toUpper } from "lodash";
import Mixins from "../utils/Mixins.vue";
import Pagination from "../utils/Pagination.vue";
import DataSourceTable from "./DataSourceTable.vue";
export default {
mixins: [Mixins],
components: {
"data-source-table": DataSourceTable,
"data-source-pagination": Pagination
},
data: function() {
return {
dataSources: [],
//[...]
};
},
methods: {
getAllDataSources(page) {
//[...]
},
search() {
//[...]
},
setSortAttribute(attribute) {
//[...]
},
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
this.dataSources[i] = updatedDataSource;
break; // Stop this loop, we found it!
}
}
}
},
created: function() {
this.getAllDataSources(this.currentPage);
// Capture updated data source via websocket listener
this.$options.sockets.onmessage = function(data) {
let message = JSON.parse(data.data);
if (message.id == "dataSource" && message.type == "data") {
let updatedDataSource = message.payload.data.listen.relatedNode;
this.updateDataSource(updatedDataSource);
}
}
}
};
</script>
In the created hook, I capture changes coming from a websocket and I update the corresponding item in the array dataSources. I can see the item is properly updated in Vue Dev Tools but it is still not updated in the component template. Example below:
This is a common mistake with vuejs.
You can check the document here: Why isn’t the DOM updating?
In Your case, you can use push or $set to achieve your purpose.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
Example:
Replace this.dataSources[i] = updatedDataSource;
By this.dataSources.splice(i, 1, updatedDataSource);
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely. Vue provides a convenience method arr.$set(index, value) which is syntax sugar for arr.splice(index, 1, value).
updateDataSource(updatedDataSource){
for (let i = 0; i < this.dataSources.length; i++) {
if (this.dataSources[i].id == updatedDataSource.id) {
//this.dataSources[i] = updatedDataSource;
this.dataSources.$set(i, updatedDataSource);
break; // Stop this loop, we found it!
}
}
}
Related
I have created a vuejs2 component that I want to do the following:
Receive an "event" using $on and display a <b-alert> for 6 seconds.
When the same "event" message is received restart the b-alert timeout
Display multiple <b-alert> if there are different messages received
This is what I've tried:
<template>
<div>
<b-alert
v-for="message in bannerMessages"
:key="message.messageId"
:show="message.secondsLeft"
:variant="message.level"
dismissible
fade
#dismissed="bannerMessageDismissed(message)"
#dismiss-count-down="bannerCountDown(message)"
>
{{ message.message }}
</b-alert>
</div>
</template>
<script>
import EventBus from "#/eventBus"
export default {
name: "EventBusNotifications",
data() {
return {
bannerMessages: []
};
},
methods: {
showMessage(message) {
for (var i = 0; i < this.bannerMessages.length; i++) {
if (this.bannerMessages[i].message === message.message) {
this.bannerMessages[i].secondsLeft = 6;
return;
}
}
this.bannerMessages.push({
...message,
messageId: Date.now() + `-${message.message}`,
secondsLeft: 6,
});
},
bannerMessageDismissed(message) {
const index = this.bannerMessages.indexOf(message);
if (index !== -1) {
this.bannerMessages.splice(index, 1);
}
},
bannerCountDown(message) {
const index = this.bannerMessages.indexOf(message);
console.log(index);
if (index !== -1) {
this.bannerMessages[index].secondsLeft -= 1;
if (this.bannerMessages[index].secondsLeft === 0) {
this.bannerMessages.splice(index, 1);
}
}
},
},
mounted() {
EventBus.$on("notification", this.showMessage);
},
destroyed() {
EventBus.$off("notification", this.showMessage);
},
};
</script>
And here is the eventBus file:
import Vue from "vue";
const EventBus = new Vue();
export default EventBus;
THen, to use it, in any vue.js component or page, import EventBus from "#/eventBus" and then send an $emit like this:
EventBus.$emit("notification", { level: "warning", message: "This is the message", secondsLeft: 5});
The idea here is to reset the number of seconds on the <b-alert> if a matching message came in, but instead the message only appears very briefly (less than half a second). The bannerCountDown() method gets called rapidly instead of every second.
What would be the best approach to making a "restart-able" <b-alert>?
The problem is v-model/show implementation in <b-alert> was not designed for your use-case. The dismiss-count-down is emitted on any component update.
In your case, this creates a loop which brings the counter down immediately, hiding the alert (because you're mutating the objects in the dismiss-count-down, thus triggering another emit). The alerts are only visible because they have fade. Without it, they'd be removed on $nextTick, so they'd probably not be visible for the human eye.
The proper way to circumvent this issue would be to keep counters outside of the component and not rely on the component's counter at all, using some form of debounce. You'll need this solution if you want to display the counter values.
If you only want to reset the counter, without actually displaying its value in real time, here's a working demo, reusing most of your existing logic. I moved the timers in an external object to avoiding additional updates on <b-alert>s.
The key to why the above works is I'm not replacing the timers object when I'm changing a timer. if I replaced it (e.g: in tick i'd do:
this.timers = { ...this.timers, [message]: this.timers[message] - 1 }
it would behave exactly like yours.
I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty
I'm building a shop in Ember with the list of products that are being added to Local Storage when the user clicks on the
add to cart button. Each product is an object that has a property called ordered_quantity, I'm trying to change this property's value when the user tries to remove the product from the cart. (example: ordered quantity: 8, when the button is clicked it should be 7).
I have the following code in my service file:
remove(item) {
let new_arr = this.get('items');
let elementIndex = new_arr.findIndex(obj => {
return obj.id === item.id;
});
if (elementIndex !== -1) {
new_arr[elementIndex].ordered_quantity = new_arr[elementIndex].ordered_quantity - 1;
}
this.set('cart.items', new_arr);
}
I'm using Local Storage add-on (https://github.com/funkensturm/ember-local-storage#methods)
and I have the following action:
actions: {
removeFromCart(){
this.get('cart').remove(this.product);
}
}
When I try to run the following code I get an error:
Uncaught Error: Assertion Failed: You attempted to update [object Object].ordered_quantity to "7", but it is being tracked by a tracking context, such as a template, computed property, or observer. In order to make sure the context updates properly, you must invalidate the property when updating it. You can mark the property as #tracked, or use #ember/object#set to do this.
I tried using the set function like this:
let updated = item.ordered_quantity - 1;
set(item, 'ordered_quantity', updated);
https://api.emberjs.com/ember/release/functions/#ember%2Fobject/set and the code worked with no errors as expected, but the value of my property ordered_quantity was not updated in the Local Storage.
You need to adapt the set function to your use-case:
remove(item) {
let new_arr = this.get('items');
let elementIndex = new_arr.findIndex(obj => {
return obj.id === item.id;
});
if (elementIndex !== -1) {
set(new_arr[elementIndex], 'ordered_quantity', new_arr[elementIndex].ordered_quantity - 1);
}
this.set('cart.items', new_arr);
}
Based of #Lux's original answer (and to also update for the Ember 3.16+ era), I wanted to add that you could use functional programming patterns to clean things up a little bit:
If in your cart Service, items is #tracked, like so:
import Service from '#ember/service';
import { tracked } from '#glimmer/tracking';
export default class CartService extends Service {
#tracked items = [];
}
Note: assumes that ordered_quantity is not tracked.
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
// set must be used because quantity is not known to the tracking system
set(obj, 'ordered_quantity', obj.ordered_quantity - 1);
});
// invalidate the reference on the card service so that all things that
// reference cart.items re-compute
this.cart.items = this.items;
}
Here is a live demo: https://ember-twiddle.com/e18433b851091b527512e27ae792640c?openFiles=components.demo%5C.js%2C
There is also a utility addon, which allows for more ergonomic interaction with arrays, objects, etc: https://github.com/pzuraq/tracked-built-ins
the above code would then look like this:
import Service from '#ember/service';
import { tracked } from '#glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';
export default class CartService extends Service {
#tracked items = new TrackedArray([]);
}
and then in your component:
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
// set must be used because quantity is not known to the tracking system
set(obj, 'ordered_quantity', obj.ordered_quantity - 1);
});
}
Here is a live demo: https://ember-twiddle.com/23c5a7efdb605d7b5fa9cd9da61c1294?openFiles=services.cart%5C.js%2C
You could then take it a step further, and add tracking to your "items".
so then your remove method would look like this:
remove(item) {
for (let obj of this.items) {
if (item.id !== obj.id) continue;
obj.ordered_quantity = obj.ordered_quantity - 1;
});
}
And here is a live demo: https://ember-twiddle.com/48c26e2e0f0e5f3ac7685e4bdc0eda4e?openFiles=components.demo%5C.js%2C
I would like to get into VueJs development and created a simple Minesweeper game.The two dimensional grid is managed by a Vuex state. When clicking on a cell I would like to reveal it so my current code is
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
state.board[rowIndex][columnIndex].isRevealed = true;
}
Unfortunately this has no affect to the UI. This problem is known and described here
https://v2.vuejs.org/v2/guide/list.html#Caveats
The docs told me to use something like this
import Vue from "vue";
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
const updatedCell = state.board[rowIndex][columnIndex];
updatedCell.isRevealed = true;
Vue.set(state.board[rowIndex], columnIndex, updatedCell);
Vue.set(state.board, rowIndex, state.board[rowIndex]);
}
but it did not help. Lastly I tried to create a copy of the board, modify the values and assign that copy to the board.
[MutationTypes.REVEAL_CELL]: (state, { rowIndex, columnIndex }) => {
const newBoard = state.board.map((row, mapRowIndex) => {
return row.map((cell, cellIndex) => {
if (mapRowIndex === rowIndex && cellIndex === columnIndex) {
cell = { ...cell, isRevealed: true };
}
return cell;
});
});
state.board = newBoard;
}
This didn't work neither. Does someone got an idea?
I created a Codesandbox showing my project
https://codesandbox.io/s/vuetify-vuex-and-vuerouter-d4q2b
but I think the only relevant file is /store/gameBoard/mutations.js and the function REVEAL_CELL
The problem is in Cell.vue and the issue is that you're checking an unchanging variable to determine the state of reveal. You've abstracted this.cell.isRevealed into a variable called isUnrevealed which is never told how to change after the initial load.
Option 1
isUnrevealed seems like an unnecessary convenience variable. If you get rid of isUnrevealed and change the references to it to !cell.isRevealed, the code will work as expected.
Option 2
If you're set on using this variable, change it to a computed so that it constantly updates itself whenever the Vuex state propagates a change to the cell isRevealed prop:
computed: {
isUnrevealed() {
return !this.cell.isRevealed;
}
}
If you go this route, don't forget to remove the property from data and remove the assignment in mounted (first line).
You'll also have the same problem with isMine and cellStyle. So, completely remove data and mounted and make them both computed as well.
computed: {
isMine() {
return this.cell.isMine;
},
cellStyle() {
if (!this.cell.isRevealed) {
return "unrevealedCell";
} else {
if (this.isMine) {
return "mineCell";
} else {
let neighbourCountStyle = "";
... // Switch statement
return `neutralCell ${neighbourCountStyle}`;
}
}
}
}
Is it possible to do the following?
export default {
props: ['parentArray'],
data () {
return {
computedArray: null
}
},
computed: {
computedResult: function () {
var flags = []
var output = []
var l = this.parentArray.length
var i
for (i = 0; i < l; i++) {
if (flags[this.parentArray[i].myitem]) continue
flags[this.parentArray[i].myitem] = true
var firstfilter = this.parentArray[i].myitem.replace('something', '')
output.push(firstfilter)
}
return output
}
},
mounted () {
this.computedArray = this.computedResult
}
When I try, the structure of my array makes it to the data element, but none of the data (at least not in the vue dev-tools. The computed array properly shows up in computed)
As is often the case, the unexpected result was because of something outside the logic of the particular question. In my case, my parentArray was coming from an axios call, which is async, which meant that it did not arrive until AFTER the component was mounted, and hence my empty array. Adding a check for loaded data (by setting data item loaded to false, then setting it to true in my axios response and finally adding v-if="loaded" to my component made it work as expected.
So the answer to the question above is YES, this is possible (BUT make sure your parent prop data is available before mounting).