Angular- detecting a change in scope - javascript

I have a service that grabs JSON data for me and hands it off to a controller:
Service snippet:
...
getP2PKeywordData: function(global_m, global_y) {
// Return the promise
return $http({
url: base_url + 'get/P2P/kwords/',
method: "GET",
// Set the proper parameters
params: {
year: global_y,
month: global_m
}
})
.then(function(result) {
// Resolve the promise as the data
return result.data;
},
function(data) {
// Error handling
});
}
...
The controller successfully grabs the data, which I have tested with a console.log underneath the $scope.d3Data = data; line.
Controller snippet:
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore) {
$rootScope.$on('updateDashboard', function(event, month, year) {
updateDashboard(month, year);
});
var updateDashboard = function(month, year) {
requestService.getP2PKeywordData(month, year).then(function(data) {
$scope.d3Data = data;
});
};
updateDashboard($cookieStore.get('month'), $cookieStore.get('year'));
}]);
The controller is hooked up to a d3-cloud directive (d3 word cloud) that actually appends the proper svg elements and draws the word cloud with the data. However, for some reason the controller above isn't passing the $scope.d3Data to the directive.
This is confusing because when I hardcode in an array of data into the controller, something like this...
$scope.d3Data = [
{
'kword': 'a',
'count': 20,
},{
'kword': 'b',
'count': 10,
...
... it connects to the directive perfectly!
Directive snippet:
myApp.directive('d3Cloud', ['$window',
'd3Service',
'd3Cloud',
function($window,
d3Service,
d3Cloud) {
return {
restrict: 'EA',
scope: {
data: '=',
label: '#'
},
link: function(scope, element, attrs) {
d3Service.d3().then(function(d3) {
window.onresize = function() {
scope.$apply();
};
scope.$watch(function() {
return angular.element($window)[0].innerWidth;
}, function() {
scope.render(scope.data);
});
scope.render = function(data) {
HTML snippet:
<div class="col-md-6">
<div class="module">
<div class="inner-module" ng-controller="DownloadsCloudCtrl">
<div class="module-graph">
<d3-cloud data="d3Data"></d3-cloud>
</div>
</div>
</div>
</div>
What have I tried:
I tried to add a manual $scope.$apply() after the $scope.d3Data = data; line in the controller. This, oddly, worked the first time I did it, but on every page refresh after that I got a "$digest already in progress" error (which was to be expected...).
In order to fix the $digest error, I tried encapsulating my $apply function in a $timeout code chunk, and even the dreaded $$phase conditional. Both of these solutions fixed the console error, but failed to solve the original problem of passing the data from the controller to the directive.
TL;DR: I'm fairly lost. Ideas on where to troubleshoot next?

It seems you are treating the response as a promise twice. So once in the service:
.then(function(result) {
// Resolve the promise as the data
return result.data;
},
And in the controller you resolve the promise again:
requestService.getP2PKeywordData(month, year).then(function(data) {
$scope.d3Data = data;
});
This can work because (from my understanding) Angular sometimes resolves promises automatically when binding to the scope.
It would be better to just handle the promise in the controller only. So the service becomes:
getP2PKeywordData: function(global_m, global_y) {
// Return the promise
return $http({
url: base_url + 'get/P2P/kwords/',
method: "GET",
// Set the proper parameters
params: {
year: global_y,
month: global_m
}
});
}
UPDATE:
Try to initialize the d3Data scope property to an empty collection, and then push the response data into it. For example:
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore) {
//added
$scope.d3Data = [];
$rootScope.$on('updateDashboard', function(event, month, year) {
updateDashboard(month, year);
});
var updateDashboard = function(month, year) {
requestService.getP2PKeywordData(month, year).then(function(data) {
//then
angular.forEach(data, function(thing) {
$scope.d3Data.push(thing);
)};
});
};
updateDashboard($cookieStore.get('month'), $cookieStore.get('year'));
}]);

Related

ui-sref blocked from accessing controller data or view

I am having some trouble getting to the controller for my state param. I am using the correct state to link to the next view.
<td><a ui-sref="orders({customerId: cust.id})">View Orders</a></td>
In my config file I am referencing the state that name and the route params. I commented out the resolve object for now. My goal is to get into the controller then pass the correct data. Notice that I am using controllerAs
My initial thought was ({customerId: ctrl.cust.id }) However that did not change the url route.
The url is changing to match the url name but is not connecting to the controller and is not giving me the view.
(function() {
'use strict';
angular
.module('app.orders')
.config(config);
function config($stateProvider) {
$stateProvider
.state('orders',{
// params: {customerid: null},
url:'/customers:customerId',
templateUrl: './components/orders/orders.html',
controller: 'OrdersController',
controllerAs: 'ctrl',
resolve: {
customerFactory: 'customerFactory',
customerInfo: function( customerFactory, $stateParams) {
return customerFactory.getCustomers($stateParams.id);
}
}
************** my main problem is the resolve. This is blocking me from getting into the next controller. *****************
resolve: {
customerId:[ '$stateParams','customerFactory', function( $stateParams, customerFactory) {
return customerFactory.getCustomers($stateParams.id);
}]
}
})
};
})();
For now my controller is very small. I just want to connect to it. I have checked my networks tab and see GET for the files.
(function() {
// 'use strict';
angular
.module('app.orders')
.controller('OrdersController', OrdersController);
function OrdersController($stateParams) {
console.log('in');
var vm = this;
vm.title = "Customer Orders";
vm.customer = null;
}
}());
I have referenced my module in the main javascript file.
(function () {
'use strict';
angular.module('app', ['app.services',
'app.customers',
'app.orders','ui.router']);
})();
When I comment out the resolve I am able to access the controller. So I know the problem is in the resolve. Here is my service. I am making a request to a Json file with $http request and using .then
Updates Here is my refactored service call I am getting back the correct customer in the console each time.
(function() {
angular
.module('app.services',[])
.constant('_', window._)
.factory('customersFactory', customersFactory);
function customersFactory($http, $log) {
return {
getCustomers: getCustomers,
getCustomer: getCustomer
};
function getCustomers(){
return $http.get('./Services/customers.json',{catch: true})
.then(getCustomerListComplete)
.catch(getCustomerListFailed);
function getCustomerListComplete(response) {
console.log('response.data',response.data);
return response.data;
}
function getCustomerListFailed(error) {
console.log('error', error);
}
}
function getCustomer(id) {
var url = './Services/customers.json';
return $http.get(url, {
catch: true
})
.then(function(response) {
console.log('promise id',id);
var data = response.data;
for(var i =0, len=data.length;i<len;i++) {
console.log('data[i].id',data[i].id);
if(data[i].id === parseInt(id)) {
console.log('data[i]', data[i]);
return data[i];
}
}
})
}
}
}());
There is a working example with your code
It is very hard to guess what is wrong. Based on suggestion I gave you here Have a expression error in ui-sref ... your code seems to be completely valid.
I placed your stuff into this app.orders.js file (the ONLY change is templateUrl path, just for plunker purposes):
angular
.module('app.orders', ['ui.router'])
'use strict';
angular
.module('app.orders')
.config(['$stateProvider', config]);
//config.$inject = ['$stateProvider'];
function config($stateProvider) {
$stateProvider
.state('orders',{
// params: {customerid: null},
url:'/customers:customerId',
//templateUrl: './components/orders/orders.html',
templateUrl: 'components/orders/orders.html',
controller: 'OrdersController',
controllerAs: 'ctrl'
// resolve: {
// customerId:[ '$stateParams','customerFactory', function( $stateParams, customerFactory) {
// return customerFactory.getCustomers($stateParams.id);
// }]
// }
})
};
// 'use strict';
angular
.module('app.orders')
.controller('OrdersController', OrdersController);
OrdersController.$inject = ['$stateParams'];
function OrdersController($stateParams) {
console.log('in');
var vm = this;
vm.title = "Customer Orders " + $stateParams.customerId;
vm.customer = null;
}
And this is the working template components/orders/orders.html:
<div >
<h3>current state name: <var>{{$state.current.name}}</var></h3>
<h5>title</h5>
<pre>{{ctrl.title}}</pre>
...
When I call it like this:
<li ng-repeat="cust in [{id:1}, {id:2}]"
><a ui-sref="orders({customerId: cust.id})">View Orders - cust ID == {{cust.id}}</a>
</li>
Check it in action here
So, whil my previous answer was about make the state working without resolve, now we will observe few adjustments (and one fix) to make even resolve working.
There is a working plunker, extending the previous one.
FIX
The only fix, the most important change come from this definition:
angular
.module('app.services',[])
.factory('customersFactory', customersFactory);
see the plural in the factory name, the 'customersFactory'. While here:
...my main problem is the resolve. This is blocking me from getting into the next controller....
resolve: {
customerId:[ '$stateParams','customerFactory', function( $stateParams, customerFactory) {
return customerFactory.getCustomers($stateParams.id);
}]
}
we ask for 'customerFactory' (singular, no s in the middle)
Few improvements:
So, this would be our adjusted state def:
$stateProvider
.state('orders',{
// INTEGER is here used to later easily use LO_DASH
url:'/customers{customerId:int}', // int is the type
templateUrl: './components/orders/orders.html',
controller: 'OrdersController',
controllerAs: 'ctrl',
resolve: {
// wrong name with 's'
//customerId:[ '$stateParams','customerFactory',
// we use customer, because we also changed the factory
// implementation - to return customer related to
// $statePrams.customerId
customer:[ '$stateParams','customersFactory',
function( $stateParams, customersFactory) {
return customersFactory
//.getCustomers($stateParams.id)
.getCustomer($stateParams.customerId)
;
}]
}
})
Now, this is our adjusted factory, and its new method getCustomer
angular
.module('app.services', [])
.factory('customersFactory', customersFactory);
customersFactory.$inject = ['$http', '$log', '$q', '$stateParams'];
function customersFactory($http, $log, $q, $stateParams) {
return {
getCustomers: getCustomers,
getCustomer: getCustomer
};
function getCustomers() {
// see plunker for this, or above in question
}
// new function
function getCustomer(id) {
var url = "customer.data.json";
return $http
.get(url, {
catch: true
})
.then(function(response){
var data = response.data;
var customer = _.find(data, {"id" : id});
return customer;
})
;
}
}
this is our data.json:
[
{
"id" : 1, "name": "Abc", "Code" : "N1"
},
{
"id" : 2, "name": "Def", "Code" : "N22"
},
{
"id" : 3, "name": "Yyz", "Code" : "N333"
}
]
And here we have controller:
OrdersController.$inject = ['$stateParams', 'customer'];
function OrdersController($stateParams, customer) {
console.log('in');
var vm = this;
vm.title = "Customer Orders " + $stateParams.customerId;
vm.customer = customer;
}
a view to show customer
<h3>customer</h3>
<pre>{{ctrl.customer | json}}</pre>
Check it here in action

Router resolve will not inject into controller

I have tried everything to get ui-router's resolve to pass it's value to the given controller–AppCtrl. I am using dependency injection with $inject, and that seems to cause the issues. What am I missing?
Routing
$stateProvider.state('app.index', {
url: '/me',
templateUrl: '/includes/app/me.jade',
controller: 'AppCtrl',
controllerAs: 'vm',
resolve: {
auser: ['User', function(User) {
return User.getUser().then(function(user) {
return user;
});
}],
}
});
Controller
appControllers.controller('AppCtrl', AppCtrl);
AppCtrl.$inject = ['$scope', '$rootScope'];
function AppCtrl($scope, $rootScope, auser) {
var vm = this;
console.log(auser); // undefined
...
}
Edit
Here's a plunk http://plnkr.co/edit/PoCiEnh64hR4XM24aH33?p=preview
When you use route resolve argument as dependency injection in the controller bound to the route, you cannot use that controller with ng-controller directive because the service provider with the name aname does not exist. It is a dynamic dependency that is injected by the router when it instantiates the controller to be bound in its respective partial view.
Also remember to return $timeout in your example, because it returns a promise otherwise your argument will get resolved with no value, same is the case if you are using $http or another service that returns a promise.
i.e
resolve: {
auser: ['$timeout', function($timeout) {
return $timeout(function() {
return {name:'me'}
}, 1000);
}],
In the controller inject the resolve dependency.
appControllers.controller('AppCtrl', AppCtrl);
AppCtrl.$inject = ['$scope', '$rootScope','auser']; //Inject auser here
function AppCtrl($scope, $rootScope, auser) {
var vm = this;
vm.user = auser;
}
in the view instead of ng-controller, use ui-view directive:
<div ui-view></div>
Demo
Here is how I work with resolve. It should receive promise. So I create service accordingly.
app.factory('User', function($http){
var user = {};
return {
resolve: function() {
return $http.get('api/user/1').success(function(data){
user = data;
});
},
get: function() {
return user;
}
}
});
This is main idea. You can also do something like this with $q
app.factory('User', function($q, $http){
var user = {};
var defer = $q.defer();
$http.get('api/user/1').success(function(data){
user = data;
defer.resolve();
}).error(function(){
defer.reject();
});
return {
resolve: function() {
return defer.promise;
},
get: function() {
return user;
}
}
});
These are almost identical in action. The difference is that in first case, service will start fetching date when you call resolve() method of service and in second example it will start fetch when factory object is created.
Now in your state.
$stateProvider.state('app.index', {
url: '/me',
templateUrl: '/includes/app/me.jade',
controller: function ($scope, $rootScope, User) {
$scope.user = User.get();
console.log($scope.user);
},
controllerAs: 'vm',
resolve: {
auser: function(User) {
return User.resolve()
}
}
});

angular js $scope.apply function not updating template

When I return from my service call I seem unable to update my view. Why does 'not broken' never get out putted to the console?
the services returns [{test: 'service workies'}]
app.controller('foo-controller', ['fooService','$scope', function (fooService,$scope) {
var ctrl = this;
ctrl.Results = [{ test: 'no workies' }];
ctrl.Search = function () {
fooService.GetFoos().then(function (result) {
console.log('test');
console.log(ctrl.Results);
ctrl.Results = result;
console.log(ctrl.Results);
$scope.$apply(function () {
console.log('not broken');//never fires!!
ctrl.Results = [{test : 'workies' }]
});
});
};
return ctrl;
}]);
app.directive('fooLogo', function () {
return {
restrict: 'E',
templateUrl: './App/Templates/foo.html',
controller: 'foo-controller',
controllerAs: 'f'
};
});
edit foo service
.service('fooService', ['$http', function ($http) {
return $http.get("https://www.googleapis.com/books/v1/volumes?q=harry+potter").then(
function(result){ return [{ test: 'service workies'}]},
function(error) { return [{test: 'service call no workies'}] );
I see a few issues in your code. I don't see anywhere inside fooService where GetFoos() is declared, so that's one issue. Try the following:
app.controller('MainCtrl', ['$scope', 'BookQueryService',
function($scope, BookQueryService) {
$scope.search = function() {
BookQueryService.getBooks().then(function(data) {
$scope.books = data.data.items;
});
};
// call immediately for the sake of this example
$scope.search();
}
]);
app.service('BookQueryService', ['$http',
function($http) {
var service = {};
service.getBooks = function() {
return $http.get("https://www.googleapis.com/books/v1/volumes?q=harry+potter");
};
return service;
}
]);
app.directive('myBookList', function() {
return {
restrict: 'E',
templateUrl: 'BookList.html',
controller: 'MainCtrl'
}
});
With the following html:
<body>
<my-book-list></my-book-list>
</body>
And the following directive template:
<div>
<ul>
<li data-ng-repeat="book in books">
{{book.volumeInfo.title}}
</li>
</ul>
</div>
Here's a plunker with a working example:
http://plnkr.co/edit/KJPUWj0ghDi1tyojHNzI?p=preview
Is anything inside the fooService.GetFoos().then(function(result){...}) being run? If the code you posted is all there is for fooService, then it looks like there is no .GetFoos method & therefore nothing inside the following .then would get run.
Try adding a .error after the original .then that is chained onto fooService.GetFoos:
fooService.GetFoos().then(function (result) {
// your code
}).error(function (data, status){
console.log("Error!\t", status);
};
This will help you figure out what exactly is going on. Whenever your using any sort of promise, make sure you have a .catch or .error — they can save you a lot of trouble when debugging. Check out angular's $http documentation for more details.
Additionally, it looks like the original call to $scope.$apply() is unnecessary. You would only use that if you want to run a function outside of angular, or if you manually want to trigger the digest cycle (if that were the case calling $scope.$digest() explicitly would be much more appropriate than $scope.$apply.
Check out this blog post about when to use $scope.$apply and the $scope.$apply documentation page for more info

AngularJS: how to execute a method in another controller?

I've searched on Google but can't find information on how to do this properly. Seems like all the answers on Google are now outdated (using older versions of AngularJS).
I'm trying to setup two controllers on my AngularJS module. For example, the first controller is handling $http GET requests. And the second controller is displaying either a 'success' or 'error' message. I want to be able to call a method from the second controller with the success/error message that is to be displayed.
Or am I supposed to use a service/factory for this? I've read about services but can't figure out how to make something like this work.
var module = angular.module('app', []);
module.controller('ApiController', ['$scope', '$http', function ($scope, $http) {
$http.get('/api').
success(function(data){
// call AlertController('success')
}).
error(function(data){
// call AlertController('failed')
});
}]);
module.controller('AlertController', ['$scope', function ($scope) {
$scope.message = {
show_message: true,
type: 'info',
message: "Display message!"
};
}]);
Either doing it that way, or perhaps I would like to push the incoming alert onto a global object variable, and then remove it after it has been displayed.
Anyone know the proper way to set this up?
Ok let's try this - you should also check out Injecting $scope into an angular service function()
The Message service:
module.service('MessageService', function ($timeout) {
var messageQueue = [];
var DISPLAY_TIME = 5000; // each message will be displayed for 5 seconds
function startTimer() {
$timeout(function() {
// Remove the first message in the queue
messageQueue.shift();
// Start timer for next message (if there is one)
if (messageQueue.length > 0) startTimer();
}, DISPLAY_TIME);
}
function add(message) {
messageQueue.push(message);
// If this is the only message in the queue you need to start the timer
if (messageQueue.length==0) startTimer();
}
function get() {
if (messageQueue.length==0) return "";
else return messageQueue[0];
}
return { add: add, get: get };
});
You can still use this ApiService as well:
module.service('ApiService', ['$http', function ($http) {
return {
get: function(url) {
return $http.get(url);
}
};
}]);
Your Search controller:
module.controller('SearchController', ['$scope', 'ApiService', 'MessageService', function ($scope, api, messages) {
api.get('/yelp').
success(function(data){
messages.add('success');
}).
error(function(data){
messages.add('failed');
});
}]);
Your Alert controller:
module.controller('AlertController', ['$scope', 'MessageService', function ($scope, messages) {
$scope.getMessage = function() { messages.get(); }
}]);
So in your html you can have:
<div ng-controller="AlertController">
<div>{{ getMessage() }}</div>
</div>
here is how you make factory
module.factory('appService', ['$window', '$http', '$q', function(win, $http, $q) {
return{
backendcall: function(){
var deferred = $q.defer();
$http.get('/yelp').
success(function(data){
deferred.resolve(data);
}).
error(function(data){
deferred.resolve(status);
});
return deferred.promise;
}
}
}]);
and your controller will be like this
module.controller('AlertController', ['$scope', 'appService', function ($scope, appService) {
appService.backendcall().then(function(response){
$scope.message = {
show_message: true,
type: 'info',
message: "Display message!"
};
})
}]);

Angular data issue- not sure how to troubleshoot

I have a controller (code below) that links to a d3-cloud directive and works perfectly. Data is added in the controller and passed to the directive.
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore){
$scope.d3Data = [
{
'kword': 'a',
'count': 141658,
},{
'kword': 'b',
'count': 105465,
}
];
}]);
Now I'm trying to pull data from a JSON request service by switching my controller to the following code. When I do a console.log in the controller underneath the $scope.d3Data = data line, everything appears to be working properly (the proper data is returned).
However, something breaks when trying to link the controller to the directive, for some reason the directive is getting an undefined/null data set.
I'm wondering if the issue is in the order with which the code executes. Perhaps the controller tries to pass data to the directive before the JSON service has finished, thus resulting in no graph being drawn. Could this be happening, and if so, how can I go about fixing it?
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore){
$rootScope.$on('updateDashboard', function(event, month, year) {
updateDashboard(month, year);
});
var updateDashboard = function(month, year) {
requestService.getP2PKeywordData(month, year).then(function(data) {
$scope.d3Data = data;
});
};
updateDashboard($cookieStore.get('month'), $cookieStore.get('year'));
}]);
EDIT: Directive code:
myApp.directive('d3Cloud', ['$window',
'd3Service',
'd3Cloud',
function($window,
d3Service,
d3Cloud) {
return {
// Restrict usage to element/attribute
restrict: 'EA',
// Manage scope properties
scope: {
// Bi-directional data binding
data: '=',
// Bind to DOM attribute
label: '#'
},
// Link to DOM
link: function(scope, element, attrs) {
// Load d3 service
d3Service.d3().then(function(d3) {
// Re-render on window resize
window.onresize = function() {
scope.$apply();
};
// Call render function on window resize
scope.$watch(function() {
return angular.element($window)[0].innerWidth;
}, function() {
scope.render(scope.data);
});
// Render d3 chart
scope.render = function(data) {
// d3 specific appends... not important
HTML Code: (simple enough)
<div class="col-md-6">
<div class="module">
<div class="inner-module" ng-controller="DownloadsCloudCtrl">
<div class="module-graph">
<d3-cloud data="d3Data"></d3-cloud>
</div>
</div>
</div>
</div>
try adding a $scope.$apply() after $scope.d3Data = data;
$scope.d3Data = data;
$scope.$apply();
if this doesn't work, you can always pass a function down into the directive and set it to update the data and then manually call it from the controller:
so controller logic:
$scope.updateDirective = function () {}; // this will be overridden in directive
directive logic:
scope: {
data: '=',
update: '&updateDirective'
label: '#'
}
scope.updateDirective = function () {
scope.render(scope.data); // call to update function
};
markup:
<div class="col-md-6">
<div class="module">
<div class="inner-module" ng-controller="DownloadsCloudCtrl">
<div class="module-graph">
<d3-cloud data="d3Data" update-directive="updateDirective"></d3-cloud>
</div>
</div>
</div>
</div>

Categories