$watch() isn't updating the $scope - javascript

I have the following controller, and when I call $scope.remove() it makes a request to the usercart, which makes a request to the api. The api returns json object which has an object with an array of cart items.
The html on the page uses an ng-repeat to loop through the items, but the page isn't updating for some reason, and I can not figure out why.
// Main controller
app.controller('Checkout', function($scope, usercart){
$scope.cart = [];
$scope.$watch(function(){
return usercart.cart;
}, function(newVal, oldVal){
if(newVal !== oldVal){
$scope.cart = newVal;
}
}, true);
$scope.remove = function(domain){
usercart.remove(domain);
};
});
This service makes a request to the api and saves the cart data.
// User cart service
app.service('usercart', function(cart){
this.remove = function(domain){
// cart is an api service to make http requests
cart.removeDomain(domain).success(function(data){
this.cart = data.cart;
});
};
});
Here is a json response example:
{
"message":"Success",
"cart":[{
"domain":"asdfsadfsadf.org",
"years":2,
"amount":9
},{
"domain":"asdsmembers.cc",
"years":2,
"amount":24.95
},{
"domain":"asdsmembers.tv",
"years":2,
"amount":39.95
}]
}
Here is the html:
<tr ng-repeat="i in cart">
<td data-th="Product">
{{i.domain}}
</td>
<td data-th="Price">${{i.amount|number:2}}</td>
<td data-th="Quantity">
<select ng-model="i.years" ng-options="y.value as y.name for y in selYears" ng-disable="isInCart(i.domain)" ng-class="{disabled: isInCart(i.domain)}" ng-change="update(i.domain, 'years', i.years)"></select>
</td>
<td class="actions" data-th="" align="center">
<button class="btn btn-default btn-sm" style="background: #333;" ng-click="remove(i.domain)"><span class="glyphicon glyphicon-remove-circle" aria-hidden="true" style="color:#fff;"></span></button>
</td>
<td data-th="Subtotal" class="text-center">${{i.years * i.amount|number:2}}</td>
</tr>
Also when the page loads the table displays fine. It is just when I run the remove function.

I haven't tried but i believe here is the problem
cart.removeDomain(domain).success(function(data){
this.cart = data.cart;
});
Since this is a pointer to the caller of the callback function you will create cart property onto your cart api service. In order to circumvent this issue you should create variable called (by convention) self and assign this to it (at the begining of the usercart service):
var self = this;
after that, change your code into this:
cart.removeDomain(domain).success(function(data){
self.cart = data.cart;
});
To get better understanding you can go through this post

Watching your local $scope for a value you change in your singleton usercart definitely wouldn't work, unless you explicitely passed in that local scope. We can simplify this by ridding the $watch and resolving a promise we can return from our service instead. This allows for generic re-use and alleviates watchers from polluting our controllers. Observe the following changes...
app.service('usercart', function(cart) {
this.remove = function(domain) {
return cart.removeDomain(domain) // return promise
};
});
app.controller('Checkout', function($scope, usercart) {
$scope.remove = function(domain) {
usercart.remove(domain).then(function(data) { // resolve promise
$scope.cart = data.cart;
});
};
});

Related

javascript ajax not updating variable on time

I'm sure this is probably easy, but I am messing up ajax calls here. I am pretty new to javascript so I don't really know what I'm doing wrong. I've tried to look it up online and can't get my calls to work at the correct time. Any help is really appreciated.
All i am trying to do is get NHL player data from a json to a table i created using angularjs. Right now the table is displayed when $scope.players is undefined, but once the ajax completes it has data. I am not displaying it at the right time, my table is always empty
RosterController related code:
(function () {
'use strict';
angular.module("DevilsFanApp")
.controller("RosterController", RosterController);
function RosterController($rootScope, $scope, RosterService) {
$scope.players = RosterService.players;
$scope.addPlayer = addPlayer;
$scope.updatePlayer = updatePlayer;
$scope.deletePlayer = deletePlayer;
$scope.selectPlayer = selectPlayer;
$scope.fetchPlayers = fetchPlayers;
function init() {
fetchPlayers(function(res){
$scope.players = RosterService.players;
console.log("Goalies Now", $scope.players);
});
}
init();
function fetchPlayers(callback) {
var players = RosterService.updatePlayers(function(res){
callback(players);
});
}
}
})();
RosterService:
function RosterService($rootScope) {
var model = {
players: [],
updatePlayers: updatePlayers,
setCurrentPlayer: setCurrentPlayer,
getCurrentPlayer: getCurrentPlayer,
findPlayerByName: findPlayerByName,
findAllPlayers: findAllPlayers,
createPlayer: createPlayer,
deletePlayerById: deletePlayerById,
updatePlayer: updatePlayer,
findPlayerById: findPlayerById
};
return model;
function updatePlayers(callback){
$.ajax({
url: URL,
dataType: 'json',
type: 'GET',
}).done(function(response) {
var data = angular.fromJson(response);
for (var g = 0; g < data.goalie.length; g++) {
var player = model.findPlayerByName(data.goalie[g].name);
if (player == null) {
var newPlayer = {
_id: (new Date).getTime(),
name: data.goalie[g].name,
position: data.goalie[g].position,
height: data.goalie[g].height,
weight: data.goalie[g].weight,
birthday: data.goalie[g].birthdate,
age: 25,
birthPlace: data.goalie[g].birthplace,
number: data.goalie[g].number
};
model.players.push(newPlayer);
}
else{
callback(null)
}
}
return callback(model.players)
});
}
RosterView table code:
<tr ng-repeat="player in players">
<td>{{player.number}}</td>
<td>{{player.name}}</td>
<td>{{player.position}}</td>
<td>{{player.height}}</td>
<td>{{player.weight}}</td>
<td>{{player.birthday}}</td>
<td>{{player.age}}</td>
<td>{{player.birthPlace}}</td>
<td>
<div class="col-sm-4 btn-group">
<button ng-click="deletePlayer($index)" type="button" class="btn btn-table">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</button>
<button ng-click="selectPlayer($index)" type="button" class="btn btn-table">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
</button>
</div>
</td>
</tr>
They way you're trying to implement it seems too much of the "JQuery way", which is very different from how angular works. First of all, avoid callbacks, use promises instead. Also, if it is an option, use $http or restangular instead.
An example of the service following my suggestions would be like this (the example is only for the fetchPlayers funcionality):
angular.module('myModule', [])
.service('playersService', ['$http', function($http){
this.fetchPlayers = function(){
return $http.get(url);
}
}])
.controller('playerCtrl', ['$scope', 'playersService', function($scope, playersService){
$scope.players = []; //Start as an empty array
this.fetchPlayers = function(){
playersService.fetchPlayers.then(function(players){
//here you can process the data as you need
$scope.players = players; //assign to a property in scope so template can see it
});
};
this.fetchPlayers(); //Invoke fetch on load
}])
Here you can find a controller in a project that performs CRUD operations with $http and handles the response to show them in a table, and here is the implementation of the service to perform the calls to the backend API.

Angular: Best practice for updating a variable from a function?

Within an ng-repeat block I have textboxes. To detect when the content differs from the original, the original data is stored in a variable.
<tr data-ng-repeat="p in products">
<td>
<textarea data-elastic data-ng-model="p.comment" data-ng-change="hasChanged=checkChange(original, rnd.comment);" data-ng-init="original=rnd.comment; hasChanged=false"></textarea>
</td>
<td class="save" ng-show="hasChanged" ng-click="save(p, original)">Save</td>
A save button is shown only when the content has changed. After a successful save the original value should be updated to the new value.
I can do it like this with a function in the controller:
$scope.save = function (p, original) {
//...successful save
this.original = p.comment; //this works
original = p.comment; //this does not
}
Relying on some implicit scope in the form of 'this' doesn't seem sensible.
Why doesn't updating the variable (original = ...) work? What's a smarter way to do this?
Based on comments I've updated it as follows:
ng-click="save(p, this)"
$scope.save = function (p, scope) {
//...successful save
scope.original = p.comment; //this works
}
This seems failrly sensible now. Is passing scope around like this considered bad practice or acceptable?
Products is defined as follows:
productStatusApp.controller('productStatusCtrl', function ($scope, $http, cid) {
$http.get('api/company/products/' + cid).success(function (data) {
$scope.products = data.products;
});
I've found the best way to avoid this kind of problems, is to use services
https://docs.angularjs.org/guide/services
http://viralpatel.net/blogs/angularjs-service-factory-tutorial/
some rough code(use it just for pointers, not tested at all)
<tr data-ng-repeat="p in ProductsService.products">
<td>
<textarea data-elastic data-ng-model="p.comment"></textarea>
</td>
<td class="save" ng-show="p.originalComment!==p.comment" ng-click="ProductsService.save(p)">Save</td>
</tr>
and
var module = angular.module('app', []);
module.service('ProductsService', function () {
var products = [postA,postB,...,PostC];
products = products.map(function(p){p.originalComment=p.comment});
var save = function(p){
p.originalComment=p.comment;
someAjaxRequest(function _callback(err,response){....})
}
return {products:products,save:save};
});
and
module.controller('ProductsController', function ($scope, ProductsService) {
$scope.ProductsService= ProductsService;
});
They also allow better readability , WIN WIN

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.

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

How to create a restricted directive?

I'm experimenting with AngularJS and NG-Table and cannot solve the following:
I'm displaying a collection of User objects from a Django app in an NG-Table. One of the properties of the model is a boolean indicating whether the object is active. In stead of displating true/false, I want to display a glyph from the Font Awesome set using an AngularJS directive.
From various samples I've got the following.
The module:
var main = angular.module("main", ["ngTable"]);
Retrieval of the objects to be displayed in the table:
main.factory('User', function ($http) {
return {
async: function() {
var promise = $http.get('api/v1/users').then(function (response) {
return response.data["objects"];
});
// Return the promise to the controller
return promise;
}
};
});
The controller and the directive to transform the boolean to the glyph:
main.controller("UsersCtrl", function ($scope, $filter, ngTableParams, User) {
User.async().then(function(data) {
$scope.tableParams = new ngTableParams({
page: 1,
count: 4,
sorting: {
name: 'asc'
}
},{
total: data.length, // length of data
getData: function ($defer, params) {
// use build-in angular filter
var orderedData = params.sorting() ? $filter('orderBy')(data, params.orderBy()) : data;
$defer.resolve(orderedData.slice((params.page() - 1) * params.count(), params.page() * params.count()));
}
});
});
}).directive('boolean', function () {
return {
restrict: 'E',
link: function (scope, elem, attrs) {
var userObject = scope.userObject;
if (userObject["active"]) {
console.log("active");
console.log(elem);
elem.html = "<i class='fa fa-check text-success fa-lg'></i>";
} else {
console.log("not active");
console.log(elem);
elem.html = "<i class='fa fa-times text-danger fa-lg'></i>";
}
}
}
});
Then in my HTML template:
<table ng-table="tableParams" class="table">
<tr ng-repeat="propertyObject in $data">
<td data-title="'Name'" sortable="'name'">
[[ userObject.name ]]
</td>
<td>
<boolean>[[ userObject.active ]]</boolean>
</td>
</tr>
</table>
Due to collision with Django template conventions I had to change the Angular's default double curly brackets to square brackets.
The table displays ok, but for my boolean directive which fails to display a glyph and still just shows true or false. By logging to the console I can inspect the actual objects and they appear correct. I'm obviously missing something but would appreciate any help as to what...
You are running into issue where you need to let ng-repeat complete it's digest before trying to manipulate the element html. There are several ways to do it, using attrs.$observe or $timeout.
Baasically what is happening is your code is firing before the element is rendered
For no more than what you are doing you could simply use ng-class and you won't need a directive
<table ng-table="tableParams" class="table">
<tr ng-repeat="propertyObject in $data">
<td data-title="'Name'" sortable="'name'">
[[ userObject.name ]]
</td>
<td>
<i class='fa fa-times fa-lg'
ng-class="{'text-danger':!userObject.active,'text-success':userObject.active}">
</i>
</td>
</tr>
</table>
Or you could really simplify the directive to only return the <i> as template using the ng-class

Categories

Resources