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.
Related
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.
I'm making a modded version of the angular-fire todo list. This modification includes making sublists and sublists of sublists.
My problem is that when I add my first level of sublists, the sub-objects don't have the $id that I need to affix a next level of sublist.
They don't appear to have the usual rigamarole of firebase properties, just "title" and "completed" status.
I can't figure out why the ng-repeat I have doesn't give me more information, and especially why it works for the top level objects but not further below.
The original addition:
$scope.addTodo = function(theTodo) {
var newTodo = theTodo.trim();
if (!newTodo.length) {
return;
}
$scope.todos.$add({
title: newTodo,
completed: false
});
$scope.newTodo = '';
$scope.subtodo = false;
};
The sublist addition:
$scope.addSubList = function(parent, toDo) {
console.log(parent, toDo)
var subRef = newRef.child(parent.$id)
var newArray = $firebaseArray(subRef)
var newTodo = toDo.trim();
if (!newTodo.length) {
return;
}
newArray.$add({title: newTodo,
completed: false
})
$scope.sublistExists = true;
$scope.newTodo = '';
}
The $firebaseArray object only works its magic on the first-level children under the location that you initialize it with. So the behavior you are seeing is "working as designed".
If you want to handle multi-level collections, you have two options (as far as I can see):
handle the lower levels yourself
store all lists on a single level and then build the multi-level manually by keeping a parentId in each list
I'd recommend going with option 2, since it closer aligns with the Firebase recommendation to prevent unnecessary nesting. See https://www.firebase.com/docs/web/guide/structuring-data.html
Update: The problem I'm having is doing a combination of three things:
Adding a header to an array when the $priority (set to date created) changes. This is so I can group tasks by week and day in an ng-repeat.
Resorting that list when a task is checked. Checked tasks should go to the bottom.
When creating new tasks, I need to add them to the top of the list instead of the bottom.
Here is a plnkr of all the code: http://plnkr.co/edit/A8lDKbNvhcSzbWVrysVm
I'm using a priorityChanged function to add a header based on comparing the dates on a task:
//controller
var last = null;
$scope.priorityChanged = function(priority) {
var current = moment(priority).startOf('day');
var changed = last === null || !last.isSame(current);
last = current;
return changed;
};
//view
<li ng-repeat="task in list track by task.$id">
<h3 ng-show="priorityChanged(task.$priority)">{{getDayName(task.$priority)}}</h3>
and to move a task to the bottom of the list when a task is completed I am using a .sort function when I populate the task list:
var populateTasks = function(start, end) {
$scope.start = start;
$scope.end = end;
var ref = new Firebase('https://plnkr.firebaseio.com/tasks').startAt(start).endAt(end);
var list = $firebase(ref).$asArray();
list.sort(compare);
list.$watch(function() {
list.sort(compare);
});
function compare(a, b) {
return a.completeTime - b.completeTime;
}
$scope.list = list;
};
It seems as though these approaches will not work together. Is there a way of combining them so that when the list is re-sorted the ng-repeat will run through the tasks again and add the necessary headers? Is that the ideal solution? Can the header be separate?
Update: I moved the ng-init functionality directly into the h3 to try to get that to run again but it does not display the header in that case.
Update2: The header does seem to show up if at least two of the $priority dates are unique but I still have the problem of deleting or moving the associated list item removing the connected header.
USING A DIRECTIVE
You can create a directive to simplify things by nesting your client contents. demo
app.directive('repeatByWeek', function($parse, $window) {
return {
// must be an element called <repeat-by-week />
restrict: 'E',
// replace the element with template's contents
replace: true,
templateUrl: 'repeat.html',
// create an isolate scope so we don't interfere with page
scope: {
// an attribute collection="nameOfScopeVariable" must exist
'master': '=collection'
},
link: function(scope, el, attrs) {
// get the global moment lib
var moment = $window.moment;
scope.weeks = [];
updateList();
// whenever the source collection changes, update our nested list
scope.master.$watch(updateList);
function updateList() {
scope.weeks = sortItems(parseItems(scope.master));
}
function sortItems(sets) {
var items = [];
// get a list of weeks and sort them
var weeks = sortDescending(Object.keys(sets));
for(var i=0, wlen=weeks.length; i < wlen; i++) {
var w = weeks[i];
// get a list of days and sort them
var days = sortDescending(Object.keys(sets[w]));
var weekEntry = {
time: w,
days: []
};
items.push(weekEntry);
// now iterate the days and add entries
for(var j=0, dlen=days.length; j < dlen; j++) {
var d = days[j];
weekEntry.days.push({
time: d,
// here is the list of tasks from parseItems
items: sets[w][d]
});
}
}
console.log('sortItems', items);
return items;
}
// take the array and nest it in an object by week and then day
function parseItems(master) {
var sets = {};
angular.forEach(master, function(item) {
var week = moment(item.$priority).startOf('week').valueOf()
var day = moment(item.$priority).startOf('day').valueOf();
if( !sets.hasOwnProperty(week) ) {
sets[week] = {};
}
if( !sets[week].hasOwnProperty(day) ) {
sets[week][day] = [];
}
sets[week][day].push(item);
});
console.log('parseItems', sets);
return sets;
}
function sortDescending(list) {
return list.sort().reverse();
}
}
}
});
The repeat.html template:
<ul>
<!--
it would actually be more elegant to put this content directly in index.html
so that the view can render it, rather than needing a new directive for
each variant on this layout; transclude should take care of this but I
left it out for simplicity (let's slay one dragon at a time)
-->
<li ng-repeat="week in weeks">
<h3>{{week.time | date:"MMMM dd'th'" }}</h3>
<ul>
<li ng-repeat="day in week.days">
<h4>{{day.time | date:"MMMM dd'th'" }}</h4>
<ul>
<li ng-repeat="task in day.items">
<input type="checkbox" ng-model="task.complete" ng-change="isCompleteTask(task)">
<input ng-model="task.title" ng-change="updateTask(task)">
<span ng-click="deleteTask(task)">x</span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
OTHER IDEAS
Most likely, you just need to move your changed out of ng-init. I don't think that is re-run when elements move/resort.
<li ng-repeat="task in list">
<h3 ng-show="priorityChanged(task.$priority)">{{getDayName(task.$priority)}}</h3>
<!-- ... -->
</li>
Since your list may resort several times, you can probably also get a pretty significant speed boost by using track by
<li ng-repeat="task in list track by task.$id">
If that doesn't resolve the problem, it might be time to think about writing your own directive (these are more fun than they sound) and possibly to consider setting aside AngularFire and going right to the source.
You really want a more deeply nested data structure here you can iterate at multiple levels, and you may need to structure that yourself either on the client or the server, now that you have a sense of how you want them organized (essentially a group by week functionality).
you could use "unshift" javascript function
var fruits = ["1", "2", "3", "4"];
fruits.unshift("5","6");
Result
[ '5', '6', '1', '2', '3', '4' ]
Plunker - http://plnkr.co/edit/jXdwOQR2YLnIWv8j02Yp
I'm using angular to build a view which has a list of users in the main container (array-A) and a sidebar of users which host any 'selected' users (array-B).
The first one (A) has all the users.
[{ $$hashKey: "00F", id: "118207f5e52c3eb619a8760bc08c8224", username: "John Smith" },
{ $$hashKey: "00G", id: "9c2d6f31c88e7a654e64bd0d3371360a", username: "Fredy Rincon" },
{ ... }]
The second one (B) has the users that are already selected (Firebase db) and through which the sidebar is pre-populated. If the users are already selected then they appear here exactly as they do in array-A.
My goal is to be able to add and remove objects from array-B / Sidebar view, by clicking on items from array-A in the main container.
The below is as close as I've been able to get but it doesn't take into account the existing users which are pre-populated onto the sidebar, and just adds and removes items that are already present, as if they weren't there.
For your reference:
$scope.selectedUsers = array-B
user = object in array-A
$scope.selectUser = function (user) {
console.log($scope.selectedUsers.indexOf(user))
if ($scope.selectedUsers.indexOf(user) === -1 ) {
$scope.selectedUsers.push(user);
console.log($scope.selectedUsers);
} else {
$scope.selectedUsers.splice($scope.selectedUsers.indexOf(user), 1);
console.log($scope.selectedUsers);
}
};
I really have no idea as to how to approach a solution for this, would really appreciate any help possible.
Thanks.
Use Array.prototype.reduce to check users before adding:
$scope.add = function(user){
var match = $scope.selectedUsers.reduce(function(prev, curr) {
return (user.id === curr.id) || prev;
}, false);
console.log(match ? 'match' : 'no match');
if (!match) $scope.selectedUsers.push(user);
};
... where your view looks like:
<p ng-repeat="user in users">{{user.username}} <a ng-click="add(user)">+</a></p>
To remove users from the selectedUsers array, use Array.prototype.splice:
$scope.remove = function(index){
$scope.selectedUsers.splice(index, 1)
}
...where your view looks like:
<p ng-repeat="selectedUser in selectedUsers">{{selectedUser.username}}
<a ng-click="remove($index)">--</a></p>
Working Plunker
Since it appears that each of your objects has a unique string ID, a viable approach is to store only the IDs in array-A and array-B, store a dictionary of the full data objects in a shared service, and use an angular filter to look up the full data object by ID when needed for presentation, etc.
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/