$watch got unexpected multiple events in AngularJS? - javascript

I am trying to use AngularJS and moment.js in-order to format time after the json data loaded, and I used $watch to monitor the $scope.comments, but not sure why the $watch recognized 3 events (the result set from json contains 3 items) instead of 1-time as I expected. The console.lof('changed') has been executed 3 tiem
var MyApp = angular.module('MyApp', ['ng', 'commentController']);
MyApp.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
when('', {
templateUrl: '/partials/comment-list.html',
controller: 'CommentListCtrl'
});
}
]);
MyApp.directive("timeago", function () {
return function ($scope, element, attrs) {
$scope.$watch("comments", function () {
$('.timeago').each(function (index) {
console.log('chaneged');
$(this).removeClass('timeago');
var time = moment($(this).text());
//console.log(time.fromNow());
$(this).text(time.fromNow());
})
});
};
});
/* Controllers */
var commentController = angular.module('commentController', []);
commentController.controller('CommentListCtrl', function CommentListCtrl($http, $scope) {
$scope.comments = [];
$http.get('/api/json?n=3').success(function (data) {
$scope.commentsLoaded(data);
});
$scope.commentsLoaded = function (data, status) {
$scope.comments = data;
}
});
and the template:
<div ng-Controller="CommentListCtrl">
<ul class="comments" timeago>
<li ng-repeat="comment in comments">
<span class="timeago">{{comment.time}}</span>
<p>{{comment.content}}</p>
</li>
</ul>
</div>
Thank you very much for any help.

In your case, the reason $watch executes 3 times is:
The first time it executes is on startup, where newValue == undefined
The second time is when you call this line: $scope.comments = [];
The third time is when the json is received: $scope.comments = data;
It has nothing to do with your json has 3 items.
however, not sure why the console.log($(this).text()); after the data
loaded only get this : {{comment.time}} It seems that the event was
catched before the template rendered
Because at the time, angular does not update its bindings yet and the view is not updated.
For separations of concern and how we should work with mvc structure like angular, view is for displaying, you should not access data from there, access it though model instead. In your case, you're trying to format the display, it should be the job of a filter
Write a filter like this:
angular.module('commentController').
filter('dateFormat', function() {
return function(input) {
return moment(input).fromNow();
}
});
Use it in HTML, don't need timeago directive:
<div ng-Controller="CommentListCtrl">
<ul class="comments">
<li ng-repeat="comment in comments">
<span class="timeago">{{comment.time | dateFormat }}</span>
<p>{{comment.content}}</p>
</li>
</ul>
</div>

The watch method takes a function with 2 arguments (newValue,oldValue). You can check these values when the watch is executed.
$scope.$watch("comments", function (newValue,oldValue) {
From what i can tell, the first time it executes is on setup, where oldValue is null. Then on any other assignment. Check the values and you would know.
To handle it correctly put checks like
if(newValue && newValue!=oldValue) {
//do something
}

Related

AngularJs - binding passed in paramter to object from different controller

I'm having difficulties in solving this. What I'm trying to achieve is to update iterated objects which is passed in to a function in a different controller.
Here is my controllers -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function(item) {
if(item.type == 'variations'){
item = newItemObj;
}
}
})
.controller('BrowseCtrl', function($scope, dataService, $localstorage) {
dataService.getItems().then(function(returnData) {
$scope.items = returnData.products;
})
});
Here is my view -
<div ng-controller="BrowseCtrl">
<div class="list card product" ng-repeat="item in items" ng-click="intoCart(item)">
<div class="item item-text-wrap">
<span class="ifs-productcat" ng-repeat="category in item.categories">{{category}}<span ng-if="$index != item.categories.length - 1">,</span></span><br>
<h3>{{item.title}}</h3>
<h3>Rs.{{item.price}}</h3>
</div>
</div>
</div>
I need to update item object with newItemObject in iteration(ng-repeat) implemeted in template view after doing some condition check with method (intoCart) in another controller(AppCtrl). I'm fairly new to javascript programming and I'm looking for some help.
The problem I had was not able to get access to 'ng-repeat' child scope in controller.
I solved this using 'this.item' in controller rather than passing the whole object or index.
HTML -
<div class="list card product" ng-repeat="item in items" ng-click="intoCart()"></div>
Controller -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function() {
item = this.item; // current (ng-click) child scope of ng-repeat
if(item.type == 'variations'){
item = newItemObj;
}
}
})
Now, whenever I made changes to 'item' object, it automatically updates scope in view (ng-repeat).
Once way I like to handle this is by using services as setters and getters. The problem is you have to include the service with every controller that needs to access it, but if you don't have too many it's no big deal. So something like this:
.service('userFirstName', function() {
var userFirstNameProp;
return {
getter: function() {
return userFirstNameProp;
},
setter: function(value) {
userFirstNameProp = value;
}
};
})
Then you can call userFirstName.getter() or userFirstName.setter("John") as appropriate.

how to trigger an event in controller2 based on an action in controller1

Here is the plunkr i have created.
Basically i am facing 2 issues with this piece of code -
I need help loading months in the drop down and
When month is changed in the dropdown from headerController, the sales for that month is displayed in detailController. I am trying to create a dependency between multiple controllers using a service.
I will appreciate any help fixing these 2 issues.
You can use $broadcast service for event purposes. Following is the link which explains the use of $broadcast and communicating between two controllers.
enter code herehttp://plnkr.co/edit/d98mOuVvFMr1YtgRr9hq?p=preview
You could simply achieve this by using $broadcast from one controller in $rootScope and listen that event in $scope using $on. Though I would suggest you to use service that will share data among to controller. Using dot rule will reduce your code. Take a look at below optimized code. Also you could replace your select with ng-repeat with ng-option to save object on select.
Markup
<div data-ng-controller="headerController">
<h3>Select Month</h3>
<select id="month" ng-model="sales.selectedMonth"
ng-options="month.monthId for month in sales.monthlySales">
</select>
</div>
<div data-ng-controller="detailsController">
<h3>Sales for Month</h3>
<div ng-bind="sales.selectedMonth"></div>
</div>
Code
app.service('valuesService', [function() {
var factory = {};
factory.sales = {}
factory.sales.salesForMonthId = 10;
factory.sales.months = [1, 2];
factory.sales.monthlySales = [{monthId: 1,sales: 10}, {monthId: 2,sales: 20}];
factory.sales.selectedMonth = factory.sales.monthlySales[0];
return factory;
}]);
app.controller('headerController', ['$scope', 'valuesService',
function($scope, valuesService) {
$scope.sales = {};
getData();
function getData() {
$scope.sales = valuesService.sales;
}
}
]);
app.controller('detailsController', ['$scope', 'valuesService',
function($scope, valuesService) {
$scope.sales = {};
getData();
function getData() {
$scope.sales = valuesService.sales;
}
}
]);
Demo Plunkr
I can see the months are already loading fine.
For proper data binding to work across service and controller, you would need to bind one level above the actual data, resulting a dot in your expression. This is because javascript doesn't pass by reference for primitive type.
In service:
factory.data = {
salesForMonthId: 0
}
In controller:
app.controller('detailsController', ['$scope', 'valuesService',
function ($scope, valuesService) {
$scope.values = valuesService.data;
}
]);
In template:
<div>{{values.salesForMonthId}}</div>
Plunker

How to create own angular service with XHR properly?

I am very new about AngularJS things. Need to do file upload with other datas in form, I found some scripts and angular plugins but I am using my own service calls $xhr. I was able to send file but i got error, bug(not real error-bug, i just named like that) or i can not use AngularJS properly. Here it is:
.
JS
var app = angular.module('ngnNews', []);
app.factory('posts', [function () {...}]); // I reduced the codes
app.factory('$xhr', function () {
var $xhr = { reqit: function (components) { ... //My Xml HTTP Request codes here }}
return $xhr;
});
app.controller('MainCtrl', ['$http','$scope','$xhr','posts',
function ($http, $scope, $xhr, posts) {
$scope.posts = posts.posts;
$scope.files = [];
var newPost = { title: 'post one', upvotes: 20, downvotes: 5 };
$scope.posts.push(newPost);
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
console.log($scope.posts); //[FIRST OUT]
$scope.posts.push(result.post);
$scope.title = '';
console.log($scope.posts); //[SECOND OUT]
}
}
},
values: { upvotes: 0, downvotes: 0 },
files: $scope.files
});
...
}
}]);
.
HTML
<form action="/Home/FileUp" id="postForm" method="post" enctype="multipart/form-data">
<div class="form-group input-group">
<span class="input-group-addon">Post Title</span>
<input name="title" class="form-control" type="text" data-ng-model="title" />
</div>
<ul>
<li ng-repeat="file in files">{{file.name}}</li>
</ul>
<button class="btn btn-primary" type="button" data-ng-click="addPost()">Add New</button>
</form>
SCREEN
Sample post displayed in list
.
PROBLEMS
When I click first time Add New button everything works well until $scope.posts.push(result.post);. In console, [SECOND OUT] is here:
First object has $$hashKey but second object which sent from server(added by $scope.posts.push(result.post); function) doesn't have. I want to know why is this happening? But it's not only weird thing, when I second time click Add New button, everything completed successfully (No new logs in console, adding new post to list shown screen image above).
MAIN PROPLEM
I pushed returned value from the server but post list(in screen) is not affected when first click.
QUESTIONS
- What is happening? or
- What am I doing wrong? Thanks for any explanation.
You are doing nothing wrong with respect to $$hashkey if that is your concern. When you use ng-repeat with array of objects angular by default attaches a unique key to the items which is with the property $$hashkey. This property is then used as a key to associated DOM elements with the corresponding item in the array by identity. Moving the same object in array would move the DOM element in the same way in the DOM. You can avoid this (addition of additional property on the object by angular) by using track by with ng-repeat by providing a unique key on the object or a mere $index. So with that instead of creating a unique key and attaching it to $$haskey property angular will use the unique identifier you have provided to associate the DOM element with the respective array item.
ng-repeat="post in posts track by $index"
or (id you have a unique id for each of the object in the array, say id then)
ng-repeat="post in posts track by post.id"
And since you say you are using my xml http request code here, i am assuming it is not within the angular context so you would need to manually perform the digest cycle by using $scope.$apply() is on of those ways.
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
$scope.posts.push(result.post);
$scope.title = '';
$scope.$apply();//<-- here
}
}
},
But ideally you could wrap your xhr implementation with a $q and if you pass $q promise from your api, you wont need to perform a manual $scope.$apply() everywhere. Because $q promise chaining will take care of digest cycle invocation.

Saving an item through factory casts an error with AngularJS and Firebase

I'm using Firebase and AngularJS bundled up together and as I'm in the learningprocess of both, I guess. I'm having some trouble saving an item through a factory with Angular.
This is my script
var app = angular.module('LinkApp', ['firebase']);
app.constant('FirebaseLinks', 'https://[hidden].firebaseio.com/links')
app.controller('LinkCtrl', ['$scope', 'LinksFactory',
function($scope, LinksFactory) {
// get links
$scope.links = LinksFactory.getItems();
// update link
$scope.updateLink = function(link) {
LinksFactory.updateItem(link);
};
}
]);
app.factory('LinksFactory', ['$firebase', 'FirebaseLinks',
function($firebase, FirebaseLinks) {
var ref = new Firebase(FirebaseLinks);
var items = $firebase(ref);
return {
getItems: function() {
return items.$asArray();
},
updateItem: function(item) {
items.$save(item);
}
};
}
]);
This is my html
<div ng-repeat="link in links | orderBy: link.number">
<input type="number" ng-model="link.number" ng-blur="updateLink(link)">
{{ link.name }}: {{ link.url }}
</div>
I'm using my blur function (updateLink) to pass my item into my factory, but from there I get this error: "TypeError: undefined is not a function".
If I pass my item to my function and save my collection from there width $scope.links.$save(link), it is successfull.
How can this be?
Thanks in regards. Say if you need further details.
After a lot of work I figured it out.
I had to return my factory functions and I had to call my variable 'items' with an $asArray() at the end and remove the $asArray() from my getItems function.
But now it works :)

Populate jQuery UI accordion after AngularJS service call

I'm currently trying to build an AngularJS app where I'm using a jQuery UI accordion control.
The problem is, that the jQuery UI accordion is initiated before my AngularJS service is done loading data from the server. In other words: the accordion doesn't have any data when it's initiated and thus does not show when the data from AngularJS is populated.
The view looks like this:
<!-- Pretty standard accordion markup omitted -->
$("#b2b-line-accordion").togglepanels();
My AngularJS controller looks like this:
app.controller('orderController', function ($scope, orderService, userService) {
// Constructor for this controller
init();
function init() {
$scope.selected = {};
$scope.totalSum = 0.00;
$scope.shippingDate = "";
$scope.selectedShippingAddress = "";
$scope.orderComment = "";
$scope.agreements = false;
$scope.passwordResetSuccess = false;
$scope.passwordResetError = true;
userService.getCurrentUser(2).then(function (response) {
$scope.user = response.data;
orderService.getProductCategoriesWithProducts($scope.user).then(function (d) {
$scope.categories = d.data;
});
});
}
// Other methods omitted
});
And my AngularJS services looks like this:
app.service('orderService', function ($http) {
this.getProductCategoriesWithProducts = function (user) {
return $http.post('url to my service', user);
};
});
app.service('userService', function ($http) {
this.getCurrentUser = function(companyId) {
return $http.get('url to my service' + companyId + '.aspx');
};
this.resetPassword = function() {
return true;
};
});
Is there any way to tell the accordion to "wait" to initialise until the data is returned from the service? :-)
Thanks in advance!
Update
I tried chaining the methods and added some logging and it seems that the accordion is in fact initiated after the JSON is returned from the service.
userService.getCurrentUser(2).then(function(response) {
$scope.user = response.data;
}).then(function() {
orderService.getProductCategoriesWithProducts($scope.user).then(function(d) {
$scope.categories = d.data;
console.log("categories loaded");
}).then(function () {
$("#b2b-line-accordion").accordion();
console.log("accordion loaded");
});
});
However, it doesn't display the accordion :-( The first accordion div looks fine in the generated DOM:
<div id="b2b-line-accordion" class="ui-accordion ui-widget ui-helper-reset" role="tablist">
...
</div>
But the rest of the markup (which is databound with angular) itsn't initiated.
Complete markup:
<div id="b2b-line-accordion">
<div ng-repeat="productCategory in categories">
<h3>{{ productCategory.CategoryName }}</h3>
<div class="b2b-line-wrapper">
<table>
<tr>
<th>Betegnelse</th>
<th>Str.</th>
<th>Enhed</th>
<th>HF varenr.</th>
<th>Antal</th>
<th>Bemærkninger</th>
<th>Beløb</th>
</tr>
<tr ng-repeat="product in productCategory.Products">
<td>{{ product.ItemGroupName }}</td>
<td>{{ product.ItemAttribute }}</td>
<td>
<select ng-model="product.SelectedVariant"
ng-options="variant as variant.VariantUnit for variant in product.Variants"
ng-init="product.SelectedVariant = product.Variants[0]"
ng-change="calculateLinePrice(product); calculateTotalPrice();">
</select>
</td>
<td>{{ product.ItemNumber }}</td>
<td class="line-amount">
<span class="ensure-number-label" ng-show="product.IsNumOfSelectedItemsValid">Indtast venligst et tal</span>
<input type="number" class="line-amount" name="amount" min="0" ng-change="ensureNumber(product); calculateLinePrice(product); calculateTotalPrice();" ng-model="product.NumOfSelectedItems" value="{{ product.NumOfSelectedItems }}" />
<td>
<input type="text" name="line-comments" ng-model="product.UserComment" value="{{ product.UserComment }}" /></td>
<td><span class="line-sum">{{ product.LinePrice | currency:"" }}</span></td>
</tr>
</table>
</div>
</div>
</div>
SOLUTION
Finally I found a way around this! I'm not entirely sure if it's that pretty and if it's the Angular-way of doing stuff (I guess it isn't)
Made a directive with the following code:
app.directive('accordion', function () {
return {
restrict: 'A',
link: function ($scope, $element, attrs) {
$(document).ready(function () {
$scope.$watch('categories', function () {
if ($scope.categories != null) {
$element.accordion();
}
});
});
}
};
});
So basically when the DOM is ready and when the categories array changes (which it does when the data has been loaded), I'm initiating the jQuery UI accordion.
Thanks a lot t #Sgoldy for pointing me in the right direction here!
Yes you need a directive and you can handle this more angular way !
In HTML define the directive
<div ui-accordion="accordionData" ></div>
Return promise from your service and pass the promise to the directive.
In controller
$scope.accordionData = myService.getAccordionData();
The ui-accordion directive looks like
.directive('uiAccordion', function($timeout) {
return {
scope:{
myAccordionData: '=uiAccordion'
},
template: '<div ng-repeat="item in myData"><h3 ng-bind="item.title"></h3><div><p ng-bind="item.data"></p></div></div>',
link: function(scope, element) {
scope.myAccordionData.then(function(data) {
scope.myData = data;
generateAccordion();
});
var generateAccordion = function() {
$timeout(function() { //<--- used $timeout to make sure ng-repeat is REALLY finished
$(element).accordion({
header: "> div > h3"
});
});
}
}
}
})
When your service call succeed then you create your accordion. Here you can define your own accordion-template like
<div ng-repeat="item in myData">
<h3 ng-bind="item.title"></h3>
<div>
<p ng-bind="item.data"></p>
</div>
</div>
Template binds with your model data myData. I use ng-repeat inside the template to create accordion-header and accordion-body HTML.
In the generateAccordion method i use $timeout to make sure the ng-repeat is really finished rendering because $timeout will execute at the end of the current digest cycle.
Check the Demo
My best practice is to resolve your asynchronous services before controller is initiated.
As you can see in the document, http://docs.angularjs.org/api/ngRoute.$routeProvider
resolve - {Object.=} - An optional map of
dependencies which should be injected into the controller. If any of
these dependencies are promises, the router will wait for them all to
be resolved or one to be rejected before the controller is
instantiated. If all the promises are resolved successfully, the
values of the resolved promises are injected and $routeChangeSuccess
event is fired. If any of the promises are rejected the
$routeChangeError event is fired.
Your controller and view won't be even started before your service is resolved or rejected.
There is a good video tutorial about this, https://egghead.io/lessons/angularjs-resolve
In your case, you can config routes like the following
var myApp = angular.module('myApp', ['ngRoute']);
myApp.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'main.html',
controller: orderController,
resolve: {
categories: function(orderService) {
return orderService.getProductCategoriesWithProducts();
},
user: function(userService) {
return userService.getCurrentUser();
}
}
});
Then, with your controller
app.controller('orderController', function($scope, categories, user) {
//categories and user is always here, so use it.
});
I have also found a similar question and answer here

Categories

Resources