Modify source data for ng-repeat - javascript

I have an similar app as in the following example and I can't figure out why the source data is not updating. More info is in the example comments. I'm sure this is some trivial issue that I've overlooked.
Controller
$scope.items = [
{ id: 1, color: 'red', title: 'car' },
{ id: 2, color: 'blue', title: 'sky' },
{ id: 3, color: 'transparent', title: 'nothing' }
]
$scope.favoriteIds = [1, 2, 3]
$scope.getItem = function(id) { /* returns an item with given id */ }
Finally, there are two methods to modify $scope.items, but only the first one works, because the new item gets not-already-known id.
$scope.changeData1 = function() {
$scope.items = [{ id: 666, color: 'ugly', title: 'face' }]
$scope.favoriteIds = [666]
}
$scope.changeData2 = function() {
$scope.items = [{ id: 1, color: 'ugly', title: 'face' }]
$scope.favoriteIds = [1]
}
View
<h1>Favourite items</h1>
<ul ng-repeat="id in favoriteIds" data-ng-init="item = getItem(id)">
<li>I like my {{ item.color }} {{ item.title }}</li>
</ul>
<button ng-click="changeData1()">Modify data</button>
<!-- prints: I like my ugly face -->
<button ng-click="changeData2()">Modify nothing</button>
<!-- prints: I like my red car -->
The problem is, that I need to use this second way to modify data.
http://jsfiddle.net/4pEpN/7/

I'm relatively new to Angular as well, so if there's a simple way to do this, I don't know what it is (unfortunately, Angular documentation is atrocious). Regardless, you can avoid this by rethinking the structure of your code (and you'll end up with a better program too).
In your view, you're using ng-init to call getItem on the id during each iteration of your ng-repeat loop. This is what's causing your problem, and it's (apparently) due to an Angular performance feature (more at this question).
Basically, don't use ng-init except to execute something when your app starts. Otherwise, you'll end up with what you've got now: logic in the view (calling getItem(id)) rather than the model, where it belongs.
Instead, use ng-repeat to repeat over the exact data you want to display. Like I said before, this means some code rearrangement. For example, you could use a function to generate the user's current list of items on the fly. Check out this fiddle: http://jsfiddle.net/4pEpN/19/
See my comments in that code for all the changes I made, but the most relevant one is:
$scope.favoriteItems = function() {
var favObjs = [];
for (var i = 0; i < favoriteIds.length; ++i) {
favObjs.push(getItem(favoriteIds[i]));
}
return favObjs;
};
then in your view: <ul ng-repeat="item in favoriteItems()">
There are also lots of other approaches you could use. For instance, you could have an update function, which handles anything that might need to be done after any user input (including updating the user's custom array of items). Then you could call this in your changeData functions.

I don't think ng-init is appropriate since it only affects template initialization.
So how about just calling your getItem(id) for fetching each attribute, like this:
http://jsfiddle.net/JrvbD/1/

Related

How to initialize data in component according to the received props and don't lose reactivity

I have a problem with initialization data before component is created. My actual question depends on that problem: I'm losing reactivity in one of my data properties because I initialize it in lifecycle hook. But I don't know how to initialize an array from data(){} with a length which I receive from props. If I make it in lifecycle hook, then I'm losing reactivity, as I metioned before.
Here is some more details about my component:
In my Vue.js learning I'm trying to implement a stepper component. I decided to make it a little more dynamic and to be with a flexible size. So in my props of stepper component I receive an Object with such structure:
stepperData: {
steps: 3, //maybe later I'll add more options to stepperData, so I decided to implement it as an Object, not Array of content.
content: [
{
header: "Stepper #1",
text: "Hello World 1!"
},
{
header: "Stepper #2",
text: "Hello World 2!"
},
{
header: "Stepper #3",
text: "Hello World 3!"
}
]
}
Than in my stepper component I am using a steps field to determine a length of another array which hold data about marked or unmarked steps. Here is a code which I am using to initialize that array of marked steps:
methods: {
initializeMarkedSteps() {
this.markedSteps = [];
for (var i = 0; i < this.dataStepper.steps; i++) {
this.markedSteps[i] = false;
}
}
},
created: function() {
this.initializeMarkedSteps();
}
markedSteps is an empty array in data(){}
So, after that, I had an array of false values. In my template I have a v-bind:class
<div class="circle" v-bind:class="{markedCircle: markedSteps[s]}">
Thanks to it all of the steps are unmarked and they can became marked after user clicks "next" button.
<my-btn #click="nextStep">Next</my-btn>
my-btn is my wrapper component for simple button.
Code in nextStep():
nextStep() {
for (let i = 0; i < this.dataStepper.steps; i++) {
if (this.markedSteps[i] === false) {
this.markedSteps[i] = true;
console.log(this.markedSteps);
return;
}
}
}
BUT, when I click button, markedCircle class is not assigned as I expect despite the fact, that acual value of markedSteps[i] was changed to true after button was clicked.
I am very frustrated with this stuff with which I am so messed up. Any help will be appreciated. I have already checked docs on this theme and also I've read "Reactivity in Depth" section but I didn't saw an answer.
There are multiple problems
In your examples you don't show how you initialize your data() but assuming from the code this.markedSteps = []; in initializeMarkedSteps I think you have no markedSteps in data(). That's problem number 1. Properties in data are only reactive if they existed when the instance was created (add markedSteps: [] into data())
Due to limitations in JavaScript, Vue cannot detect changes to an array when you directly set an item with the index - use Vue.set(this.markedSteps, i, true) instead

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

AngularJS view doesn't update when assigning a $http response to $scope

I have a ng-repeat loop for a number of values. While looping, I fetch a variable from another array of values and use that:
<div ng-repeat="i in [1,2,3]">
<div ng-init="obj = getObject(i)">
<pre>{{ obj }} </pre>
</div>
</div>
My goal is now to change a property of that variable and do a POST request containing the updated variable. The response for that from the server then contains all values, which I bind to $scope in order to update the view.
<a ng-click="change(obj, 5)">Set property to 5</a>
$scope.change = function(o, value) {
o.prop = value;
// save() sends a POST requests and returns a JSON with all values
$scope.values = save(o);
}
This works, but only the first time I do it. All other changes will be reflected in the $scope.variables, but not in the {{ obj }} variables in the template. $scope.$apply() has no effect, either.
I've created a JS Fiddle to show my problem, which only mocks the HTTP requests. However, I have found that even when I run this code against my REST backend, the first time everything works fine but every time after that reflects no changes at all.
I think the issue is caused because you are using ng-init, which probably sets a non-changing value since you are calling a function. It will work once you change {{ obj }} to {{ getObject(i) }}. The only issue is that your variables are also being referenced and modified in the script allTwo and allThree are being modified since you directly assign them. I fixed that by cloning the objects, but it will probably not be an issue when you are using AJAX.
Here is an updated version of your fiddle: http://jsfiddle.net/0ps2d7Lp/6/
I have made changes to your fiddle.
<div ng-repeat="i in [1,2,3]">
<div>
<pre>{{ getObject(i) }} </pre>
</div>
</div>
Controller changes:
$scope.changeType = function(ids, type) {
angular.forEach($scope.objects, function(o) {
if (ids.indexOf(o.id) > -1) {
o.type = type;
var response = (type === 2) ? allTwo : allThree
$scope.objects = angular.copy(response);
}
});
};
Link to your updated fiddle is here
In your case, getObject() is necessary, but I excluded it in my answer for simplicity sake. I understand that you need to perform a PUT/POST request to update the objects on the server-side, but I don't believe it's necessary at all to re-bind the view to the server's response. Fundamentally, a PUT doesn't require a response other than 200 OK in most cases. The point is you're telling the server to update objects, not create them. Thus, no primary keys change, so, you don't need to rebind the objects. Changes are already resident in memory.
HTML
<div class="example" ng-controller="MyCtrl">
<div ng-repeat="obj in objects">
<div><pre>{{ obj }}</pre></div>
</div>
Change all to Type 2
Change all to Type 3
</div>
JavaScript
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
// initial objects
$scope.objects = [
{ id: 1, type: 1 },
{ id: 2, type: 2 },
{ id: 3, type: 3 }
];
$scope.changeType = function(ids, type) {
angular.forEach($scope.objects, function(o) {
if (ids.indexOf(o.id) > -1) {
o.type = type;
// Perform your PUT/POST request here for each
// updated object to update it on the server-side.
// There is no need to bind to a server response.
}
});
};
}

Bring single item to top of list

Given an arbitrary list of items, in a ul or set of divs, I would like to use the angular way to bring the selected item to the top of a list/display.
$scope.items = [{name: "Garfield", id: 1}, {name: "Simon", id: 2}, {name: "Whatever", id: 3}]
$scope.model = {selectedItemId: 3}
In the view, using ng-repeat
ul.items
li.item ng-repeat="item in items" ng-class="{selected: model.selectedItemId == item.id"
div {{item.name}}
I would like the selected item to be filtered or sorted to the top of the list while leaving remaining order in tact using AngularJS approach.
Since your data is already nicely formatted as an array, you would use a custom filter.
Angular provides a lot of built in filters, but it's pretty easy to write your own.
I was going to put an example in here, but a search found a very nice example created by Sam Deering
here that I really can't improve on.
For ease of access, here is relevant part of the code:
app.filter('currentUserToTop', function () {
return function (users, current) {
var newList = [];
angular.forEach(users, function (u) {
if (u.id == current) {
newList.unshift(u);
}
else {
newList.push(u);
}
});
return newList;
};
});
Here is the fiddle associated with this example.

Object equality comparison for input[radio] with ng-model and ng-value

Let me start by saying that this question is very similar to issues with selection in a <select> tag using ng-options. For example, Working with select using AngularJS's ng-options. The specific problem is comparing two different instances of an object which are not reference equal, but which logically represent the same data.
To demonstrate, let's say we have the following array of options and selected option variable in the model:
$scope.items = [
{ID: 1, Label: 'Foo', Extra: 17},
{ID: 2, Label: 'Bar', Extra: 18},
{ID: 3, Label: 'Baz', Extra: 19}
];
$scope.selectedItem = {ID: 1, Label: 'Foo'};
Note that the above objects are just for demonstration. I specifically left off the 'Extra' property on selectedItem to show that sometimes my model objects differ in their specific properties. The important thing is that I want to compare on the ID property. I have an equals() function on my real objects that compares both prototype 'class' and ID.
And then in the view:
<label class="radio inline" ng-repeat="item in items">
<input type="radio" ng-model="selectedItem" ng-value="item"> {{item.Label}}
</label>
Now, the problem here is that the radio button for 'Foo' will not start selected, because angular is using reference equality for the objects. If I changed the last line in my scope to the below, everything would work as expected.
$scope.selectedItem = items[0];
But, the problem I'm having is that in my application, I'm not simply declaring these two simple variables in scope. Rather, the options list and the data structure where the selected option are being bound are both part of larger sets of JSON data that are queried from the server using $http. In the general case, it's very difficult for me to go change the data-bound selected property to be the equivalent option from my data query.
So, my question:
In ng-options for the <select>, angular offers a track by expression that allows me to say something like "object.ID" and inform angular that it should compare the selected model value to the options via the ID property. Is there something similar that I can use for a bunch of radio inputs all bound to the same model property? Ideally, I would be able to tell angular to use my own custom equals() method that I've placed on these model objects, which checks both object type as well as ID. Failing that though, being able to specify ID comparison would also work.
I write a most simple directive. Using a kind of "track-by" to map two different objects. See the http://jsfiddle.net/xWWwT/146/.
HTML
<div ng-app="app">
<div ng-app ng-controller="ThingControl">
<ul >
<li ng-repeat="color in colors">
<input type="radio" name="color" ng-model="$parent.thing" ng-value="color" radio-track-by="name" />{{ color.name }}
</li>
</ul>
Preview: {{ thing }}
</div>
</div>
JS
var app = angular.module('app', []);
app.controller('ThingControl', function($scope){
$scope.colors = [
{ name: "White", hex: "#ffffff"},
{ name: "Black", hex: "#000000"},
{ name: "Red", hex: "#000000"},
{ name: "Green", hex: "#000000"}
];
$scope.thing = { name: "White", hex: "#ffffff"};
});
app.directive('radioTrackBy', function(){
return {
restrict: "A",
scope: {
ngModel: "=",
ngValue: "=",
radioTrackBy: "#"
},
link: function (ng) {
if (ng.ngValue[ng.radioTrackBy] === ng.ngModel[ng.radioTrackBy]) {
ng.ngModel = ng.ngValue;
}
}
};
});
OK, so after further review, I decided to go with a more "mix-in" approach, just replacing the ng-model directive with my own custom directive, in essence. This is very similar to the approach I used for making a "checkbox list" directive based on this answer: https://stackoverflow.com/a/14519881/561604.
.directive('radioOptions', function() {
// Apply this directive as an attribute to multiple radio inputs. The value of the attribute
// should be the scope variable/expression which contains the available options for the
// radio list. Typically, this will be the collection variable in an ng-repeat directive
// that templates the individual radio inputs which have radio-options applied. In addition,
// instead of the normal ng-model, use a selected-option attribute set to the same expression.
// For example, you might use radio-options like this:
// <label ... ng-repeat="item in collection">
// <input type="radio" ... ng-value="item" radio-options="collection" selected-option="myModel.myProperty">
// </label>
//
// See https://stackoverflow.com/questions/19281404/object-equality-comparison-for-inputradio-with-ng-model-and-ng-value
// for the SO question that inspired this directive.
return {
scope: {
radioOptions: '=',
selectedOption: '=',
ngValue: '='
},
link: function( scope, elem, attrs ) {
var modelChanged = function() {
if( jQuery.isArray(scope.radioOptions) ) {
jQuery.each( scope.radioOptions, function(idx, item) {
// This uses our models' custom 'equals' function for comparison, but another application could use
// ID propeties, etc.
if( typeof item.equals === 'function' && item.equals(scope.selectedOption) ) {
elem.prop( 'checked', item === scope.ngValue );
}
});
}
};
scope.$watch( 'radioOptions', modelChanged );
scope.$watch( 'selectedOption', modelChanged );
var viewChanged = function() {
var checked = elem.prop( 'checked' );
if( checked ) {
scope.selectedOption = scope.ngValue;
}
};
elem.bind( 'change', function() {
scope.$apply( viewChanged );
});
}
};
});
As OP requested, here's an example radio button directive that will work with complex objects. It uses underscore.js to find the the selected item from the options. It's a little more complicated than it should be because it also supports loading the options and selected value with AJAX calls.
Why don't you just use the ID for the select like this?
<input type="radio" ng-model="selectedItem" ng-value="item.ID"> {{item.Label}}
And then instead of using selectedItem you could write items[selectedItem].
Oh, and while playing with your problem in jsfiddle I noticed to other things:
a.) You forgot to add a name attribute to the input.
b.) Don't ever use something without a dot in ng-model. If you actually try to output selectedItem with {{selectedItem}} outside the ng-repeat block, you will notice that the value does not update when you chose a radio button. This is due to ng-repeat creating a own child scope.
Since I'm not yet able to add comments, so I have to reply here. Dana's answer worked ok for me. Although I'd like to point out in order to use his approach, one would have to implement the 'equals' function on the objects in the collection. See below example:
.controller('ExampleController', ['$scope', function($scope) {
var eq = function(obj) {
return this.id === obj.id;
};
col = [{id: 1, name: 'pizza', equals: eq}, {id:2, name:'unicorns', equals: eq}, {id:3, name:'robots', equals: eq}];
$scope.collection = col;
$scope.my = { favorite : {id:2, name:'unicorns'} };
}]);
See the plunker link.

Categories

Resources