In an angular app running on 1.3.0-beta.15, I have a directive that looks like this:
.directive('digester', function() {
return {
scope: {
object: '=',
digestTrigger: '='
},
templateUrl: 'some-template.html,
link: function(scope) {
scope.digestTrigger = function() {
scope.$digest();
}
}
};
});
a template that looks like this:
<div>{{object.title}}</div>
a service somewhere that looks like this:
.service('serviceToCallDigest', function() {
return {
object: {
title: 'someTitle'
},
digestTrigger: function() {},
someFunction: function() {
this.object.title = 'newTitle';
this.digestTrigger();
}
};
});
and a controller that gets the service injected like this:
.controller('someCtrl', function($scope, serviceToCallDigest) {
$scope.serviceToCallDigest = serviceToCallDigest;
}
and wires up the directive like this:
<digester
digest-trigger="serviceToCallDigest.digestTrigger"
object="serviceToCallDigest.object">
</digester>
Whats happening here is, as the directive gets the digestTrigger injected (as an empty function) it sets the function to fire the directives local $digest() function, and by that enabeling code from outside the directive to trigger the local digest whenever appropriate.
I know that $apply() is adviced to use, which fires $digest() at the $rootscope level, but I'm in a perfomance critical situation, where I need to update directives locally.
This setup has worked fine until upgrading to 1.3.0-beta.16 or newer versions. Now the new object.title is not passed to the directive and updated in the view, when someFunction is called.
Does anyone know why this is the case and what to do to work around it?
Related
New to creating custom directives. It renders fine on the initial render. However, I am trying to $watch for changes to the original data, and then, trigger an update.
As a quick test, I created a button and used jQuery to update the costPerDay.costs array (by hand)...but the $watch still doesn't fire & my breakpoint wasn't reached.
Thanks for the help...
MY CONTROLLER LOOKS LIKE:
The GET is mocked to return an object, not a promise, so ignore that particular line. Once I get the $watch working, I will update this part of the code accordingly.
// CONTROLLER
application.controller('HomeIndexController', function ($scope, costPerDayDataService) {
var vm = this;
// Internal
vm.on = {
databind: {
costPerDay: function () {
// The GET is mocked to return an object, not a promise, so ignore this line
var costPerDay = costPerDayDataService.get();
$scope.data.costPerDay = costPerDay;
}
}
};
vm.databind = function () {
vm.on.databind.costPerDay();
};
// Scope
$scope.data = {
costPerDay: {}
};
$scope.on = {
alterCosts: function (e) {
var costs = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300];
$scope.data.costPerDay.costs = costs;
}
}
// Databind
vm.databind();
});
MY ELEMENT LOOKS LIKE:
This renders fine initially. I need to automate updates.
<ul id="sparks" class="pull-down pull-left">
<li cost-per-day-sparkline costperday="data.costPerDay">
</li>
</ul>
MY DIRECTIVE LOOKS LIKE:
I am just trying to get ONE of them to work...I will obviously remove the others when I get a working example. And yes, I am aware you should NOT update the $parent directly. I'm just trying to find a combination that works before I get fancy.
define([], function () {
'use strict';
function CostPerDaySparklineDirective() {
return {
replace: true,
restrict: "AE",
scope: {
costperday: "=costperday"
},
templateUrl: '/modules/templates/sparklines/costperdaysparklinetemplate.html',
link: function (scope, elem, attrs) {
// This fails
scope.$watch('costperday', function (newval) {
// ... code to update will go here
}, true);
// This fails
scope.$watch('costperday', function (newval) {
// ... code to update will go here
});
// This fails
scope.$parent.$watch('data.costPerDay.costs', function (newval) {
// ... code to update will go here
});
// This renders initially, but fails to fire again
scope.$watch('scope.$parent.data.costPerDay.costs', function (newval) {
var eleSparkline = $('.sparkline', elem);
eleSparkline.sparkline(scope.costperday.costs, { type: "bar" });
});
}
};
}
return CostPerDaySparklineDirective;
});
UPDATE:
Even using ng-click to test the $watch fails to hit the breakpoint...
<a ng-click="on.alterCosts()">Change Costs</a>
In this case I'd run $scope.$apply(); in your alterCosts method to trigger a template digest. This will update the value in the DOM, which your directive catches, and subsequently triggers the $watch.
For more information on $apply(), https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
"$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events.."
In this particular scenario you're changing the value from a DOM event.
in this situation I would watch the actual get of the costPerDayDataService vs listening to the controllers scope variable. so in your controller you would 'set' the variable in costPerDayDataService and in your directive you would just inject your service and watch the get function. OR if you are using 1.3.x > you can use bindToController which I believe eliminates the whole need for watches.
bindToController: {
costperday: '='
}
I've got a ng-repeat creating multiple directives, each with an isolated scope. I want to be able to call a single function that calls a function on each directive, so it resets a variable on each isolated scope. I can't work out anyway to do this? I realise this might not be best practice, but I just need a solution at the moment.
Another option that i use in a similar vein is angulars event system.
I'm creating a dashboard of widgets. My widget is a directive contained within a ng-repeat.
I emit events in my controller $scope.$broadcast then listen in my directive with $scope.$on. I use the $index of the ng-repeat to be able to target specific widgets.
quick example fiddle: http://jsfiddle.net/adyjm9g4/
EDIT: Forgot to mention you can also pass data: http://jsfiddle.net/adyjm9g4/1/
A way to do it is to provide a callback to the directive (i.e. scope: { xxx: '&' }) that will execute some functionality. Could be:
<the-directive callback="ctrl.command(action, argument)" />
And the directive looks like:
app.directive('theDirective', function() {
return {
...
scope: {
callback: '&',
...
},
controller: ['$scope', function($scope) {
this.resetTheVariable = function() {
// DO WHAT YOU WANT HERE
};
$scope.callback({ action: 'register', argument: this });
$scope.$on('$destroy', function() {
scope.callback({ action: 'deregister', argument: this });
});
}]
};
})
Now the controller invoking this directive would look like:
function TheController() {
var registeredDirectives = [];
this.command = function(action, argument) {
switch(action) {
case 'register':
registeredDirectives.push(argument);
break;
case 'deregister':
var index = registeredDirectives.indexOf(argument);
if( index >= 0 ) {
registeredDirectives.splice(index, 1);
}
break;
}
};
this.resetAll = function() {
registeredDirectives.forEach(function(registeredDirectiveController) {
registeredDirectiveController.resetTheVariable();
});
};
}
I've been with Angularjs a few days and I'm struggling with a few aspects of it. I'll do my best to try and explain what the issue is, and I'd really appreciate any help anyone can give me about it.
My situation (simplified) is this:
I have a service which loads some info from a json and stores it in an object. It also have some functions to be used for other controllers to retrieve that information.
var particServices = angular.module('particServices', []);
particServices.service('particSrv', function() {
var data = {};
this.updateData = function(scope) {
data = // http call, saves in data
}
this.getName = function(code) {
return data.name;
}
});
I have an html page assisted by a controller, which uses a directive board (no params, really simple). This is the controller:
var bControllers = angular.module('bControllers', []);
bControllers.controller('bController', ['$scope', 'particSrv', function ($scope, particSrv) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
particSrv.updateData($scope);
}]);
As you can see, the controller makes the call to initialize the object in the service. As this is a singleton, I understand once that info is loaded no other call needs to be make to updateData and that info is available to others using the getters in the service (getName in this case).
I have a really simple directive board (which I simplified here), which uses another directive bio.
angular.module('tsDirectives', [])
.directive('board', ['dataSrv', 'particSrv', function(dataSrv, particSrv) {
return {
restrict: 'E',
replace: true,
scope: true,
controller: function($scope) {
$scope.getName = function(code) {
return particSrv.getName(code);
};
dataSrv.updateData($scope, 'board', 'U');
},
templateUrl: '<div class="board"><div bio class="name" partic="getName(code)"/></div></div>'
};
}]);
And this is the bio directive:
angular.module('gDirectives', [])
.directive('bio', function() {
return {
scope: {
partic: '&'
},
controller: function($scope) {
$scope.name = $scope.partic({code: $scope.athid});
},
template: '<a ng-href="PROFILE.html">{{name}}</a>'
};
})
Now, what I expected is that in the bio directive the info retrieved from party was displayed, but apparently this directive is processed before the partic is initialized in the main controller.
I was under the impression that even though this information was still not loaded when the directive is processed, as soon as the service finishes and the info is ready, automagically it would appear in my directive, but that does not seem to work like that. I've been reading about $watch and $digest, but I fail to see why (and if) I would need to call them manually to fix this.
Any hint will be much appreciated. I could provide more technical details if needed.
Directive will initialise when app is loaded and user opens the page where that directive is, if you have some property that is set later (from api for example), it will update that property in directive but that directive will not be reinitialised ($scope.partic({code: $scope.athid}) wont be called).
If you want for directive to wait for initialisation you should use ng-if. Something like this:
<div data-directive-name data-some-property="someProperty" data-ng-if="someProperty"></div>
In this case directive will be initialised when (if) you have some value in $scope.someProperty. But this is not very good if you can have false values for someProperty.
In that case you would need to use some kind of loaded flag.
You have not included "particServices" as a dependency in other modules which use the services of "particServices". Your modules should look like:
var bControllers = angular.module('bControllers', ['particServices']);
angular.module('tsDirectives', ['particServices']);
angular.module('gDirectives', ['particServices']);
I've written a pretty simple test app as follows:
angular.module('tddApp', [])
.controller('MainCtrl', function ($scope, $rootScope, BetslipService) {
$scope.displayEvents = [
{
id: 1,
name: 'Belarus v Ukraine',
homeTeam: 'Belarus',
awayTeam: 'Ukraine',
markets: {home: '2/1', draw: '3/2', away: '5/3'},
display: true
}
];
$scope.betslipArray = BetslipService.betslipArray;
$scope.oddsBtnCallback = BetslipService.addToBetslip;
$scope.clearBetslip = BetslipService.clearBetslip;
})
.directive('oddsButton', function () {
return {
template: '<div class="odds-btn">{{market}}</div>',
replace: true,
scope: {
market: '#',
marketName: '#',
eventName: '#',
callback: '&'
},
link: function (scope, element) {
element.on('click', function() {
scope.callback({
name: scope.eventName,
marketName: scope.marketName,
odds:scope.market
});
});
}
};
})
.factory ('BetslipService', function ($rootScope) {
var rtnObject = {};
rtnObject.betslipArray = [];
rtnObject.addToBetslip = function (name, marketName, odds) {
rtnObject.betslipArray.push({
eventName: name,
marketName: marketName,
odds: odds
});
};
rtnObject.clearBetslip = function () {
rtnObject.betslipArray = [];
};
return rtnObject;
});
I've assigned an array to a controller variable. I've also assigned functions to modify the array. To add an object to the array the callback is called by a directive with isolate scope. There's some strange behaviour happening that I don't quite understand:
=> clicking the directive runs the callback in the service. I've done some debugging and it seems that the controller variable is updated but it doesn't show in the html.
=> clicking the button to clear the array isn't working as expected. The first time it's causing an element to display, after which it has no effect.
I think that this may have to do with the nested ng-repeats creating their own scopes
NB
I fixed the array not clearing by changing the function in the service to:
while (rtnObject.betslipArray.length > 0) {
rtnObject.betslipArray.pop();
}
// instead of
rtnObject.betslipArray = [];
This makes sense as the service variable was being pointed at a new object while the old reference would persist in the controller.
I got the html to update by wrapping the callback call in the directive in a scope.$apply().
This part I dont really understand. How can scope.$apply() called in the directive have an effect on the controller scope when the directive has an isolate scope? updated fiddle: http://jsfiddle.net/b6ww0rx8/7/
Any thought's greatly appreciated
jsfiddle: http://jsfiddle.net/b6ww0rx8/5/
C
I got it working here: http://jsfiddle.net/b6ww0rx8/8/
Added $q, $scope.$emit and $timeout clauses to help with communications between your directive / service and controller.
I would like to also say that I wouldn't assign service functions to a controller $scope, You should define functions in the controller that call service functions.
Instead of this:
$scope.clearBetslip = BetslipService.clearBetslip;
Do this:
$scope.clearBetslip = function(){
BetslipService.clearBetslip().then(function(){
$scope.betslipArray = BetslipService.getBetslipArray();
});
};
myApp.directive('myView', ['myService',function($scope) {
return {
restrict : 'C',
template : '',
templateUrl : 'myView.html',
controller : function($scope,myService){
$scope.items = myService.$data;
$scope.$watch('myService.$data' , function(newVal, oldVal, scope) {
console.log("Watching items ...");
});
},
};
}]);
I am using the above code, where I want to watch myService.$data which is defined in its service. $watch gets called first time when controller is loaded, Elsewhere in the code I update the service variable $data. This gets reflected in UI as I am using ng-repeat directive and binding with $data.
According to my understating before UI gets updated, this particular watch has to be called again, but it is not. So I assume I am not using watch properly.
Just do:
$scope.$watch(
function() { return myService.$data; },
function(...) { /*same*/ }
);
BUT:
If $data is an object you may want to add ,true to the watch, so that you get notified even if an inner property changes:
$scope.$watch(
function() { return myService.$data; },
function(...) { /*same*/ },
true
);
The declarations in your code are wrong. It should be (only changes shown):
myApp.directive('myView', ['myService',function(myService) { // NOT $scope
...
controller: ["$scope", function($scope) { // YOU GET myService FROM THE DIRECTIVE