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: '='
}
Related
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();
});
};
}
Does anyone of you think that the form in angular 1.3 is causing tightly coupling between the controller and the DOM? This is an anti pattern of Angular.
For example, when giving a form the name='formExample' attribute, to set it to dirty or invalid programmily, in the controller we have to do $scope.formExample.$setDirty()
This is a bad practice!
Waiting to hear your thoughts!
Example :
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
ProjectsBLL.update($scope.entities.project.Model, function (data) {
$scope.entities.project.Model = data;
$scope.configurationForm.$setPristine();
that.saveMessage = that.SAVING_FINISHED;
}, null)
}
It sounds like you don't want the following line in the controller
$scope.configurationForm.$setPristine();
To achieve this, one way is to:
Change ProjectsBLL.update to return a promise rather than accept a callback (how to do this is probably beyond the scope of this question). Then also return the derived promise from then from the function in the controller:
this.onSaveClicked = function () {
that.saveMessage = that.SAVING_IN_PROGRESS;
//Update project with changes
return ProjectsBLL.update($scope.entities.project.Model).then(function (data) {
$scope.entities.project.Model = data;
that.saveMessage = that.SAVING_FINISHED;
});
}
And then use a custom directive rather than ng-click to call the save function. There are probably a few ways of doing this, but one is to add one that takes a function as an attribute:
<button type="button" my-submit="onSaveClicked()">Save</button>
which adds a manual click listener, calls the passed function on click, and after its returned promise is resolved, calls $setPristine() on a requireed form:
app.directive('mySubmit', function($parse) {
return {
require: '^form',
link: function(scope, element, attrs, formController) {
var callback = $parse(attrs.mySubmit);
element.on('click', function() {
callback(scope, {}).then(function() {
formController.$setPristine();
});
});
}
};
});
A version of this, just using a $timeout to simulate a call to $http can be seen at http://plnkr.co/edit/IdcfQ4N9MhDKdvyorzeO?p=preview
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();
});
};
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?
There are a lot of questions/answers here on stackoverflow and out on google about this topic ($apply), and I feel like I have read every one and followed them, but to no avail. All my searches on google now return purple links.
Here's the issue I'm facing (trying to be specific without overkill):
I have an app which pulls data via a webservice, storing the records in an array. I have created a drag/drop target to upload an excel file with changes/additions to the data. I have created a directive for the drop target, which binds the event listeners to the element. I have isolated the scope, using & to bind a function from the controller into the directive. The function in the controller processes the file dropped and updates the model. Here is the setup:
HTML
<div ng-controller="myController as vm">
<div class="row">
<div class="col-lg-12">
<div drop-target drop="vm.drop(files)">
<p>Drag an XLSX file here to import.</p>
</div>
</div>
</div>
</div>
I also have a table afterward with ng-repeat to display the records.
Controller
app.controller('myController', ['dataService', '$scope', function (data, $scope) {
var vm = this;
vm.data = data;
vm.drop = function (files) {
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson); //call function to update model with objects
};
reader.readAsBinaryString(files[0]);
}
function importLines(lines) {
//do a bunch of validation and update model data
}
}
Directive
app.directive('dropTarget', function () {
return {
restrict: 'A',
scope: {
drop: '&'
},
link: function (scope, el, attrs, controller) {
el.bind("dragover", function (e) {
...
});
el.bind("dragenter", function (e) {
...
});
el.bind("dragleave", function (e) {
...
});
el.bind("drop", function (e) {
if (e.preventDefault) { e.preventDefault(); } // Necessary. Allows us to drop.
if (e.stopPropogation) { e.stopPropogation(); } // Necessary. Allows us to drop.
var files = e.originalEvent.dataTransfer.files;
scope.$apply(function () {
scope.drop({ files: files });
});
});
}
}
});
So everything I have read online seems to indicate that I should wrap my call to the controller function in $apply(), as you see I have done. All of the functionality of the drag/drop and updating the model works fine. However, the view does not update. The model is updated--I can see it in the console. And when I trigger any other Angular activity (clicking some button that has an ng-click or checking a checkbox that has ng-change, etc.), the whole UI updates and I can see all the updates to the model.
If I wrap the call to importLines in the controller with $apply(), it works great. But my understanding is the $apply() call should be done within the directive...try to avoid it in the controller.
I can't for the life of me figure out why it doesn't work. It seems to follow the literally dozens and dozens of examples on forums and blogs about $apply() that I have read. I am still fairly new to angular--I know I don't fully understand some of the forums/blogs when they start discussing $digest cycles and such (but I know a lot more than I did a couple days ago). I understand that some changes to the model are done outside of angular's context, so you have to call $apply(). Some answers have said that the link function of a directive is within angular's context, others no. I don't get any errors (no $apply within $apply). Everything runs smoothly...it just doesn't update the view.
What am I missing? Any help would be appreciated.
UPDATE:
Thanks Erti-Chris for the answer. I ended up putting a $q promise in the vm.drop function, resolving it after the importLines function finishes. In the directive, when I left the scope.$apply(), I was getting an error that an $apply was already in process. But I still had to have a .then on the function call, even though it's empty. Without it, it wouldn't work right.
Controller
app.controller('myController', ['dataService', '$scope', function (data, $scope) {
var vm = this;
vm.data = data;
vm.drop = function (files) {
var deferred = $q.defer(); //Added
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson);
deferred.resolve(); //Added
};
reader.readAsBinaryString(files[0]);
return deferred.promise; //Added
}
function importLines(lines) {
//do a bunch of validation and update model data
}
}
Directive
scope.drop({ files: files }).then(function (r) {
// Do nothing here, but doesn't work without .then
});
It updates right away now. Thanks for the help!
I would expect the FileReader class to be asynchronous. It doesn't make sense that it would block in JS. Thus that would explain why your scope.apply is not working in directive. The reason is: scope apply runs BEFORE importLines has chance to run(as it is asynchronous).
You can look into $q(a promise object, which will solve it nicely) or you can create a callback function which will "notify" directive that importLines has finally been done.
vm.drop = function (files, doneCallback) {
var reader = new FileReader();
reader.onload = function (e) {
... Read and parse excel sheet into array of objects ...
importLines(wbJson); //call function to update model with objects
if(doneCallback)
doneCallback();
};
reader.readAsBinaryString(files[0]);
}
and in directive:
scope.drop({ files: files }, scope.$apply);
Directives can have an isolated scope or it can share the scope with the parent scope. However in your case you are making use of the isolated scope by declaring
scope: {
drop: '&'
},
in your directive. & allows the directive's isolate scope to pass values into the parent scope for evaluation in the expression defined in the attribute. Instead, = sets up a two-way binding expression between the directive's isolate scope and the parent scope. If you make use of
scope: {
drop: '='
},
in your directive then it will pass that scope object completely and make use of the two way binding.
You can make use of ngModel in your directive.
app.directive('dropTarget', function () {
return {
restrict: 'A',
require:'^ngModel',
link: function (scope, el, attrs, controller,ngModel) {
el.bind("dragover", function (e) {
...
});
el.bind("dragenter", function (e) {
...
});
el.bind("dragleave", function (e) {
...
});
el.bind("drop", function (e) {
if (e.preventDefault) { e.preventDefault(); } // Necessary. Allows us to drop.
if (e.stopPropogation) { e.stopPropogation(); } // Necessary. Allows us to drop.
var files = e.originalEvent.dataTransfer.files;
scope.$apply(function () {
ngModel.$setViewValue({ files: files });
});
});
}
}
});