The Problem
I have a "products" array with multiple objects. Each product object contains the property "price". I want to watch this property in each product for possible changes. I am using this to calculate a commission price when the user changes the price in an input box.
My products array looks like this;
[
0: {
name: ...,
price: ...,
commission: ...,
},
1: {
name: ...,
price: ...,
commission: ...,
},
2: {
name: ...,
price: ...,
commission: ...,
},
...
...
...
]
My code
I've tried this, but it doesn't detect any changes except for when the products are first loaded;
watch : {
// Watch for changes in the product price, in order to calculate final price with commission
'products.price': {
handler: function (after, before) {
console.log('The price changed!');
},
deep : true
}
},
The products are loaded like this;
mounted: async function () {
this.products = await this.apiRequest('event/1/products').then(function (products) {
// Attach reactive properties 'delete' & 'chosen' to all products so these can be toggled in real time
for (let product of products) {
console.log(product.absorb);
Vue.set(product, 'delete', false);
Vue.set(product, 'chosen', product.absorb);
}
console.log(products);
return products;
})
}
Other questions I've looked at
Vue.js watching deep properties
This one is trying to watch a property that does not yet exist.
VueJs watching deep changes in object
This one is watching for changes in another component.
You can't really deep-watch products.price, because price is a property of individual product, not the products array.
Declarative watchers are problematic with arrays, if you attempt to use an index in the watch expression, e.g products[0].price, you get a warning from Vue
[Vue warn]: Failed watching path: “products[0].price”. Watcher only accepts simple dot-delimited paths. For full control, use a function instead.
What this means is you can use a programmatic watch with a function, but it's not explained all that well.
Here is one way to do it in your scenario
<script>
export default {
name: "Products",
data() {
return {
products: []
};
},
mounted: async function() {
this.products = await this.apiRequest('event/1/products')...
console.log("After assigning to this.products", this.products);
// Add watchers here, using a common handler
this.products.forEach(p => this.$watch(() => p.price, this.onPriceChanged) );
// Simulate a change
setTimeout(() => {
console.log("Changing price");
this.products[0].price= 100;
}, 1000);
},
methods: {
onPriceChanged(after, before) {
console.log(before, after);
}
}
};
</script>
Here is my test Codesandbox (I use color instead of price because there's no price in the test api)
Related
I am developing a component where I will get the data from a call back function. Initially the state of the component will be empty [], later once the callback function is called I need to update the values into the state. At a time I'll recive only one array, meaning user can add one item at a time that item will consists of nested objects and array values. I have added the logic for the same to handle the scenario, but when I am testing in jest when I am trying to add another set of item from mock meaning the user can select next item when the done with selecting and submitting the first item at that time my logic is getting failed, I am not getting where I went wrong, could any one help me to resolve this issue, thanks in advance! I have added the mock data structure and logic and jest test below.
Mock:
const items = {
itemList: {
itemOne: [{
id: "01",
category: "It-A",
isCreated:"true"
}],
itemDesc:[{
id:"01",
type:"A-1",
isCreated:"true"
}]
}
ItemID:'123'
}
Code:
class ItemComp extends React.Component{
this.state = {
processingItems:[]
onAddItemHandle = (processingItem) => {
this.setState(prevState => ({
processingItems: [...prevState.processingItems, processingItem]
}))
}
JEST:
describe('handleonAddItem', () => {
it('should allow to add multiple items based on prevState', () => {
const compView = mountWithIntl(
<compView
itemId={12}
/>
}
const instance = compView.find(compViewComponent).instance();
instance.onAddItemHandle(items) // when I am giving only one instance my logic is working
instance.onAddItemHandle(items) //when I am giving it for second time it's failing I am getting error like expected - 0 , received +18 I want to update the items here when user clicks for second time but it is failing.
expect(instance.state.processingItems).toEqual([items])
Missing a ',' before the ItemID is the only issue I faced while reproducing.- https://codesandbox.io/s/intelligent-chaplygin-0ot56e?file=/src/App.js
const items = {
itemList: {
itemOne: [{
id: "01",
category: "It-A",
isCreated:"true"
}],
itemDesc:[{
id:"01",
type:"A-1",
isCreated:"true"
}]
},
ItemID:'123'
}
I've got my <MockedProvider /> set up passing in mocks={mocks}. everything is working, all good.
the issue is I have a form that whenever any part of it is edited then this makes a mutation, which returns a response and updates the total. say for example, quantity is changed, mutation increases quantity from 1 to 2. total price should double
problem is that in unit tests and mocked provider you only test the functionality in props and hardcoded response. it's not a proper test. perhaps it's more of an e2e/integration test but I was wondering if there's anything you can do with MockedProvider that allows for better testing in this situation?
Instead of using the normal static result property of the objects in the mocks array, you can set a newData function that will run dynamically and use whatever is returned as the result value. For example:
let totalNoteCount = 0;
const mocks = [{
request: {
query: CREATE_NOTE,
variables: {
title: 'Aloha!',
content: 'This is a note ...',
},
},
newData: () => {
// do something dynamic before returning your data ...
totalNoteCount += 1;
return {
data: {
createNote: {
id: 1,
totalNoteCount,
},
},
};
}
}];
It seems to me that I found some sort of bug. Basically I want to get components object by index (for the tag). But I've experienced a strange issue. I've included necessary pieces of my code below:
Working example:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
currentStep: steps[0] // When specifying index without a variable
}
},
}
Broken example:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
currentStep: steps[this.step] // When specifying index by a variable
}
},
}
In working example I am getting component (as expected), but in broken I am getting currentStep: undefined in Vue DevTools. However, no errors in the console. What am I doing wrong?
Your best bet is to move the currentStep to a computed property. Also, steps need to exist in data so they are reactive:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
steps,
}
},
computed: {
currentStep() {
return this.steps[this.step];
}
}
}
If possible, prefer to stick the steps directly in data:
data() {
return {
step: 0,
steps: ['Handler', 'Categories', 'Finalize'];,
}
},
(But that may not be possible if you're importing them from the outside. I don't know about your specific use case).
In general, in Vue, when something directly depends on the value of some component properties, computed properties are the way to go: they are performant and clear.
In your original code, should it have worked, currentStep would not react to a change in step. Using a computed property, instead, whenever the step updates, the currentStep will update accordingly.
Suppose I have an array feedsArray, the example value may look like this:
this.feedsArray = [
{
id: 1,
type: 'Comment',
value: 'How are you today ?'
},
{
id: 2,
type: 'Meet',
name: 'Daily sync up'
}
]
Suppose I have registered two components: Comment and Meet, Each component has a prop setting as the following:
props: {
feed: Object
}
and the main component has the following definition:
<component v-for="feed in feedsArray" :feed="feed" :key="feed.id" :is="feed.type"></component>
As you can see, it uses is property to select different component. My question is, how to detect feed object change in the child component ? Like when I set
this.feedsArray[0] = {
id: 1,
type: 'Comment',
value: 'I am not ok'
}
How can the Comment component detect the changes ? I tried to add a watcher definition in the child component like the following:
watch: {
feed: {
handler (val) {
console.log('this feed is changed')
},
deep: true
}
},
But it doesn't work here. Anyone know how to solve this ?
Do not assign directly to an array using index - use splice() instead, otherwise JavaScript can not detect that you have changed the array.
If you want to change only the value of an already existing key of an object - then simply update it e.g. this.feeds[0].value = 'I am not okay any more';
This works for existing keys only - otherwise you have to use this.$set(this.feeds[0], 'value', 'I am not okay any more');
I want to let the user create the structure of my website. For example, I have buildings and rooms. The user must be able to create a building and subsequently insert rooms into it. However, what I tried to do seems not to achieve it:
JSFiddle of what I have done so far.
js
new Vue({
el: '#vue-app',
data: {
buildings: []
},
computed: {
buildingCount() {
return this.buildings.length
},
getBuildingRoomsLength(section) {
return this.buildings.rooms.length
}
},
methods: {
addNewRoomToBuilding(buildingId, newRoom) {
if(newRoom !== undefined) { this.buildings[parseInt(buildingId)-1].rooms.push(newRoom.title)
console.log(this.buildings[parseInt(buildingId)-1])
}
},
addNewBuilding() {
this.buildings.push({
id: this.buildings.length+1,
rooms: []
})
},
deleteTodo(todo) {
this.todos.$remove(todo)
}
}
});
I am not sure how to make it work. A couple of the things I have noticed is that the room model now is same for all buildings and I have to change it according to the buildingId, however, I can't figure it out yet. Could you please assist me how to do this.
Make your model unique for each item in the buildings array by appending the building id to the end of the name.
So the model name becomes v-model="newRoom[building.id]"
And pass the same into your method addNewRoomToBuilding(building.id, newRoom[building.id])