VueJs - DOM not updating on array mutation - javascript

I have a model in which I'm initializing an array on ajax success after the model is mounted
var self = this;
$.getJSON("somejson.json",
function (data) {
var list = [];
list = data.list.map(function (item) {
return { id: item.id, text: item.text };
});
self.selectableItems = list;
});
I have a click method on each of these items which removes the item from selectableItems
select: function (item) {
this.selectableItems.pop(item);
},
selectableItems renders correctly initially, but when I mutate the array, the dom isn't updating. Although the actual array is being modified correctly.
I verified this by having a computed property that returns the count of selectableItems. This count is updated correctly when the item is removed, but the dom still shows the item.
I also noticed that when I hard code the value of selectableItems in the ajax, everything works as expected!
self.selectableItems = [{ id: 1, text: "adsad"}];
I'm aware of the caveats of array mutation in vue. But I feel I'm missing something basic here, as I have just started exploring Vue.
Can someone point out on what I'm missing?

Array.pop() removes the last item from the array, it does not take any argument. It only removes the last item any argument you pass it.
That the reason your computed property showing the array count works as last item is being removed but not the item you want.
Use Array.splice()instead.
pass the index to your click method like this:
<ul>
<li v-for="(item, index) in selectableItems" #click="select(index)>{{item}}</li>
</ul>
script
select: function (index) {
this.selectableItems.splice(index, 1);
},

Related

Can't get mutator logic to correctly add the proper amount of items into a cart

I'm trying to make an add to cart function that first checks if the item being added is already in the cart. If it's in the cart, update its quantity property. If not in the cart, add the entire object to the cart. I think my problem is I'm getting the logic wrong inside my "ADD_ITEM_TO_CART" mutator function.
This is my store with some console.logs() from when I click "addToCart()"
state: {
checkoutCart: [],
},
actions: {
cartAdd({ commit }, payload) {
commit("ADD_ITEM_TO_CART", payload);
},
},
mutations: {
ADD_ITEM_TO_CART(state, payload) {
//CONSOLE.LOG()'s
console.log("state.checkoutCart[0]", state.checkoutCart[0]);
// eslint-disable-next-line
console.log("state.checkoutCart[0].item", state.checkoutCart.item);
console.log("state.checkoutCart", state.checkoutCart);
//IF ITEM ALREADY IN checkoutCart, UPDATE IT'S QUANTITY
if (state.checkoutCart.includes(payload.item)) {
state.checkoutCart.quantity += payload.quantity;
console.log("Item already in cart");
}
//IF ITEM NOT IN checkoutCart, UPDATE THE QUANTITY PROPERTY AND ADD ITEM TO CART
else {
payload.item.quantity = payload.quantity;
state.checkoutCart.push(payload);
}
https://i.imgur.com/rjOOljN.png
I thought this code would work, but it ALWAYS executes the ELSE condition and adds to cart like the
if (state.checkoutCart.includes(payload.item))
isn't being recognized or working at all.
https://i.imgur.com/LLB790Z.png
VueX devtools shows the same thing. An "item" object inside an object inside an array.
I also tried:
ADD_ITEM_TO_CART(state, payload) {
console.log("add_item_to_cart"); <---ONLY PART THAT SHOWS UP IN CONSOLE.LOG() WHEN EXECUTED
//LOOP THROUGH ALL ARRAY ENTRIES TO GAIN ACCESS TO state.checkoutCart.item
for (let i = 0; i < state.checkoutCart.length; i++) {
console.log("i=", i);
console.log("state.checkoutCart.item", state.checkoutCart.item);
//IF ITEM ALREADY IN checkoutCart, UPDATE IT'S QUANTITY
if (state.checkoutCart[i].item.includes(payload.item)) {
state.checkoutCart.quantity += payload.quantity;
console.log("Item already in cart");
return;
};
}
//IF ITEM NOT IN checkoutCart, UPDATE THE QUANTITY PROPERTY AND ADD ITEM TO CART
payload.item.quantity = payload.quantity;
state.checkoutCart.push(payload);
},
because I figured I needed to loop through all the array entries. BUT the for loop doesn't even run, and with this code nothing gets added to the cart at all.
I can't figure out what I'm doing wrong here. Can somebody help? Is my syntax wrong? Or is my logic? Am I accessing the arrays/objects incorrectly? How do I write the "ADD_ITEM_TO_CART" mutator function correctly? I've literally spent all day on this and my brain is shutting down.
EDIT:
https://i.imgur.com/bkU8YSo.png
PAYLOAD
<div v-for="item in items"> <--ACTUALLY PROP FROM PARENT COMPONENT BUT SAME IDEA
<p>
Qty
<select v-model="quantity">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</p>
<p>
<button type="button" #click="addToCart()">
Add to Cart
</button>
</p>
</div>
let quantity = ref("1");
const addToCart = () => {
console.log("addToCart Running");
store.dispatch("cartAdd", { item: item.value, quantity: quantity.value });
};
That is because your if condition is not checking for what you think.
Array.prototype.includes checks if a value is in the array but there are two cases:
the value is a primary type (string, number, boolean, ...). It compares by value.
the value is an object. Then it compares by reference.
So here, you are checking if the reference of your item object is already included in the array. But it's not, since it's a new object.
Solution: check if there is an object with the same values, not reference.
You can use the some method, and you have to write a condition that checks if two items are equals.
Here is an example if your items have an id:
if (state.checkoutCart.some(item => item.id === payload.item.id))
The problem is indeed inside ADD_ITEM_TO_CART mutation.
As Kapcash has pointed out, two objects having the same properties and the same values are not the same.
In other words, .includes() checks for identity, not equality. To better understand this, consider this example:
const a = { foo: 'bar' }
const b = [{ foo: 'bar' }]
const c = [a]
const d = [{ ...a }]
console.log(b.includes(a)) // false
console.log(c.includes(a)) // true
console.log(d.includes(a)) // false
To get past this, use Kapcash's answer.
I'll just mention the standard way of dealing with this problem is using unique identifiers on objects (e.g: uuids).
Once you fix the above, it's still not going to work, because you'll run into the following problem: inside the if you're attempting to alter state.checkoutCart's quantity. And an array does not have a quantity property.
The proper logic to achieve the desired functionality is (assuming the unique identifier on checkoutCart members is item._id, from the pictures you posted):
ADD_ITEM_TO_CART(state, payload) {
// find the index of a cart element having same `item._id` value as the payload
const index = state.checkoutCart.findIndex(
(el) => el.item._id === payload.item._id
)
if (index > -1) {
// if the index is higher than `-1`, an element was found
// create a copy, update its quantity and then
// replace the original element with the copy
const copy = { ...state.checkoutChart[index] }
copy.quantity += payload.quantity
state.checkoutCart.splice(index, 1, copy)
} else {
// no element found, add payload to `checkoutCart`
state.checkoutCart.push(payload)
}
}
Side note: None of your items should contain a quantity property. That's the cart's responsibility, not the item's. By adding it to the item you end up with two sources of truth which you'd need to keep in sync. You don't want this type of problem.

Misunderstanding `each...in` rendering with reactive variable

I'm new to Meteor/Blaze but that is what my company is using.
I'm struggling to understand how Blaze decides to render what based on ReactiveDict
TL;DR
I create some children templates from a ReactiveDict array in the parent array. The data is not refreshed in the children templates when the ReactiveDict changes and I don't understand why.
I probably have misunderstood something about Blaze rendering. Could you help me out?
Parent
Parent Html template
<template name="parent">
{{#each child in getChildren}}
{{> childTemplate (childArgs child)}}
{{/each}}
</template>
Parent Javascript
Reactive variable
The template renders children templates from a getChildren helper that just retrieves a ReactiveDict.
// Child data object
const child = () => ({
id: uuid.v4(),
value: ""
});
// Create a reactive dictionary
Template.parent.onCreated(function() {
this.state = new ReactiveDict();
this.state.setDefault({ children: [child()] });
});
// Return the children from the reactive dictionary
Template.parent.helpers({
getChildren() {
return Template.instance().state.get('children');
}
});
Child template arguments (from parent template)
The parent template gives the child template some data used to set default values and callbacks.
Each is instantiated using a childArgs function that uses the child's id to set the correct data and callbacks.
When clicking a add button, it adds a child to the children array in the ReactiveDict.
When clicking a delete button, it removes the child from the children array in the ReactiveDict.
Template.parent.helpers({
// Set the children arguments: default values and callbacks
childArgs(child) {
const instance = Template.instance();
const state = instance.state;
const children = state.get('children');
return {
id: child.id,
// Default values
value: child.value,
// Just adding a child to the reactive variable using callback
onAddClick() {
const newChildren = [...children, child()];
state.set('children', newChildren);
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick(childId) {
const childIndex = children.findIndex(child => child.id === childId);
children.splice(childIndex, 1);
state.set('children', children);
}
}
}
})
Child
Child html template
The template displays the data from the parent and 2 buttons, add and delete.
<template name="child">
<div>{{value}}</div>
<button class="add_row" type="button">add</button>
<button class="delete_row" type="button">delete</button>
</template>
Child javascript (events)
The two functions called here are the callbacks passed as arguments from the parent template.
// The two functions are callbacks passed as parameters to the child template
Template.child.events({
'click .add_row'(event, templateInstance) {
templateInstance.data.onAddClick();
},
'click .delete_row'(event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id);
},
Problem
My problem is that when I delete a child (using a callback to set the ReactiveDict like the onAddClick() function), my data is not rendered correctly.
Text Example:
I add rows like this.
child 1 | value 1
child 2 | value 2
child 3 | value 3
When I delete the child 2, I get this:
child 1 | value 1
child 3 | value 2
And I want this:
child 1 | value 1
child 3 | value 3
I'm initialising the child with the data from childArgs in the Template.child.onRendered() function.
Good: The getChildren() function is called when deleting the child in the ReactiveDict and I have the correct data in the variable (children in the ReactiveDict).
Good: If I have 3 children and I delete one, the parent template renders only 2 children.
Bad: Yet the child's onRendered() is never called (neither is the child's onCreated() function). Which means the data displayed for the child template is wrong.
Picture example
I am adding pictures to help understand:
Correct html
The displayed HTML is correct: I had 3 children, and I deleted the second one. In my HTML, I can see that the two children that are displayed have the correct ID in their divs. Yet the displayed data is wrong.
Stale data
I already deleted the second child in the first picture. The children displayed should be the first and the third.
In the console log, my data is correct. Red data is the first. Purple is the third.
Yet we can see that the deleted child's data is displayed (asd and asdasd). When deleting a tag, I can see the second child's ID in the log, though it should not exist anymore. The second child ID is in green.
I probably have misunderstood something. Could you help me out?
I am not sure where to start but there are many errors and I rather like to provide a running solution here with comments.
First the each function should correctly pass the id instead of the whole child or the find will result in faults:
<template name="parent">
{{#each child in getChildren}}
{{#with (childArgs child.id)}}
{{> childTemplate this}}
{{/with}}
{{/each}}
</template>
In the helper you can avoid calling too many of the Template.instance() functions by using lexical scoping:
childArgs (childId) {
const instance = Template.instance()
const children = instance.state.get('children')
const childData = children.find(child => child.id === childId)
const value = {
// Default values
data: childData,
// Just adding a child to the reactive variable using callback
onAddClick () {
const children = instance.state.get('children')
const length = children ? children.length : 1
const newChild = { id: `data ${length}` }
const newChildren = [...children, newChild]
instance.state.set('children', newChildren)
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick (childId) {
const children = instance.state.get('children')
const childIndex = children.findIndex(child => child.id === childId)
children.splice(childIndex, 1)
instance.state.set('children', children)
}
}
return value
}
Then note, that in the event callback 'click .delete_row' you are using templateInstance.data.id but this is always undefined with your current structure. It should be templateInstance.data.data.id because data is always defined for all data coming in a Template instance and if you name a property data then you have to access it via data.data:
Template.childTemplate.events({
'click .add_row' (event, templateInstance) {
templateInstance.data.onAddClick()
},
'click .delete_row' (event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id)
}
})
Now it also makes sense why your data was weirdly sorted. Take a look at the onDeleteClick callback:
onDeleteClick (childId) {
// childId is always undefined in your code
const children = instance.state.get('children')
const childIndex = children.findIndex(child => child.id === childId)
// childIndex is always -1 in your code
// next time make a dead switch in such situations:
if (childIndex === -1) {
throw new Error(`Expected child by id ${childId}`)
}
children.splice(childIndex, 1)
// if index is -1 this removes the LAST element
instance.state.set('children', children)
}
So your issue was the splice behavior and passing an unchecked index into splice:
The index at which to start changing the array. If greater than the length of the array, start will be set to the length of the array. If negative, it will begin that many elements from the end of the array (with origin -1, meaning -n is the index of the nth last element and is therefore equivalent to the index of array.length - n). If array.length + start is less than 0, it will begin from index 0.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
I fixed my problem. But I still don't understand how Blaze chooses to render.
Now, the solution looks a bit like the one given by #Jankapunkt in the first part of his solution, but not exactly. The find to get the child was working completely fine. But now that I make the template rendering dependent on a reactive helper, it re-renders the template when the id changes (which it did not when it was only dependent on the child itself from the each...in loop).
In the end, I don't understand what the each...in loop does and how it uses the data to loop. See Caveats.
To give credits where it's due, I had the idea of implementing that dependency from this post.
Edits from the original code
I edit the parent template to make the child rendering dependent on its own id. That way, when the child.id changes, the template re-renders.
Html template
I added a dependency on the child.id to re-render the child template.
<template name="parent">
{{#each childId in getChildrenIds}}
{{#let child=(getChild childId)}}
{{> childTemplate (childArgs child)}}
{{/let}}
{{/each}}
</template>
Javascript
I have now two helpers. One to return the ids for the each...in loop, the other to return the child from the id and force the child template re-render.
Template.parent.helpers({
// Return the children ids from the reactive dictionary
getChildrenIds() {
const children = Template.instance().state.get('children');
const childrenIds = children.map(child => child.id);
return childrenIds;
},
// Return the child object from its id
getChild(childId) {
const children = Template.instance().state.get('children');
const child = children.find(child => child.id === childId);
return child;
}
});
Complete Code
Here is the complete solution.
Parent
Html template
<template name="parent">
{{#each childId in getChildrenIds}}
{{#let child=(getChild childId)}}
{{> childTemplate (childArgs child)}}
{{/let}}
{{/each}}
</template>
Javascript
// Child data object
const child = () => ({
id: uuid.v4(),
value: ""
});
// Create a reactive dictionary
Template.parent.onCreated(function() {
this.state = new ReactiveDict();
this.state.setDefault({ children: [child()] });
});
Template.parent.helpers({
// Return the children ids from the reactive dictionary
getChildrenIds() {
const children = Template.instance().state.get('children');
const childrenIds = children.map(child => child.id);
return childrenIds;
},
// Return the child object from its id
getChild(childId) {
const children = Template.instance().state.get('children');
const child = children.find(child => child.id === childId);
return child;
},
// Set the children arguments: default values and callbacks
childArgs(child) {
const instance = Template.instance();
const state = instance.state;
const children = state.get('children');
return {
id: child.id,
// Default values
value: child.value,
// Just adding a child to the reactive variable using callback
onAddClick() {
const newChildren = [...children, child()];
state.set('children', newChildren);
},
// Just deleting the child in the reactive variable in the callback
onDeleteClick(childId) {
const childIndex = children.findIndex(child => child.id === childId);
children.splice(childIndex, 1);
state.set('children', children);
}
}
}
});
Child
Html template
<template name="child">
<div>{{value}}</div>
<button class="add_row" type="button">add</button>
<button class="delete_row" type="button">delete</button>
</template>
Javascript
Template.child.events({
'click .add_row'(event, templateInstance) {
templateInstance.data.onAddClick();
},
'click .delete_row'(event, templateInstance) {
templateInstance.data.onDeleteClick(templateInstance.data.id);
}
});
Caveats
The solution is working. But my each..in loop is weird.
When I delete a child, I get the correct IDs when the getChildrenIds() helper is called.
But the each..in loops over the original IDs, even those who were deleted and are NOT in the getChildrenIds() return value. The template is not rendered of course because the getChild(childId) throws an error (the child is deleted). The display is then correct.
I don't understand that behaviour at all. Anybody knows what is happening here?
If anybody has the definitive answer, I would love to hear it.
Correct way of solving this issue
The correct way to fix this is to create your own _id which gets a new unique _id each time the array of objects changes. It is outlined in the Blaze docs here: http://blazejs.org/api/spacebars.html#Reactivity-Model-for-Each
This will only happen when you are dealing with #each blocks with non-cursors, like arrays or arrays of objects.
Cursor-based data together with #each blocks works fine and gets rerendered correctly, like Pages.findOne(id).
Examples if you need to deal with arrays and #each blocks
Not working
[
{
name: "Fred",
value: 1337
},
{
name: "Olga",
value: 7331
}
]
Working
[
{
name: "Fred",
value: 1337,
_id: "<random generated string>"
},
{
name: "Olga",
value: 7331,
_id: "<random generated string>"
}
]

v-model not update after settimeout finish Vue [duplicate]

I'm new to Vuejs. Made something, but I don't know it's the simple / right way.
what I want
I want some dates in an array and update them on a event. First I tried Vue.set, but it dind't work out. Now after changing my array item:
this.items[index] = val;
this.items.push();
I push() nothing to the array and it will update.. But sometimes the last item will be hidden, somehow... I think this solution is a bit hacky, how can I make it stable?
Simple code is here:
new Vue({
el: '#app',
data: {
f: 'DD-MM-YYYY',
items: [
"10-03-2017",
"12-03-2017"
]
},
methods: {
cha: function(index, item, what, count) {
console.log(item + " index > " + index);
val = moment(this.items[index], this.f).add(count, what).format(this.f);
this.items[index] = val;
this.items.push();
console.log("arr length: " + this.items.length);
}
}
})
ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="app">
<ul>
<li v-for="(index, item) in items">
<br><br>
<button v-on:click="cha(index, item, 'day', -1)">
- day</button>
{{ item }}
<button v-on:click="cha(index, item, 'day', 1)">
+ day</button>
<br><br>
</li>
</ul>
</div>
EDIT 2
For all object changes that need reactivity use Vue.set(object, prop, value)
For array mutations, you can look at the currently supported list here
EDIT 1
For vuex you will want to do Vue.set(state.object, key, value)
Original
So just for others who come to this question. It appears at some point in Vue 2.* they removed this.items.$set(index, val) in favor of this.$set(this.items, index, val).
Splice is still available and here is a link to array mutation methods available in vue link.
VueJS can't pickup your changes to the state if you manipulate arrays like this.
As explained in Common Beginner Gotchas, you should use array methods like push, splice or whatever and never modify the indexes like this a[2] = 2 nor the .length property of an array.
new Vue({
el: '#app',
data: {
f: 'DD-MM-YYYY',
items: [
"10-03-2017",
"12-03-2017"
]
},
methods: {
cha: function(index, item, what, count) {
console.log(item + " index > " + index);
val = moment(this.items[index], this.f).add(count, what).format(this.f);
this.items.$set(index, val)
console.log("arr length: " + this.items.length);
}
}
})
ul {
list-style-type: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.11/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="app">
<ul>
<li v-for="(index, item) in items">
<br><br>
<button v-on:click="cha(index, item, 'day', -1)">
- day</button> {{ item }}
<button v-on:click="cha(index, item, 'day', 1)">
+ day</button>
<br><br>
</li>
</ul>
</div>
As stated before - VueJS simply can't track those operations(array elements assignment).
All operations that are tracked by VueJS with array are here.
But I'll copy them once again:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
During development, you face a problem - how to live with that :).
push(), pop(), shift(), unshift(), sort() and reverse() are pretty plain and help you in some cases but the main focus lies within the splice(), which allows you effectively modify the array that would be tracked by VueJs.
So I can share some of the approaches, that are used the most working with arrays.
You need to replace Item in Array:
// note - findIndex might be replaced with some(), filter(), forEach()
// or any other function/approach if you need
// additional browser support, or you might use a polyfill
const index = this.values.findIndex(item => {
return (replacementItem.id === item.id)
})
this.values.splice(index, 1, replacementItem)
Note: if you just need to modify an item field - you can do it just by:
this.values[index].itemField = newItemFieldValue
And this would be tracked by VueJS as the item(Object) fields would be tracked.
You need to empty the array:
this.values.splice(0, this.values.length)
Actually you can do much more with this function splice() - w3schools link
You can add multiple records, delete multiple records, etc.
Vue.set() and Vue.delete()
Vue.set() and Vue.delete() might be used for adding field to your UI version of data. For example, you need some additional calculated data or flags within your objects. You can do this for your objects, or list of objects(in the loop):
Vue.set(plan, 'editEnabled', true) //(or this.$set)
And send edited data back to the back-end in the same format doing this before the Axios call:
Vue.delete(plan, 'editEnabled') //(or this.$delete)
One alternative - and more lightweight approach to your problem - might be, just editing the array temporarily and then assigning the whole array back to your variable. Because as Vue does not watch individual items it will watch the whole variable being updated.
So you this should work as well:
var tempArray[];
tempArray = this.items;
tempArray[targetPosition] = value;
this.items = tempArray;
This then should also update your DOM.
Observe object and array reactivity here:
https://v2.vuejs.org/v2/guide/reactivity.html

list an object in (Angular.js)

I am generating a list to search for the key "name" and "type".
results.push({ name: item.beast, type: 'color of animal' });
but I see this error to find an element that is contained in the array $scope.data:
Error: [$ rootScope: infdig] $ 10 digest () iterations reached. Aborting! Watchers fired in the last five iterations.
This is the code that I have:
http://plnkr.co/edit/EDd578?p=preview
The problem here is that you're using a set of data to filter against but trying to display a resulting data set from that filtering process that's in a different format. I'd advocate using ng-change on the input and using a new data set to fill the repeated items.
controller
$scope.matches = [];
$scope.findMatches = function(items, searchText) {
var results = [];
if (searchText) {
angular.forEach(items, function(item) {
if (item.beast.indexOf(searchText) === 0) {
results.push({
name: item.beast,
type: 'animal'
});
}
if (item.color.indexOf(searchText) === 0) {
results.push({
name: item.color,
type: 'color of animal'
});
}
});
}
return results;
}
html
<input type='text' ng-model='search' id='search' ng-change="matches = findMatches(data, search)">
<hr/>
<ul>
<li ng-repeat="item in matches track by $index">{{item.name}} and {{item.type}}</li>
</ul>
plunkr - http://plnkr.co/edit/hkMXPP?p=preview
You are creating a new array everytime your filter is run, and returning that. This makes angular think you've changed the array everytime (it doesn't check for item equality, rather, reference equality by ===).
Have a look at this for more details.
A solution is to modify the items array inplace, and return it, so the reference remains the same.

Update Position Of Observable Array With New Item In Knockout.js

I have a situation where I need to replace a certain item in an observable array at a certain position of it. Right now I am doing it below with the slice method. Is there a better way that is built in to knockout.js to do this at a certain position? I was even thinking about doing a push, and then do a sort on that row with a order property but I have lots of rows and thought that was to much.
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
self.list.remove(self.game);
self.list.splice(position, 0, newGame);
}
Code With Replace, Trying To Update Property Matchup That Has A New Property Called Name
var game = self.game;
if (game) {
var position = ko.utils.arrayIndexOf(self.list(), game);
if (position != -1) {
if (game.Matchup) {
game.Matchup = new Matchup(response.Data);
game.Modified(true);
}
else if (self.game) {
game = new Matchup(response.Data);
}
self.list.replace(self.list()[position], game);
}
}
HTML
<!-- ko foreach: Games -->
<td class="item-container draggable-item-container clearfix">
<div class="item clearfix draggable-active draggable-item" data-bind="draggableCss: { disabled: $data.Disabled(), matchup: $data.Matchup }, draggableGameHandler : { disabled: !$data.Matchup, disabledDrop: $data.Disabled() }, delegatedClick: $root.members.eventSchedule.editGame.open.bind($data, true, ($data.Matchup && $data.Matchup.Type == '#((int)ScheduleType.Pool)'), $parent.Games)">
<span data-bind="if: $data.Matchup">
<span data-bind="attr: { title: Matchup.Title }"><span data-bind="html: Matchup.Name"></span></span>
</span>
</div>
</td>
<!-- /ko -->
data-bind="html: Matchup.Name" doesn't update with replace.
Replacing an item in an observable array
The replace method is one option for replacing an item in an observable array. In your case, you could call it like this:
list.replace(game, newGame);
Bindings update when an observable dependency changes
But your question isn't only about replacing an item in an array. You've stated that the binding html: Matchup.Name isn't updated, so let's look at what could cause it to update:
If Name is an observable, modifying Name will cause an update.
If Matchup is an observable, modifying it will cause an update, but then you'd have to bind it like Matchup().Name and update it like game.Matchup(Matchup(response.Data));.
Replacing the entry in the observable array (is it Games or list?) with a new object will cause the whole inner template to re-render, obviously replacing each binding.
Looking through your code, I can see that in one case (if (game.Matchup)), none of these three things happen, and thus there's no way Knockout can know to update the binding. The first two obviously aren't occurring and although you do call replace on the array, it's the equivalent of this:
list.replace(game, game); // replace the item with itself
The foreach binding doesn't see the above as a change and doesn't update anything. So, to update the binding, you need to make a real update to an observable.
Further comments
To get the index of an item in an observable array, use the indexOf method:
var position = self.list.indexOf(game);
To replace an item at a specific index, use the splice method with a second parameter of 1:
self.list.splice(position, 1 /*how many to remove*/, newGame);
Observable arrays have a built in .replace method you can use to do this:
var position = ko.utils.arrayIndexOf(self.list(), game);
self.list.replace(self.list()[position], game);

Categories

Resources