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

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);

Related

angular checkboxes with 2 dimensional array

I have a template:
<mat-card *ngFor="let cargo of cargos" class="cont-mat">
/*...*/
<form [formGroup]="cargoSignupForm" (submit)="onSignupForCargo(cargo.id)" *ngIf="!isLoading">
<p>Odaberite kamione s kojima želite izvršiti prijevoz - Težina robe: {{cargo.weight}} kg</p>
<div *ngFor="let truck of (trucksByCargoId.get(cargo.id))">
<input type="checkbox" (change)="onChange(truck._id, $event.target.checked, cargo.id)">{{truck.regNumber}}
</div>
<input type="submit" value="GO" class="button-basic">
</form>
and 2 component functions:
truckFormArray = [[]];
ngOnInit() {
/*...*/
this.cargoSignupForm = new FormGroup({
trucksId: this.fb.array([])
});
/*...*/
}
onChange(truckId: string, isChecked: boolean, cargoId: string) {
this.truckFormArray[cargoId] = <FormArray>this.cargoSignupForm.controls.trucksId;
if (isChecked) {
this.truckFormArray[cargoId].push(new FormControl(truckId));
} else {
let index = this.truckFormArray[cargoId].controls.findIndex(x => x.value == truckId)
this.truckFormArray[cargoId].removeAt(index);
}
}
onSignupForCargo(cargoId: string) {
console.log(this.truckFormArray[cargoId]);
}
I just want to console_log(this.truckFormArray[cargoId]). There must be different truckFormArray for every cargoId. With that solution I'm getting trucksFormArray from previus cargoId checkboxes also. I hope you understand what I mean. Somewhere is a small mistake, but also if you think there is a better solution to do that you are welcome. Thanks in advance
truckFormArray should be an object
It is safe to assume that cargoId is not a sequentially increasing number starting from 0, hence there is no sense in declaring it as an array, declare it as an object instead:
truckFormArray = {};
Reason: Arrays in Javascript are always indexed by numbers starting from 0 and increasing sequentially until the last index.
truckFormArray is not a data member of the instance of your object
Since it is not initialized as this.truckFormArray, you do not have the this in front of it. So change all occurrences of this.truckFormArray to truckFormArray.
Reason: You will always need to be consistent when you refer to your resources.
Initializing truckFormArray
You have
this.truckFormArray[cargoId] = <FormArray>this.cargoSignupForm.controls.trucksId;
and it seems to be incorrect. Your trucksId seems to be a number and you try to assign it to a resource which is treated as an array, so there is a type discrepancy. If you want to store each trucksId into an array identified by the cargo that is present on the trucks, then you need to do a push, but only if the checkbox is checked. So, instead of the above, you will need something like this:
if (!truckFormArray[cargoId]) {
//Here I created a JS array for the sake of simplicity, but you can
//create a FormArray instance instead if that's more helpful for you
this.truckFormArray[cargoId] = [];
}
Reason: if the array you need to refer to does not exist yet, then you need to create it.
Summary
You will need to
fix the initialization of truckFormArray
ensure that you refer it consistently with the way it is defined
initialize each of its cargo array when needed

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

VueJs - DOM not updating on array mutation

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);
},

How do I reverse the order of an array using v-for and orderBy filter in Vue JS?

I am using Vue JS to do viewmodel bindings. In my data object I have an array of items that are sorted in ascending order (oldest to newest) and I'd like to keep it that way for code-based reasons.
var v = new Vue({
el: '#app',
data: {
items: [
{id: 51, message: 'first'},
{id: 265, message: 'second'},
{id: 32, message: 'third'}
],
}
}
However, when I display the array in the template I'd like to reverse the order so that it's descending (newest to oldest). I tried the following:
<ol>
<li v-for="item in items | orderBy -1" track-by="id">
This didn't work since the orderBy filter seems to require a field name as its first argument.
Is there any way to accomplish this in the template using the v-for syntax using the orderBy filter? Or am I going to have to create a custom reverse filter?
Simple and concise solution:
<li v-for="item in items.slice().reverse()">
//do something with item ...
</li>
Instead of reversing the order of the elements for creation, I only change the order of the display.
<ol class="reverseorder">
<li v-for="item in items" track-by="id">
And my CSS
<style>
.reverseorder {
display: flex;
flex-direction: column-reverse;
}
</style>
No need to clone the array and reverse it.
Note: The below works in Vue 1, but in Vue 2 filters are deprecated and you
will see: ' Property or method "reverse" is not defined on the
instance but referenced during render.' See tdom_93's answer for
vue2.
You could create a custom filter to return the items in reversed order:
Vue.filter('reverse', function(value) {
// slice to make a copy of array, then reverse the copy
return value.slice().reverse();
});
Then use it in the v-for expression:
<ol>
<li v-for="item in items | reverse" track-by="id">
https://jsfiddle.net/pespantelis/sgsdm6qc/
Update for Vue2
I want to show some ways that you can work with data and not using filters as they are deprecated in Vue2:
inside computed property
Use computed properties in place of filters, which is much better because you can use that data everywhere in component, not only just in template:
jsFiddle
computed: {
reverseItems() {
return this.items.slice().reverse();
}
}
inside Vuex getter property
If you're using Vuex, and you store your data in store.state object. The best way do some transformation with data stored in state is to do that in getters object (for example filtering through a list of items and counting them, reverse order and so on...)
getters: {
reverseItems: state => {
return state.items.slice().reverse();
}
}
and retrieve state from getters in component computed property:
computed: {
showDialogCancelMatch() {
return this.$store.state.reverseItems;
}
}
Possibly I'm missing some downsides here, but how about iterating over the array from end to start using an index?
<ol>
<li v-for="i in items.length" :set="item = items[items.length - i]">
Like, if your array consists of thousands of elements, copying it with .slice().reverse() every time is probably not the most efficient approach.
Upd.: note, :set is not an official way for defining variables in template, it just works. As an alternative, the item variable could be replaced by a call to some getItem(i) method that would encapsulate the items[items.length - i] expression.
Based on the fact that the directive v-for can accept not only an array but also any other valid JavaScript iterable object (at least in Vue 2.6+ and Vue 3 releases), we can create our own iterable object to loop through a needed array in the opposite direction. I created a very simplified runnable example (for more details - check information about the JavaScript iterator protocol).
class Iterable {
constructor(arr) {
this.arr = arr;
}
*[Symbol.iterator]() {
const arr = this.arr;
for (let i = arr.length - 1; i >= 0; i--) yield arr[i];
}
getIterable(isReversedOrder) {
return isReversedOrder ? this : this.arr;
}
}
Vue.component('list', {
props: ['iterable'],
template: '<ul><li v-for="(el, i) in iterable" :key="`${i}-${el}`">{{ el }}</li></ul>'
});
const app = new Vue({
data() {
return {
todos: new Iterable(['Learn JavaScript', 'Learn Vue', 'Learn Vuex']),
isReversed: true,
inputValue: ''
};
},
computed: {
computedTodos() {
return this.todos.getIterable(this.isReversed);
}
},
methods: {
toggleReverse() {
this.isReversed = !this.isReversed;
},
addTodo() {
this.inputValue && this.todos.arr.push(this.inputValue);
this.inputValue = '';
}
}
});
app.$mount('#app');
<!DOCTYPE html>
<html>
<body style="display: flex; justify-content: center;">
<div id="app">
<button #click="toggleReverse">Toggle reverse to {{ !isReversed }}</button>
<br />
<input v-model="inputValue" style="margin-top:5px;" />
<button #click="addTodo" :disabled="!inputValue">Add todo</button>
<!-- <ul><li v-for="(todo, i) in computedTodos" :key="`${i}-${todo}`">{{ todo }}</li></ul> -->
<list :iterable="computedTodos" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src="script.js"></script>
</body>
</html>
P.S.Try to avoid using such Array.prototype functions as shift/ unshift , reverse etc. to add / remove items from the beginning of the array or reverse the order, especially in the case when such operations are performed frequently and / or an array includes a big quantity of items, because they are quite costly as for performance (have O(n) complexity).
Another good solution is to use CSS to display elements in the reversed order (see an answer above).
The v-for directive doesn't support iterating backwards, so if you want to order by newest you're going to need to add another field to indicate when the item was added, or change id to increment every time an item is added.
Then, with field being the field indicting the order added:
<li v-for="item in items | orderBy 'field' -1" track-by="id">
For my use case (which is admittedly, apparently different than the OP...) I wanted to have the indices of the Array in reverse order in the v-for "loop."
My solution was to create a Vue app method reverseRange(length) that returns an Array of integers from length-1 to 0. I then used that in my v-for directive and simply referred to my Array elements as myArray[index] every time I needed it.
That way, the indices were in reverse order and I was able to then use them to access elements of the Array.
I hope this helps someone who landed on this page with this subtle nuance in their requirements like me.
You can use lodash reverse:
<li v-for="item in _.reverse(items)">

Angular: $scope.$watch a nested collection

In my Angular app, I have a checkbox list which is generated via a nested ng-repeat, like so:
<div ng-repeat="type in boundaryPartners">
<div class="row">
<div class="col-xs-12 highlight top-pad">
<div ng-repeat="partner in type.partners" class="highlight">
<label class="checkbox-inline">
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-true-value="{{partner}}"
ng-false-value="{{undefined}}">
<p><span ></span>{{partner.name}}<p>
</label>
</div>
</div>
</div>
</div>
and in my controller:
$scope.ids = [];
$scope.$watchCollection('ids', function(newVal) {
for (var i = 0, j = newVal.length; i < j; i++) {
// Create new participatingPatners tier if it doesn't exist
if(!$scope.report.participatingPartners[i]) $scope.report.participatingPartners[i] = {};
// Give it an id
$scope.report.participatingPartners[i].id = i + 1;
// Map our values to it
$scope.report.participatingPartners[i].entities = $.map(newVal[i], function(value, index) {
return [value];
});
}
});
The problem is, this $scope.$watchCollection stops watching once I've added one of each top-level ids, so if I add a given number of inputs from the first nested list, then another from the second list, My $scope.report.participatingPartners object never gets updated.
How can I $watch for changes within ids[$parent.$index][$index], making sure updated my object whenever a checkbox gets ticket or unticked?
You are creating an array of arrays:
$scope.ids = [
[],
[],
//...
]
But use $watchCollection to watch for changes in the outer array, i.e. of $scope.ids. This will only identify changes when nested arrays become different objects (or created the first time).
You could use $scope.$watch("ids", function(){}, true) - with true standing for "deep-watch", but that would be very wasteful, since it's an expensive check that would be performed on every digest cycle, whether a checkbox was clicked or not.
Instead, use ng-change to trigger the handler:
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-change="handleCheckboxChanged()">
$scope.handleCheckboxChanged = function(){
// whatever you wanted to do before in the handler of $watchCollection
}
$watchCollection is similar to $watch in that it checks the physical object reference, but goes one step further; it also goes one level deep and does a reference check on those properties.
You'll need to use $watch, but set the objectEquality flag to true. This will tell $watch to perform deep reference checking. Depending on the depth of the item being watched this can hurt performance significantly.
$watch(watchExpression, listener, [objectEquality]);
Can you try to watch for object equality :
$scope.$watchCollection('ids', function(newVal) {
}, true);

Categories

Resources