Why does event stop being caught when "ng-switch"-ing - angularjs

I have a file upload directive that fires events (through scope.$emit) while the file is being uploaded that is then caught in my controller (through scope.$on) to update a progress bar. The file upload directive gets ng-switch-ed out so the user can fill in other information about the file while the file is being uploaded. I want the progress bar to keep getting updated while they're doing this. This was all working just fine with version 1.0.2, but it seems to be broken now in 1.0.4.
The jsFiddle I created is greatly simplified, but it shows the problem. When the page first loads, the event is fired from the directive and it gets caught in the controller and everything gets updated as expected. However, if you change the dropdown so that the directive gets ng-switch-ed out, the event stops getting caught. If you look in the console, the event is still gettting $emit-ed, but the controller just stops catching it. Even if I put the $on on the $rootScope(http://jsfiddle.net/uysAM/1/), it still has the same problem. If the event is still being $emit-ed, why would the scopes stop catching it?
If you change the src in the <script> tag at the top to use 1.0.2, it works just fine. Is this some new bug with the latest version? Thanks for any help.

This is a scope problem. Here is how I understand it.
The scope chain when foo=1
MyCtrl's scope <- We catch here
ngSwitch's scope for foo="1"
myDirective's scope <- We emit here
The scope chain is intact and we can catch the emitted event without problem.
The scope chain changes when foo=2
MyCtrl's scope
null <- Is destroyed and set to null for memory management.
myDirective's scope
The scope chain is no longer intact. The emitted event doesn't propagate past myDirective's cleaned up parent scope. It is possible that this cleanup was added as a feature in 1.0.4.
Here is my solution to the problem. I use a fileLoader service to share state between the directive and the controller.http://jsfiddle.net/apBZX/
var myApp = angular.module("myApp", [])
myApp.controller('MyCtrl', function ($scope, $rootScope, fileLoader) {
$scope.$watch(function () {
return fileLoader.numberOfloadedFiles;
}, function (numberOfLoadedFiles) {
$scope.numberOfLoadedFiles = numberOfLoadedFiles;
});
$scope.foo = 1;
});
myApp.directive("myDirective", function (fileLoader) {
return {
restrict: 'A',
replace: true,
scope: {},
template: '<div>I am the directive</div>',
link: function (scope, element, attrs) {
if (!fileLoader.isLoading) {
fileLoader.loadFiles(scope);
}
}
}
});
myApp.service("fileLoader", function () {
return {
numberOfloadedFiles: 0,
isLoading: false,
loadFiles: function (scope) {
var self = this;
setInterval(function () {
scope.$apply(function(){
self.numberOfloadedFiles++;
});
}, 1000);
self.isLoading = true;
}
};
});

Related

Angularjs 1.5 component modal with callback function not updating binding [duplicate]

In IE 11, I have an Angularjs 1.5 modal component as below. The modal opens and on render event it calls a function outside of the angular app with a callback function contained in this component. This outside function initiates a install process which kicks off an embedded object as shown below and this then periodically calls the callback function.
The issue I am having is the binding is not being updated in the template on each callback function called from the embedded object call. The console.log is executed and i can see the message in the console.
The binding is initially updated with 'starting process' so binding is correct
<span ng-bind="$ctrl.messages[$ctrl.messages.length - 1]"></span>
I tried calling scope.apply as below but nothing happens. Only when the initiateprocess is completed, the binding is then updated with the last message shown from the final callback call. So the initiateprocess function is blocking the binding but no blocking the console.log's
is this the correct way to handle multiple callbacks and updating bindings
angular.module('components')
.component('testModal', {
bindings:{
modalInstance: '<',
resolve: '=',
dismiss: '&',
close: '&'
},
controller: TestController,
templateUrl: 'scripts/components/TestModal.html'
});
TestController.$inject = ['$scope'];
function TestController($scope) {
var ctrl = this;
ctrl.$onInit = function(){
ctrl.messages = [];
ctrl.messages.push('starting process');
};
ctrl.modalInstance.rendered.then(function(){
CallVanillaJSFunction(callback);
});
function callback(message){
ctrl.messages.push(message);
console.log(ctrl.messages[ctrl.messages.length - 1]);
CheckScopeBeforeApply();
}
function CheckScopeBeforeApply() {
if(!$scope.$$phase) {
$scope.$apply();
console.log('scope applied');
}
};
}
Vanilla Function
var globalCallback;
function CallVanillaJSFunction(callback){
globalCallback = callback;
var complete = initiateprocess();
globalCallback(complete);
}
Embedded Object
<OBJECT ID="testObj" CLASS......
<SCRIPT language=javascript for=testObj event="OnEvent(message);">
if(navigator.userAgent.indexOf("Trident") != -1)
{
globalCallback(message);
}
</SCRIPT>
This question has been marked as duplicate but having looked at the duplicates I don't think it is the same. The global callback function can be called multiple times and the angular application does not know how many times it will be called.
Use the $timeout service to force a browser tick:
function callback(message){
$timeout(function() {
ctrl.messages.push(message);
console.log(ctrl.messages[ctrl.messages.length - 1]);
});
̶C̶h̶e̶c̶k̶S̶c̶o̶p̶e̶B̶e̶f̶o̶r̶e̶A̶p̶p̶l̶y̶(̶)̶;̶
}
If the updates to the message occur all in the same browser tick, only the last update will be rendered. The $timeout service does both a framework digest cycle and a browser rendering cycle.
For more information, see AngularJS $timeout Service API Reference

Trying to $Watch a Variable from Isolated Scope

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: '='
}

Controllers and directives, precedence

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']);

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

How to trigger a method when Angular is done adding scope updates to the DOM?

I am looking for a way to execute code when after I add changes to a $scope variable, in this case $scope.results. I need to do this in order to call some legacy code that requires the items to be in the DOM before it can execute.
My real code is triggering an AJAX call, and updating a scope variable in order to update the ui. So I currently my code is executing immediately after I push to the scope, but the legacy code is failing because the dom elements are not available yet.
I could add an ugly delay with setTimeout(), but that doesn't guarantee that the DOM is truly ready.
My question is, is there any ways I can bind to a "rendered" like event?
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
$scope.results = [];
$scope.loadResults = function(){
for(var i=0; i < resultsToLoad.length; i++){
$scope.results.push(resultsToLoad[i]);
}
}
function doneAddingToDom(){
// do something awesome like trigger a service call to log
}
}]);
angular.bootstrap(document, ['myApp']);
Link to simulated code: http://jsfiddle.net/acolchado/BhApF/5/
Thanks in Advance!
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. -- http://docs.angularjs.org/guide/concepts#runtime
Okay, so what's a "stack frame"? A Github comment reveals more:
if you enqueue from a controller then it will be before, but if you enqueue from directive then it will be after. -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158
Above, Misko is discussing when code that is queued for execution by $evalAsync is run, in relation to when the DOM is updated by Angular. I suggest reading the two Github comments before as well, to get the full context.
So if code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders. If you need to run something after the browser renders, or after a controller updates a model, use $timeout(..., 0);
See also https://stackoverflow.com/a/13619324/215945, which also has an example fiddle that uses $evalAsync().
I forked your fiddle.
http://jsfiddle.net/xGCmp/7/
I added a directive called emit-when. It takes two parameters. The event to be emitted and the condition that has to be met for the event to be emitted. This works because when the link function is executed in the directive, we know that the element has been rendered in the DOM. My solution is to emit an event when the last item in the ng-repeat has been rendered.
If we had an all Angular solution, I would not recommend doing this. It is kind of hacky. But, it might be an okey solution for handling the type of legacy code that you mention.
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [
{id: 1, name: "one"},
{id: 2, name: "two"},
{id: 3, name: "three"}
];
function doneAddingToDom() {
console.log(document.getElementById('renderedList').children.length);
}
$scope.results = [];
$scope.loadResults = function(){
$scope.results = resultsToLoad;
// If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
doneAddingToDom();
}
// If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
$scope.$on('allRendered', doneAddingToDom);
}]);
myApp.directive("emitWhen", function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var params = scope.$eval(attrs.emitWhen),
event = params.event,
condition = params.condition;
if(condition){
scope.$emit(event);
}
}
}
});
angular.bootstrap(document, ['myApp']);
Using timeout is not the correct way to do this. Use a directive to add/manipulate the DOM. If you do use timeout make sure to use $timeout which is hooked into Angular (for example returns a promise).
If you're like me, you'll notice that in many instances $timeout with a wait of 0 runs well before the DOM is truly stable and completely static. When I want the DOM to be stable, I want it to be stable gosh dang it. And so the solution I've come across is to set a watcher on the element (or as in the example below the entire document), for the "DOMSubtreeModified" event. Once I've waited 500 milliseconds and there have been no DOM changes, I broadcast an event like "domRendered".
IE:
//todo: Inject $rootScope and $window,
//Every call to $window.setTimeout will use this function
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
$rootScope.$broadcast('domRendered')
}, 500)
});
//IE stupidity
} else {
document.attachEvent("DOMSubtreeModified", function (e) {
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
$rootScope.$broadcast('domRendered')
}, 500)
});
}
This event can be hooked into, like all broadcasts, like so:
$rootScope.$on("domRendered", function(){
//do something
})
I had a custom directive and I needed the resulting height() property of the element inside my directive which meant I needed to read it after angular had run the entire $digest and the browser had flowed out the layout.
In the link function of my directive;
This didn't work reliably, not nearly late enough;
scope.$watch(function() {});
This was still not quite late enough;
scope.$evalAsync(function() {});
The following seemed to work (even with 0ms on Chrome) where curiously even ẁindow.setTimeout() with scope.$apply() did not;
$timeout(function() {}, 0);
Flicker was a concern though, so in the end I resorted to using requestAnimationFrame() with fallback to $timeout inside my directive (with appropriate vendor prefixes as appropriate). Simplified, this essentially looks like;
scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
$window.requestAnimationFrame(function() {
scope.$apply(function() {
scope.height = element.height(); // OK, this seems to be accurate for the layout
});
});
});
Then of course I can just use a;
scope.$watch("height", function() {
// Adjust view model based on new layout metrics
});
interval works for me,for example:
interval = $interval(function() {
if ($("#target").children().length === 0) {
return;
}
doSomething();
$interval.cancel(interval);
}, 0);

Resources