Angular where to place $watch - javascript

I have a promise object from which my page gets data something like:
promise.then(function (data) {
$scope.myData = data;
});
and outside of the then callback I have watches on objects on the page. If I am going to watch some piece of data that my promise provides, should I always place my #watches inside of the then callback of the promise?

When you $watch an object, it's really just registering a subscription to changes of the existing object. It won't run until a change is registered, so having it inside the resolve of a promise isn't necessary.

I would have thought in principle you would only want to setup your $watch once after you have declared the variable receiving the data. Without seeing the code it's difficult to be certain but I would expect problems if I set a $watch each time a promise returned.
The callback function of the $watch is passed both the old and new value so you can inspect the values to determine watch action to take.

You should retrieve your data from a service:
app.factory('myService', function($http) {
var someData = [];
return {
getData: function() {
var promise = $http({method:'GET', url:'/api/someurl' });
promise.success(function(data) {
angular.copy(data, someData);
});
return promise;
},
data: someData
}
});
And then assign the data to your scope inside your controller
app.controller('ctrl', function($scope, myService) {
$scope.data = myService.data;
});
Consume the data within your HTML, or from inside your directive
<body ng-app="app">
<ul>
<li ng-repeat="item in data">{{ item }}</li>
</ul>
<my-directive model="data"></my-directive>
</body>
If you do go with a custom directive, it is recommended to setup your $watch inside your directive's link function:
app.directive('myDirective', function() {
return {
restrict: 'E',
link: function(scope, elem, attr) {
scope.$watch('data', function(newVal) {
...
});
}
}
});
There is no reason for $watch handlers to be inside the callback of a promise. It should remain separate so that there is a clean separation between how the data is retrieved from the service and how the data is consumed from the view.

Related

How to pass data from directive to controller

I've built a custom directive in my app which utilizes D3.js. I want to be able to make an API call to load more data when a user clicks on a node within my D3 visualization. This will require grabbing the data associated with the node which was clicked and passing it back to my controller. The controller then handles calling a function to retrieve more data.
To get started I'm simply trying to log the data associated with the node a user clicked in my controller. My problem is that this data is undefined in my controller.
Relevant directive code:
angular.module('gameApp')
.directive('gmLinkAnalysis', gmLinkAnalysis);
gmLinkAnalysis.$inject = ['$location', 'd3'];
function gmLinkAnalysis($location, d3) {
var directive = {
restrict: 'E',
templateUrl: '/app/gmDataVis/gmLinkAnalysis/gmLinkAnalysis.directive.html',
scope: {
data: '=',
logNode: '&'
},
link: function(scope) {
...
function click(d) {
scope.logNode(d);
}
}
};
return directive;
}
HTML:
<gm-link-analysis data="connections.users" log-node="connections.logNode(d)"></gm-link-analysis>
Relevant controller code:
angular.module('gameApp')
.controller('ConnectionsController', ConnectionsController);
function ConnectionsController() {
var vm = this;
...
vm.logNode = function(d) {
console.log(d);
};
}
If I replace d in my html with a string such as "hello world" (log-node="connections.logNode('hello world')") it is properly logged. So clearly my issue lies in not properly passing my data as the parameter in my html. How would I go about doing so?
you need to specify the parameter in the call:
so in your directive it should be
function click(d) {
scope.logNode({d: d})
}
here is an example:
http://jsfiddle.net/heavyhorse/7983y06k/
You may pass a model attaching some methods into the directive but I personally prefer using $.broadcast service to keep my codebase cleaner.
Directive
function click(d) {
$rootScope.$broadcast('someEvent', d);
}
Controller
angular.module('gameApp')
.controller('ConnectionsController', ConnectionsController);
function ConnectionsController() {
var vm = this;
vm.$on('someEvent', function(event, data) {
console.log(data)
});
}
If you still think that passing methods around would make the trick for you, here is a simple example passing a method to the directive via a data model

Angular directive isolate scope to parent binding undefined

I'm using (the awesome) Restangular and i'm running into something that forces me to use scope.$parent (not awesome), and i don't want to use that. It seems even though my controller is the parent scope to my directive's scope, the = isolated scope binding is evaluated before my parent controller is executed.
With the following HTML:
<div ng-controller="myController">
<div x-my-directive x-some-value="parentValue"></div>
</div>
And the following directive:
myApp.directive("myDirective", function () {
return {
restrict: 'A',
link: function (scope, elem) {
console.log(scope.someValue); // Logs 'undefined' :(
},
scope: {
someValue: "="
}
}
});
And the following controller:
myApp.controller("myController", function($scope, allMyValues) {
allMyValues.getList().then(function(parentValue){
$scope.parentValue = parentValue;
});
}
As shown in my directives link function, evaluating a scope property that should have been bound to my parent's scope property returns undefined. However when i change my directives link function to the following:
myApp.directive("myDirective", function () {
return {
restrict: 'A',
link: function (scope, elem) {
setTimeout(function() {
console.log(scope.someValue); // Logs '{1: number_1, 2: number_2}'
}, 2000);
},
scope: {
someValue: "="
}
}
});
How do i go about resolving this??
Thanks
that should helps:
myApp.controller("myController", function($scope, allMyValues) {
//add this line
$scope.parentValue={};
allMyValues.getList().then(function(parentValue){
$scope.parentValue = parentValue;
});
}
$scope.parentValue not exist until your request is resolved so add line like below to your code
sample demo http://jsbin.com/komikitado/1/edit
Looks like you are waiting for a promise to resolve before assigning the value to the scope.
There are a few ways you might handle this.
One way is to try moving the Restangular call to a resolve function for the view which holds the controller. Then you get access to the resolved data directly as an injection in your controllers
Another way might be to just assign the promise directly to the scope and then in the linking function wait for a resolution.
scope.someValue.then(function(value) { console.log(value); });

How to set a variable in different controller in AngularJS?

I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.

Preloading data in a Directive using promises in a service using AngularJS

I hope someone can help me. I have not been able to figure this one out.
I wrote a directive (see below) to dump a pre-written ul-list on a page based on html data that I retrieved async from a database server. Both the Directive and The Service work.
I assumed that the "then" in "MenuService.getMenuData().then" would force a wait until the data arrived to the directive but some how the directive completes and shows the '3empty' message before the data arrived, which indicates that the directive completed earlier. I know I could put a timeout delay but that is not good. Do you have a suggestion as to what could the problem be?
The other technique I used was to put a ng-show="dataarrived" and set the dataarrived to true only when the promised completed. But same issue.
The purpose of this directive is to retrieve the Nav menu list from the serve and display it on the index.html but It does Not matter if I put this code in a controller or in a service or directive I get the same result. It shows nothing. It is particular to displaying it in the index.html before any other view is displayed.
Here is my directive if it make sense.
TBApp.directive('tbnavMenu', function ($compile, MenuService) {
var tbTemplate = '3empty';
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
var getTemplate = function () {
return tbTemplate;
}
var linker = function (scope, element, attrs) {
element.html(tbTemplate).show();
$compile(element.contents())(scope);
}
return {
restrict: "E",
replace: true,
link: linker,
controller: function ($scope, $element) {
$scope.selectedNavMenu = GlobalService.appData.currentNavMenu;
$scope.menuClicked = function ($event, menuClicked) {
$event.preventDefault();
$scope.selectedNavMenu = menuClicked;
$scope.tbnavMenuHander({ navMenuChanged: menuClicked });
};
$scope.isSelected = function (menuClicked) {
return $scope.selectedNavMenu === menuClicked;
}
},
scope: {
tbnavMenuHander: '&'
}
}
}
I could be incredibly wrong but if your service is returning an $http object at the getMenuData method then these lines:
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
should change to either:
MenuService.getMenuData().then(function (val) {
tbTemplate = val.data;
});
or
MenuService.getMenuData().success(function (val) {
tbTemplate = val;
});
My personal recomendation is to use the .then option as it enables the concatenation of more promises.

Callback function inside directive attr defined in different attr

So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});

Categories

Resources