Accessing Data set via a Promise in an Angular Directive - angularjs

I am writing my first non-trival Angular App and I have hit a snag with a directive. The directive takes data from a controller's scope and applies it to Google Chart. The chart is not the issue - which is to say it works fine with dummy data - it is access to the properties of the scope object which were obtained via http:
I am accessing data returned via an API in a service which utilizes $http:
dashboardServices.factory('SearchList', ['$http','$q',
function($http, $q){
return {
getSearchDetails:function(searchType, resultType){
return $http.get("api/searches/"+searchType+"/"+resultType)
.then(function(response){
if (typeof(response.data === 'object')) {
return response.data;
} else {
return $q.reject(response.data);
}
},function(response){
$q.reject(response.data);
});
}
}
}]);
In my controller, I am taking the response from this service and attaching to my scope via the promises' "then" method:
dashboardControllers.controller('DashboardCtrl', ['$scope', 'SearchList',
function($scope, SearchList){
$scope.searchData = {};
$scope.searchData.chartTitle="Search Result Performance"
SearchList.getSearchDetails("all", "count").then(function(response){
$scope.searchData.total = response.value; //value is the key from my API
});
SearchList.getSearchDetails("no_results", "count").then(function(response){
$scope.searchData.noResults = response.value;
});
}]);
To an extent this works fine, i can then use the 2-way binding to print out the values in the view AS TEXT. Note: I want to be able to write the values as text as I am trying to use a single scope object for both the chart and the textual data.
{{searchData.total | number}}
As mentioned, I have written a directive that will print a specific chart for this data, in this directive ONLY the $scope.searchData.chartTitle property is accessible. The values that were set in the then functions are not accessible in the directive's link method:
Here is the directive:
statsApp.directive('searchResultsPieChart', function(){
return{
restrict : "A",
scope:{
vals:'#vals'
},
link: function($scope, $elem, $attr){
var dt_data = $scope.vals;
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
}
});
Here is how I am using the directive in the view:
<div search-results-pie-chart vals="{{searchData}}"></div>
I can see that the issue is that the numeric values are not available to the directive despite being available when bound to the view.
Clearly the directive needs to be called later when these items are available or via some callback (or perhaps an entirely different approach), unfortunately i am not sure why this is the case or how to go about solving.
Any help would be greatly appreciated. I hope this makes sense.

I think the following will help you.
First change the directive scope binding for vals to use = instead of # (see this question for good explanation of the differences - basically # interpolates the value whereas = binds to the variable in the parent scope)
Then, move the part of the directive that creates the graph into a render function within your link function.
Then, $watch vals for any changes, then call the render function with the new values
You would also have to slightly change the approach of using ele[0], as you'll need to clear out the contents of it and add a new element with the new chart when the data changes (otherwise many charts will be added as the data changes!)
Here is an example of what to do in your link function with regard to the $watch and new render function (changing the $scope binding like I mentioned is not shown):
$scope.$watch('vals', function(newVals, oldVals) {
return $scope.render(newVals);
}, true);
$scope.render = function (dt_data) {
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
Hope this helps you out!!!

Related

AngularJS - service changes controller data

I discovered that when I call a service method within my controller and pass to it an object as a parameter, any changes that are done to that object (inside service method) are also made to the original object from my controller.
I always thought that controller data should stay unchanged until I changed it inside promise win/error event and only if I need to.
JS sample:
// Code goes here
var app = angular.module('App', []);
app.controller('AppCtrl', function($scope, simpleService){
$scope.data = { d: 1, c: 10};
$scope.clickMe = function(){
simpleService.clickMe($scope.data).then(function(res){
alert($scope.data.d);
})
.catch(function(err){
alert($scope.data.d);
});
}
});
app.factory('simpleService', function($q){
var simpleServiceMethods = {};
simpleServiceMethods.clickMe = function(data){
var deffered = $q.defer();
//data = JSON.parse(JSON.stringify(data)); - solution: clone data without references
data.d = 1111;
deffered.reject();
return deffered.promise;
}
return simpleServiceMethods;
});
Plunker demo: http://plnkr.co/edit/nHz2T7D2mJ0zXWjZZKP3?p=preview
I believe this is the nature of angular's databinding. If you want to pass the details of a $scope variable you could make use of angular's cloning capability with copy or update your services to work slightly differently by creating a copy on the service side. Normal CRUD style applications you'd normally be passing the id of an entity, receiving a new entity or posting changes which may in most cases already be present client side.

AngularJS - Not able to get value from factory service

I am very much new in AngularJS and due to which I am facing an issue and not able to understand the exact problem. The code which I tried is worked in normal javascript code but now I want to use custom service (factory function). Actually, I have a textarea where user can input their text and then they can do the formating by selecting any of the text. (discovertheweb.in/woweditor - this is existing site which I have created now I want to apply angular in this code). Now, I have created a custom service to check whether the user select any content or not. I have used the below code in my custom services and try to get the selection start, end point so that I can get the selected text from the textarea field. But, the problem is that I am getting 0 value for both selection start and end point. When i used the same code inside directive it works but try to get the value through service it is showing 0 for both. Please find the below code and let me know the code which I missed out here.
Custom Service:
(function(){
"use strict";
var wsApp = angular.module("WorkApp");
wsApp.factory("InputCheckService", function(){
var defaultText = document.getElementById("input-text-area");
var selStart = defaultText.selectionStart;
var selEnd = defaultText.selectionEnd;
var selectedText;
if(selStart != selEnd){
selectedText = defaultText.value.substring(selStart, selEnd);
}else{
selectedText = null;
}
return {
selStart: selStart,
defaultText: defaultText,
selEnd: selEnd,
selectedText: selectedText
};
});
}());
The directive where I called this services. I already included the service inside the main controller in different file.
(function(){
"use strict";
var wsApp = angular.module("WorkApp");
wsApp.directive("generalDirective", generalDirective);
function generalDirective(){
return {
scope: true,
controller:function($scope, InputCheckService){
$scope.collapsed = false;
$scope.CollpasePanel = function(){
$scope.collapsed = !$scope.collapsed;
};
$scope.updatePara = function(){
alert(InputCheckService.defaultText+"Selection start: "+InputCheckService.selStart+" Selection End: "+ InputCheckService);
/**
* defaultText: defaultText,
selStart: selStart,
selEnd: selEnd,
selectedText: selectedText
*/
}
},
templateUrl: 'directive/general-directive.html'
};
}
}());
If you need any more details, please let me know.
Thanks in advance for your time and suggestion.
Regards,
Robin
You should not use service to manipulate DOM element. You should manipulate DOM only at directives. Your problem is you DONT have anywhere to listen to TEXTAREA SELECTION EVENT and your service will not update the data inside. I have created a fiddle for your problem. The watchSelection directive is based on this answer from stackoverflow. Something you should notice :
I use service only to store data. Something like selStart, selEnd or paragraphContent and provide some API to retrieve the data
.factory("InputCheckService", function () {
return {
setSelStart: function (start) {
selStart = start;
},
.....
},
});
On the watchSelection directive, you watch for the mouseup event and will perform update the service so that it will store value you need and later you can retrieve it from other directives or controllers.
elem.on('mouseup', function () {
var start = elem[0].selectionStart;
//store it to the service
InputCheckService.setSelStart(start);
});
In your generalDirective directive you can get value from your service and angular will auto update the view for you.
Hope it helps.

Modifying factory value in directive Angular

I have the following code : http://codepen.io/Andarius/pen/Ggryge .
When the user draws a 'crop_area', the crop button should not be disabled anymore.
Why is the value no_crop_area (from the Image factory) not updated when drawing ?
Is it a scope problem ?
Also, I'm pretty new to AngularJS and was wondering what is the best practice when passing a factory to a controller (if there is one)
Given a factory :
myApp.factory('myFactory', function () {
return {foo:{bar:2}};
});
Is it better to do :
myApp.controller('myCtrl', ['myFactory',function (myFactory) {
var self = this;
self.foo = myFactory;
self.bar = myFactory.bar;
}]);
or
myApp.controller('myCtrl', ['myFactory',function (myFactory) {
var self = this;
self.foo = myFactory;
self.bar = self.foo.bar;
}]);
I have forked your code. Here is a working one.
http://codepen.io/anon/pen/qERpew?editors=101
The button is not getting enabled because, you are doing all the processing related to creating crop area by using javascript event handlers, so the code related to $scope (ie.angular related), will not come into effect. To have them in effect, you have to wrap the code related to $scope into $scope.$apply(function(){ // Your $scope variable update code.}).
PS: As per convention you should not use $scope name inside directive.Instead of that use scope.

Angularjs UI won't update with service binding that's updated by promise?

I've spent the night on trying to figure this out and have finally decided to give up and ask for help.
I'm building a web-app with AngularJS that is designed to work with flakey connections (mobiles).
I'm trying to implement the functionality for a user to add a object (whether that's an appointment, book, etc is irrelevant) to the server.
Service that handles syncing objects with the server:
angular.module('App')
.service('syncUp', function syncUp($http, $q, app) {
this.addObject = function addObject(object) {
var deferred = $q.defer();
app.inSync = false;
var httpConfig = {
method: 'POST',
url: 'http://myurl.dev/app_dev.php/api/add-object',
data: object
}
function persist() { setTimeout(function() {
$http(httpConfig).
success(function(data, status) {
app.inSync = true;
deferred.resolve(data.id);
}).
error(function(data, status) {
app.inSync = false;
persist();
});
}, 3000);
};
persist();
return deferred.promise;
}
});
'app' service that the status bar is bound to:
'use strict';
angular.module('App')
.service('app', function app($http, $q) {
this.inSync = true;
});
Template binding to the 'app' service inSync property:
<div class="status" ng-class="{'insync':inSync}"></div>
Specific object service that sends data from the controller to the syncUp service:
this.addBook = function(book)
{
var tempId = syncUp.generateUid();
this.books[tempId] = book;
this.books[tempId].tempId = tempId;
syncUp.addObject({
'type': 'book',
'data': this.books[tempId]
}).then(function(newId) {
booksRef[newId] = book;
delete booksRef[tempId];
}, function() {});
}
Everything is working as it should (data is being persisted to the server and the ID is being returned and replacing the tempId just fine. The problem is, when the inSync key on the 'app' service is updated, the class isn't added/removed from the div as it should be with ng-class in the template. If I load another route, that will force iterate through whatever internal cycle angular is doing and update the class on the template.
I've tried all manner of $apply() solutions, moving where the app.inSync key is set back to true, looping a function watching it. It's being set in all the right places (from debugging I know it's set back to true correctly), I just can't figure out how to make the change appear on the UI.
I tried:
$rootScope.$apply(function() {
app.inSync = true;
});
Which gave me an error (already running a digest, or something).
So I tried the 'safeApply' version that has been circulated on many answers/blogs, which didn't throw the error, but didn't work either.
As far as I can figure out, the UI should be updated when promises are resolved (both the http and my syncUp.addObject promise are resolved, so I'm not sure why it's not working.
Any ideas? I need to keep the current implementation of promises to be able to set the returned ID from the server on the added object, to avoid a circular-dependency issue between the syncUp and object angular services.
Edit:
And the status bar directive:
angular.module('App')
.directive('navigation', function (app) {
return {
templateUrl: '/app/views/navigation.html',
restrict: 'E',
link: function (scope, element, attrs) {
scope.inSync = app.inSync;
}
}
});
References you make in templates refer to objects on the current $scope. Services do usually not create or add anything to the $scope, so putting properties on a service, will not make them available to the template. To get stuff on the $scope, you need to use a controller. You can use the ng-controller directive to reference a controller, you'll find examples of this in the first AngularJS tutorials.
What you should do is create a controller and have it listen for events from the service. Here's an example of how to do that.
That's the nice way; You might also be able to get away with it by putting the inSync = true on the $rootScope as such;
service('syncUp', function syncUp($http, $q, app, $rootScope) {
// (...)
$rootScope.inSync = true;
It looks like you're hoping to see bindings operating between a service ('app') and a template. It's hard to tell if we're not seeing the entire picture. Going on that assumption, you need to refactor so that you are setting up bindings on a controller.
I would expect the controller setup to look something like this:
angular.module('App')
.controller('app', function app($http, $q, $scope) {
$scope.inSync = true;
});
Now you will have two-way binding hooked-up on the 'inSync' property.
Otherwise, your template looks fine.
If I'm off base, please update your question with more context, or better yet make a fiddle to boil down the problem.

Update scope value when service data is changed

I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>

Resources