I am creating a small todo app to learn Angular. I am stuck in one place. My task will be listed as list of links. Each time user clicks on it its status should change. So when a user
clicks on a task its status becomes, doing. Then in next click it become done. And one click makes it later. This should also reflect be stored back to the $scope from where this loaded. I have no idea, what will be the best way to do this in Angular.
Right now I have $scope.tasks storing all the tasks and $scope.tasks.$status is holding the available stasuses.
var app = angular.module("app");
app.controller('Ctrl', function($scope) {
$scope.tasks = [
{id: 1, text: 'Do something.', status: 'doing'},
{id: 2, text: 'Undo that thing.', status: 'done'},
{id: 3, text: 'Redo it again.', status: 'started'},
{id: 4, text: 'Responsive', status:'later'}
];
$scope.tasks = {
status: ['doing','done', 'later' ]
};
$scope.clicked = function() {
//some logic.
};
}
Below is the html.
<div ng-app="app" ng-controller="Ctrl" class"container">
<div ng-repeat="task in tasks" >
<li>
<a href="#" ng-click="clicked()" id="{{task.id}}">
{{task.text}} - {{task.status}}
<a/>
</li>
</div>
</div>
Is whatever I done so far correct? What is the best approach to toggle through the options? Is there any functions for that?
There're tons of solutions depending on what you want to do with this next. The simplest one seems to be:
$scope.clicked = function(task){
task.status = nextStatus(task.status);
};
function nextStatus(st){
return $scope.statuses[$scope.statuses.indexOf(st) + 1];
}
Note that you also should pass task to the function:
<a ng-click='clicked(task)'>
Related
When using ng-repeat which approach has better performance? (assuming there are a large number of users)
Approach 1: Filter in Controller
<div ng-repeat="user in users | showBlocked">
<strong>{{user.id}}</strong>
<span>{{user.name}}
</div>
HTML code in template
$scope.users = [
{ id: 1, name: 'alex', isBlocked: true},
{ id: 2, name: 'john', isBlocked: true}
];
JavaScript code in Controller
showBlocked is a filter which returns a list of blocked users
.filter('showBlocked', function() {
return function(users) {
return users.filter(user => user.isBlocked);
}
});
Approach 2: Reassigns users list
<button ng-click="reassignUser(1)">reassign user</button>
<div ng-repeat="user in users">
<strong>{{user.id}}</strong>
<span>{{user.name}}
</div>
HTML code in template
$scope.reassignUser = function (userId) {
if (userId === 1) {
$scope.users = [{id: 1, name: 'alex', isBlocked: true}];
}
// in this case just assigns a single user
};
CodePen Demo: ng-repeat filter vs reassign binding
Do let me know if you need any additional information.
ng-repeat is evaluated on every $digest cycle, making it extremely slow with two-way data-binding due to $dirty checking. The best solution is to use one-way data-binding with {{:: data}} syntax.
But in your example it is indeed better to re-write the array rather than to filter it. The use of filters will work slower, due to each filter creating a sub collection of the original list. However, this can be resolved differently, by hiding the data with ng-show. Here is a post about the complex solution, but you can consider this simple example:
angular.module('myApp', []);
angular.module('myApp').controller('Ctrl', ['$scope', function($scope) {
$scope.users = [{name:"John", "id":0},
{name:"Adam", "id":1},
{name:"Ado", "id":2},
{name:"Chris", "id":3},
{name:"Heather", "id":4},
{name:"Stan", "id":5}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="myApp" ng-controller="Ctrl">
ID: <input ng-model="query" />
<ul>
<li ng-repeat="user in users" ng-show="([user.id] | filter:query).length">
{{user.name}}
</li>
</ul>
</div>
I've got an array arr in a function which I'd like to return to $scope.notifications so I can use it in the HTML within the Ionic Framework.
I need to do it via a function so I can perform several actions with the array before returning it later on.
My controller:
.controller('notificationsCtrl', function($scope) {
$scope.notifications = function(){
var arr = [
{user:"misterx", name:"Mister X", action:4, image: "https://www.holidaycheck.de/mediaproxy?target=hcaHR0cDovL3d3dy5ob3RlbC5kZS9tZWRpYS9ob3RlbC9waWN0dXJlcy8zMzQvMzM0MTIzL0V4dGVyaW9yXzYzNTkyNzk5NDMyODQ1OTAwMi5qcGc%3D"},
{user:"misterx", name:"Mister X", action:2, image: "https://www.holidaycheck.de/mediaproxy?target=hcaHR0cDovL3d3dy5ob3RlbC5kZS9tZWRpYS9ob3RlbC9waWN0dXJlcy8zMzQvMzM0MTIzL0V4dGVyaW9yXzYzNTkyNzk5NDMyODQ1OTAwMi5qcGc%3D"},
{user:"ladyx", name:"Lady X", action:1}
];
return arr;
}
})
The HTML:
<ion-item ng-repeat="msg in notifications" class="item-text-wrap">
<div class="row">
<div class="col-80">
<strong>{{msg.name}}</strong> (<em>#{{msg.user}}</em>) {{msg.action}}.
</div>
<div class="col-20">
<img src="{{msg.image}}" style="border-radius: 50px; width: 100%">
</div>
</div>
</ion-item>
When I pass notifications directly as an array, without a function, it works. What am I doing wrong here?
Using ng-repeat="msg in notifications" tries to repeat over the function itself, not its return value. You want to call the function instead:
<ion-item ng-repeat="msg in notifications()">
https://jsfiddle.net/dj1gpjb8/
I should point out, though, that there are performance issues with this approach: the function will be called frequently because Angular can't predict whether the result of the function will change. You're better off embedding notifications as a plain array on the scope; anything that modifies that array later will automatically trigger the component to re-render with the new value(s):
$scope.notifications = [{
user: "misterx",
name: "Mister X",
//...
}];
$scope.addNotification = function() {
$scope.notifications.unshift({
user: "newguy",
name: "New Guy"
});
// angular will notice that notifications[] has changed, and re-render the component on the next $digest
};
https://jsfiddle.net/1du593af/
The following code is current so no need to use function to return JSON format
.controller('notificationsCtrl', function($scope) {
$scope.notifications = [
{user:"misterx", name:"Mister X", action:4, image: "https://www.holidaycheck.de/mediaproxy?target=hcaHR0cDovL3d3dy5ob3RlbC5kZS9tZWRpYS9ob3RlbC9waWN0dXJlcy8zMzQvMzM0MTIzL0V4dGVyaW9yXzYzNTkyNzk5NDMyODQ1OTAwMi5qcGc%3D"},
{user:"misterx", name:"Mister X", action:2, image: "https://www.holidaycheck.de/mediaproxy?target=hcaHR0cDovL3d3dy5ob3RlbC5kZS9tZWRpYS9ob3RlbC9waWN0dXJlcy8zMzQvMzM0MTIzL0V4dGVyaW9yXzYzNTkyNzk5NDMyODQ1OTAwMi5qcGc%3D"},
{user:"ladyx", name:"Lady X", action:1}
];
}
})
I've created a custom filter using AngularJS that prints out the fruits that start with a p. As far as I can tell, I've implemented the custom filter correctly.
I'm printing out a message every time the filter is called but I'm curious to why my filter is being called twice.
Looking at similar problems on stackoverflow I found one person who had a similar issue however the problem wasn't answered and was a little different.
JSFiddle Solution
http://jsfiddle.net/ddemott/U3pVM/22606/
HTML Code
<body>
<div ng-controller="ExampleCtrl" ng-app="sampleApp">
<div class="showDiffTags" ng-repeat="val in values | myFilter:'p'">{{val.name}}</div>
</div>
</body>
AngularJS Code
angular.module('sampleApp', []).filter('myFilter', function() {
return function(items, firstLetter) {
var groups = [];
console.log("called function");
console.log(items.length);
for (var i = 0; i < items.length; i++) {
if (items[i].name.substring(0, 1) == firstLetter) {
groups.push(items[i]);
}
}
return groups;
}
}).controller('ExampleCtrl', function($scope) {
$scope.values = [{
name: 'apple'
}, {
name: 'banana'
}, {
name: 'orange'
}, {
name: 'avocado'
}, {
name: 'pineapple'
}, {
name: 'peach'
}, {
name: 'plum'
}, {
name: 'grapes'
}, {
name: 'mango'
}, {
name: 'papaya'
}, ];
});
That is correct behaviour and it's strongly coupled with how $digest cycle works
Every time model changes the $digest is run at least twice:
After model changes it runs the watchers and updates the models
To check if the first $digest made changes to model, if so another digest is called up to max ten iterations then angular throw errors.
There is nothing to worry unless you have a lot of functions in templates and unstable models (changing often)
I've updated your fiddle with simple button that updates model on scope
http://jsfiddle.net/U3pVM/22610/
<button ng-click="numModel = numModel + 1">
update model {{numModel}}
</button>
You will see that every time you click the button filter runs twice
I'd like to ask for some help because I've been trying to solve this for some time now. I've read Derick Bailey's blog post on tree structures and CompositeViews. I also read David Sulc's but I have what I think is a slightly different use case than the one described there. Note: My project uses Marionette.js 1.0.3.
I am trying to build something that will work like an inbox with emails displayed in a table. An email may be a thread, meaning that it will have other emails that are linked to it. The inbox view is being rendered in a <table> where each <tr> is an email. My JSON looks like:
[
{id: 1, subject: 'One', threads: []},
{id: 2, subject: 'Two', threads: [
{id: 3, subject: 'Three', threads: []},
{id: 4, subject: 'Four', threads: []}
]},
{id: 5, subject: 'Five', threads: []}
]
My views are configured like this:
InboxView = Marionette.CompositeView.extend({
// edited for brevity
initialize: function(options) {
this.itemView = EmailView;
}
// edited for brevity
});
EmailView = Marionette.CompositeView.extend({
// edited for brevity
tagName: 'tr',
initialize: function(options) {
this.collection = this.model.get('threads');
},
onRender: function() {
if (this.model.isThread()) this.$el.addClass('thread');
}
// edited for brevity
});
The issue I'm having is that if I let CompositeView work its magic for me by rendering the model once and then the collection of threads once, I end up with two table rows <tr> (one for each thread) inside the <tr> for the original email (parent).
There exists functionality in the InboxView and EmailView that I'm trying to reuse. What I'm trying to end up with is a table that has all rows shown at the same level.
If you're reading this and want to help me, thank you in advance!
First of all you should attach views to the DOM. Errors occur, because child views rendered before they are attached to the DOM. You can override some methods to solve the problem. That will do the trick:
EmailView = Marionette.CompositeView.extend({
className: function () {
return this.model.isThread() ? 'thread' : null;
},
initialize: function (options) {
this.collection = new Backbone.Collection(this.model.get('threads'));
},
renderItemView: function(view, index) {
this.$el.parent().append(view.el);
view.render();
}
});
InboxView = Marionette.CompositeView.extend({
itemView: EmailView,
ui: {
$tbody: 'tbody'
},
renderItemView: function (view, index) {
this.ui.$tbody.append(view.el);
view.render();
}
});
JSFiddle: http://jsfiddle.net/d1krtxtr/
How can I do this:
$scope.$watch('item.completed', function(to, from){ …
in conjunction with this:
<li ng-repeat="item in items | filter:{completed: true}" ng-controller="ItemCtrl"> …
Live plunker example: http://plnkr.co/edit/pdFkEmxyqrzS6mc2AYo2?p=preview
Currently, when I change the item object's completed property, the $watch() is not firing.
I suspect it has something to do with the filter messing up object property's reference, but how can I accomplish this otherwise? I've also tried $watch(…, …, true) and $watchCollection() but neither has any effect.
You can listen for the scope destroy event and get the value from the event object:
.controller('ItemCtrl', ['$scope', function($scope){
$scope.$on('$destroy', function(o){log(o.targetScope.item.name + '(' + o.targetScope.item.completed + '):destroyed')});
$scope.$watch('item.completed', function(to, from){
log(from + ' --> ' + to);
});
}]);
I'm still not totally sure I understand the end-goal. I think you're saying that you want to be able to log when an item in your filtered list changes. I can think of a couple of imperfect approaches.
Forget about the watch entirely. Just call a function on ng-change for the checkbox input: http://plnkr.co/edit/IeAt4a31So7zjMJzptIp?p=preview
Create a deep watch on the items array itself via $scope.$watch('items', listener, true). The third argument being true will cause an object equality check to be used, then you can compare the before and after states of the array and log what changed each time: http://plnkr.co/edit/gg5DPWJhx8syhNhelFOT?p=preview
Neither is exactly a watch that gives you a specific changed item in the list, but I think you can get to what you ultimately need to do via those routes.
You should be able to have the watch function fire if you use ng-show instead of filtering.
<li ng-repeat="item in items" ng-show="{{item.completed}}" ng-controller="ItemCtrl">
This is a hacky way to do it, but I guess that it does what the OP wants:
http://plnkr.co/edit/wGRJIGJbALjMQqeffCyF?p=preview
It also proves that jonasnas and PSL were completely right.
The idea is to do this in the Controllers:
.controller('ItemsCtrl', ['$scope', function($scope){
$scope.items = [
{name: 'item 1', completed: true},
{name: 'item 2', completed: true},
{name: 'item 3', completed: true},
{name: 'item 4', completed: false},
{name: 'item 5'},
{name: 'item 6', completed: true},
];
$scope.itemsToWatch=[];
$scope.watchMe = function(item){
var myIdx = $scope.items.indexOf(item);
if($scope.itemsToWatch.indexOf(myIdx)==-1){
$scope.itemsToWatch.push(myIdx);
$scope.$watch('items[' + myIdx + '].completed', function(to, from){
log(from + ' --> ' + to);
});
}
};
}])
.controller('ItemCtrl', ['$scope',function($scope){
$scope.$parent.watchMe($scope.item);
}]);
I don't think that I would ever code anything like this in a real app, but it has been fun to find a way to solve the question, and I've learned a few things in the process.