The use of $watch in directive - angularjs

I've rendered a chart using highchart.js using solutions from a couple questions. I understand the basic use of directives. However, in the case of highchart.js, I don't quite understand this code here:
app.directive('highchart', function () {
var direc = {};
var link = function (scope, element, attributes) {
scope.$watch(function () {
return attributes.chart;
}, function () {
var charts = JSON.parse(attributes.chart);
$(element[0]).highcharts(charts);
})
}
direc.restrict = 'E';
direc.link = link;
direc.template = '<div></div>';
//the replace method replaces the content inside the element it is called
direc.replace = true;
direc.scope = {};
return direc;
})
The charts attribute will accept a JSON array of chart attributes.
Can someone explain what's happening inside the function? Thank you for reading.

The $watch is used to monitor the changes on a specific field. In the above case the attributes.chart is being watched for changes in the first argument in the $watch function and the second argument works with actually checking the modified data and performing manipulation on it.
You can also find further options that can be used by the $watch in the official angular docs: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch

$watch monitor the model chnanges, once the model chnages , updated values get from below code and as per requirement, required action can perform.
$scope.$watch('ng-model-name', function (newval, oldval) {
if (newval< 0) {
$('#modelCustomer').modal('show');
}
});

Related

AngularJS - share variable between two controllers

I have two controllers that have to communicate each other.
The first reference to a video player and the second one to a timeline.
From the first one, I get the currentTime of the video playback and I want to pass it to the second one that should move the time-bar as the video is playing.
I tried using the factory to share a variable called time between controllers but this doesn't change during the time.
First Controller:
angular.module('videoCtrl', ['vjs.video'])
.controller('videoController', ['$scope', 'Timeline', function (scope, Timeline) {
scope.mediaToggle = {
sources: [
{
src: 'http://static.videogular.com/assets/videos/videogular.mp4',
type: 'video/mp4'
}
],
};
//listen for when the vjs-media object changes
scope.$on('vjsVideoReady', function (e, videoData) {
videoData.player.on('timeupdate', function () {
var time = this.currentTime();
Timeline.setTime(time); // setting the time on factory
})
});
}]);
Second Controller:
angular.module('timelineCtrl', ['mt.media-timeline'])
.controller('timelineController', function ($scope, Timeline) {
$scope.time = Timeline.getTime(); // here I'm trying to get the time
});
Factory:
.factory('Timeline', function(){
var timelines = [];
var time = null;
return {
getTime: function() {
return time;
},
setTime: function(_time) {
time = _time;
}
}
});
time appears to be a primitive, which means it is returned byVal rather than byRef. In other words, each call to getTime will return the value that time is currently set to, and calls to setTime will change the value for future calls, but not for anything that already called it. This is a classic case of the angular rule, Always use a dot.
Try changing time to an object instead:
.factory('Timeline', function() {
var timelines = [];
var time = {
value: null
};
return {
getTime: function() {
return time;
},
setTime: function(_time) {
time.value = _time;
}
}
});
In your HTML, use {{time.value}}.
Saving in $rootScope instead of $scope would give you the ability to access a variable across all your app and your controllers. But have in mind that creating a large number of $rootScope could affect your app's performance.
Do not forget to inject $rootScope into the controller (like you did with $scope), so you can access it.
Well as far as I can tell what're doing in the second controller is that you retrieve the value of time on instantiation of the controller. Of course further changes of the value in the service can't be picked up this way. To do that can use $scope.$watch in the second controller:
angular.module('timelineCtrl', ['mt.media-timeline'])
.controller('timelineController', function ($scope, Timeline) {
$scope.time = Timeline.getTime(); //Set the time once so it's not undefined
$scope.$watch(
function() {return Timeline.getTime();},
function(newVal) {$scope.time = newVal;}
);
});
Angular will call the first function in every $digest cycle(That's about at least every 10ms if I recall correctly) and will call the second function when a change has been detected. Detailed documentation for $watch can be found here
This is one way to do it. You could also add a function to your $scope(e.g. getTime()), which should return the current time, and then call this function in the HTML template: {{getTime()}}. Both ways pretty much work the same way, except that the second one leaves the 'dirty' work to angular(creating watchers and updating values)

Angularjs watchGroup() not working with Service changes

I'm having problems to watch multiple variables in a Service when some changes are made on it by other controllers.
I've the following:
angular.module('carApp').controller('CarListCtrl', function ($scope, CarService) {
$scope.carList = CarService.carList
$scope.initalAmount = CarService.initialAmount
$scope.startDate = CarService.startDate
$scope.$watchGroup(['carList', 'initialAmount', 'startDate'],
function (newValues, oldValues, $scope) {
console.log(newValues);
});
});
Other controllers update the values in the Service all the time but the watchGroup never fires up...
I've create a simple watch targeting the service directly to check if work, and it's working ..., so I imagine that the watchGroup should target the service variable directly but I can't find how to do that....
here is the simple watch that works:
$scope.$watch(function () {
return CarService.carList
}, function (newVal, oldVal) {
console.log(newVal);
});
What should I do to make it works with the multiple service variables?
UPDATE 1:
Just a update... if I try the watchgroup with just one element, for example $scope.$watchGroup(['carList'], ... it works, so I tried with each one and it works every time, but as soon as I add one more element it stop working... very annoying...
Tks again guys!
Just to close this, people from angularjs github help me out: here is the anwser for anyone who need:
Each value inside a watchGroup array can be a expression or a function, so you can use three different functions inside your watchGroup. http://plnkr.co/edit/nMmPt808xAFXqjJ6yEoc?p=preview
$scope.$watchGroup([
function() {
return myService.valueOne()
},
function() {
return myService.valueTwo()
}
], function(newValues, oldValues) {
$scope.valueOne = newValues[0]
$scope.valueTwo = newValues[1]
})
Your first example is possibly not working because your other controllers assign new values to initialAmount and startDate in your service, which means you have different objects with different values in your controller and your startDate. It´probably works with the carList, because you are only adding / removing items, which means it remains the same object in your controller and your service.

Angular form 1.3 tightly coupled

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

Accessing Data set via a Promise in an Angular Directive

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!!!

How can I get a change to the Model (from within a Directive) to update in the View? $apply?

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 });
});
});
}
}
});

Resources