Remove $watch and bring the logic out of the $watch in AngularJS - angularjs

I have a code in AngularJS which looks like below :
$scope.startWatching = function () {
return $scope.$watch('form', function (n, o) {
var timeoutPromise;
$timeout.cancel(timeoutPromise); //does nothing, if timeout alrdy done
timeoutPromise = $timeout(function () {
if (n !== o) {
if ($scope.isLegacy) {
$scope.showCompleteBtn = $scope.showCompleteButton2();
} else {
$scope.showCompleteBtn = $scope.showCompleteButton();
}
}
}, 400);
}, true);
So whenever form changes, either $scope.showCompleteButton2() is called or $scope.showCompleteButton() is called.
The problem is that the $watch() gets called many number if times, so I need to bring these two methods out of the $watch().

Watchers like event listeners should only be added once when the DOM is built. And removed when the DOM is torn down.
If the code needs to enable or disable the actions performed by the watcher, provide a state in the model to do so:
var enableWatch = false;
$scope.startWatching = function () {
enableWatch = true;
};
var timeoutPromise;
$scope.$watch('form', function (n, o) {
if (!enableWatch) return;
//ELSE
timeoutPromise && $timeout.cancel(timeoutPromise);
timeoutPromise = $timeout(function () {
if (n !== o) {
if ($scope.isLegacy) {
$scope.showCompleteBtn = $scope.showCompleteButton2();
} else {
$scope.showCompleteBtn = $scope.showCompleteButton();
}
}
}, 400);
}, true);
The watcher ignores changes when the enableWatch variable is false. Set the variable to true to enable the specified actions.

Related

AngularJS: Passing Promise, yet Cannot read property 'finally' of undefined

In my angular app, I have 2 methods save() and saveTriggers(). saveTriggers() updates all records by calling a web service (C#). I want to make sure that a block of code is executed after all records are updated in saveTriggers() and control is returned to save(). I believe I need to pass something from the saveTriggers() to make finally block execute. I tried various things, nothing works. Using .then() also gives the same error. I am not that good at JS. Can you please guide me.
vm.updatedTriggers = []; // IDs are pushed in
vm.saveTriggers = function () {
if (vm.updatedTriggers.length === 0) {
vm.close();
} else {
vm.saving = true;
vm.save()
.finally(function () { // ERROR - Cannot read property 'finally' of undefined
console.log("Saved all. Closing..."); // Never REACHES here
vm.saving = false;
vm.updated = true;
$uibModalInstance.close(true);
});
}
};
vm.save = function () {
//vm.saving = true;
for (var i = 0; i < vm.updatedTriggers.length; i++) {
var trigger = vm.triggers.find(t => t.id === vm.updatedTriggers[i]);
var input = {
id: trigger.id,
target: trigger.target,
targetInfo: vm.targetData,
event: trigger.event,
eventQuantity: trigger.eventQuantity,
eventQuantityExtra: trigger.eventQuantityExtra
};
rpmService.editDeviceTrigger(input);
/*.finally(function () {
console.log("Updated event"); // Reaches here
vm.updated = true;
return Promise.resolve(2);
});*/ // Commenting this also doesn't help
}
return Promise.resolve(2);
};
rpmService.editDeviceTrigger(input)
public async Task EditDeviceTrigger(EditDeviceTriggerInput input) {
// calls other methods with await
// Doesn't return anything
}
EDIT: Updated Code: I got rid of the error, but the output is not is expected series.
vm.saveTriggers = function () {
vm.saving = true;
vm.save().then
(function success() {
console.log("Returned Result ");
console.log("Saved all. Closing..."); // These lines are executed before the event is upated
vm.saving = false;
$uibModalInstance.close(true);
});
};
vm.save = function () {
var deferred = $q.defer();
for (var i = 0; i < vm.updatedTriggers.length; i++) {
var trigger = vm.triggers.find(t => t.id === vm.updatedTriggers[i]);
var input = {
id: trigger.id,
....
};
rpmService.editDeviceTrigger(input)
.finally(function () {
console.log("Updated event"); // Successfully updates all events
vm.updated = true;
});
}
deferred.resolve();
return deferred.promise;
};
OUTPUT:
Returned Result
Saved all. Closing...
Updated event
EXPECTED OUTPUT:
Updated event
Returned Result
Saved all. Closing...
Thanks.
Usually you dont need $q.defer-related things, but u can do same using it if u want.
Here I guess you just need to collect all your save promises and return new resulting one using $q.all:
vm.save = function () {
const myAwesomePromises = []
for (var i = 0; i < vm.updatedTriggers.length; i++) {
...
const savePromise = rpmService.editDeviceTrigger(input);
savePromise.finally(() => console.log('edit device finally'));// <-- not sure u need this
myAwesomePromises.push(savePromise);
}
return $q.all(myAwesomePromises).finally(() => console.log('All edit device finally'));
};

Evaluate a value after a watcher is run Angularjs

I have watcher code which gets triggered whenever an item is changed from a dropdown. I am trying to get the value of $scope.from_date after the event occurs but the value being returned from the console log is the value before the event is triggered.
$scope.$watch('strSelectedDateRange', function(strNewRange, strOldRange) {
if (strNewRange && strNewRange !== strOldRange) {
if ($scope.strSelectedDateRange === 'custom') {
$scope.bShowDateRange = true;
checkHourlyCustomDateRange();
$scope.setCustomDateRange();
} else {
$scope.bShowDateRange = false; // for safety
$scope.strCurDateRange = $scope.strSelectedDateRange;
$this.getReportData($scope.deal);
}
}
console.log($scope.from_date)
});
This the the link to example code:
JSFiddle link
angular.module('watchApp', []).controller('watchCtrl', function($scope) {
$scope.watchVariable = "hghj";
$scope.$watch('watchVariable', function(newVal, oldVal) {
// do something here
$scope.oldVal = oldVal;
$scope.newVal = newVal;
}, true);
});

Trying to bind a variable to a row in a Factory array (angular)

I have a factory that is storing an array where objects are pushed in as buttons are pressed. I want to bind a variable on a page to the first object in this array. Here's my factory:
angular.module('myApp')
.factory('errorHandler', function () {
var errorArray = [];
function compareObjs(a,b) {
if(a.type === "Alert" && b.type === "Info") {
return -1;
}
else if (a.type === "Info" && b.type === "Alert") {
return 1;
}
else {
if(a.timestamp > b.timestamp) {
return -1;
}
else if (a.timestamp < b.timestamp) {
return 1;
}
else {
return 0;
}
}
}
errorArray.addError = (function (type, message) {
var timestamp = Date.now();
errorArray.push({type: type, message: message, timestamp: timestamp});
errorArray.sort(compareObjs);
});
errorArray.removeError = (function (index) {
if(errorArray.length >= index) {
errorArray.splice(index, 1);
}
});
return errorArray;
})
And Controller:
.controller('ErrorModalCtrl', ['errorHandler', '$scope', function (errorHandler, $scope) {
$scope.errorModal = {
title: 'Notification Centre',
errorList: []
};
$scope.addError = function(type, message) {
errorHandler.addError(type, message);
console.log(errorHandler.length+"just added");
};
$scope.errorModal.errorList = errorHandler;
$scope.mostRecentError = errorHandler[0];
}]);
So when the page loads the array will be empty, but as I press these buttons $scope.addError is triggered and objects are pushed to the array. I have checked this is working correctly. However, on my HTML (within the scope of the controller) I have
{{mostRecentError.message}}
and this never populates. Why is this the case? Have I misunderstood the dependencies somewhere?
Thanks
The line
$scope.mostRecentError = errorHandler[0];
is executed when your controller is instantiated. At that moment, the error hasn't pressed any button yet. So errorHandler[0] is undefined, and $scope.mostRecentError is thus initialized to undefined.
When the user pressed the button errorHandler will have its first element, but that line of code won't be magically reexecuted, so $scope.mostRecentError wil stay undefined.
Instead of initializing a variable, you should define a function in the controller, that returns the most recent error. That, or $scope.mostRecentError must be initialized inside the addError() function of the controller, to be updated every time an error is added.

Angular preloader start after everything else has loaded

Hopefully this is an easy answer.
I have this directive that controls the background images on the site. It rotates between images, but before it starts setting the background images, it actually preloads them. This is my code for the directive:
// ---
// CONTROLLERS.
// ---
.controller('BackgroundSwitcherController', ['$rootScope', 'Options', 'BackgroundSwitcherService', function ($rootScope, options, service) {
var self = this;
// Declare our variables
self.model = {
staticImage: '',
loading: false,
rotate: false
};
// On state change
$rootScope.$on('$stateChangeStart', function (event, toState) {
// Get our data
var data = toState.data;
// Set our rotate flag
self.model.rotate = options.current.rotateBackground;
// If we are set to rotate
if (self.model.rotate) {
// Make a call to our service (this is called every state change, but only preloaded once)
service.get().then(function (response) {
// Return the images in the response
self.model.images = response;
});
} else {
// Otherwise, reset the images
self.model.images = null;
}
// If we have data
if (data) {
// Reset our variables
self.model.staticImage = data.backgroundImage;
//self.model.loading = true;
}
});
}])
// ---
// SERVICES.
// ---
.service('BackgroundSwitcherService', ['$q', 'PreloaderService', function ($q, preloader) {
this.loading = true;
this.successful = false;
this.percentLoaded = 0;
// Get our images
this.get = function () {
// Our images
var images = [
'assets/backgrounds/Avebury Steps.jpg',
'assets/backgrounds/Avebury Stripe.jpg',
'assets/backgrounds/Avebury.jpg',
'assets/backgrounds/Forest Hills.jpg',
'assets/backgrounds/Inteface.jpg',
'assets/backgrounds/New Oaklands.jpg',
'assets/backgrounds/Primo Delight.jpg',
'assets/backgrounds/Secure 3d.jpg',
'assets/backgrounds/Secure.jpg',
'assets/backgrounds/Sensation Heathers.jpg',
'assets/backgrounds/Sensation.jpg',
'assets/backgrounds/Woman.jpg'
];
// Create our promise
var deferred = $q.defer();
// Preload the images
preloader.preloadImages(images).then(
// Function to handle the changing of the flags when all images have loaded
function handleResolve(imageLocations) {
this.loading = false;
this.successful = true;
deferred.resolve(images);
},
// Function to handle any errors
function handleReject(imageLocation) {
this.loading = false;
this.successful = false;
deferred.reject();
},
// Function that notifies our percentage loaded flag
function handleNotify(event) {
this.percentLoaded = event.percent;
}
);
// Return our promise
return deferred.promise;
};
}])
// ---
// DIRECTIVES.
// ---
.directive('backgroundSwitcher', ['$interval', function ($interval) {
// Variables
var target = null,
timer = null,
images = null,
currentIndex = 0;
// Function to get a random value between two numbers
function getRandomInt(min, max) {
// Return our random number
return Math.floor(Math.random() * (max - min)) + min;
};
var getImageIndex = function (length) {
// Get our random index
var index = getRandomInt(0, images.length);
// If our index matches the current index
if (index == currentIndex) {
// Run again until we get a different index
return getImageIndex(length);
}
// Set our current index our the new index
currentIndex = index;
// Return our index
return index;
}
// Apply the image
var applyImage = function () {
// Get a random image
var image = images[getImageIndex(images.length)];
// Apply our image to our target
applyStaticImage(image);
};
// Apply a static image
var applyStaticImage = function (image) {
// Apply our image to our target
target.css('background-image', 'url(' + escape(image) + ')');
};
// Remove any images
var removeImage = function () {
// Remove our background image
target.css('background-image', 'none');
};
// Start timer function
var startTimer = function () {
// If our timer is not running
if (!timer || timer.$$state.status === 2) {
// Apply our first image
applyImage();
// Start our timer
timer = $interval(changeImage, 10000);
}
};
// Function to change the background image
var changeImage = function () {
// Apply our image
applyImage();
};
// Stop the timer
var stopTimer = function () {
// If we have started our timer
if (timer) {
// Stop it
$interval.cancel(timer);
}
};
return {
restrict: 'A',
controller: 'BackgroundSwitcherController',
link: function (scope, element, attr, controller) {
// Assign our element to our global variable
target = element;
// Watch our image
scope.$watch(function () {
// Return our image
return controller.model.staticImage;
}, function (staticImage) {
// If we have an image
if (staticImage) {
// Stop our timer
stopTimer();
// Apply our static image
applyStaticImage(staticImage);
} else {
// If we are not rotating
if (!controller.model.rotate) {
// Remove any images
removeImage();
}
}
})
// Watch our rotate
scope.$watch(function () {
// Return our rotate flag
return controller.model.images;
}, function (array) {
// Set our variable
images = array;
// If we have some images
if (images) {
// If we don't have a static image
if (!controller.model.staticImage) {
// Start rotating our images
startTimer();
}
} else {
// Remove any images
removeImage();
// Otherwise, stop our timer
stopTimer();
}
});
// Destroy function to cancel all timers
element.on('$destroy', function () {
// Stop our timer
stopTimer();
});
}
}
}]);
and for the actual preloader I have this:
// ---
// SERVICES.
// ---
.factory('PreloaderService', function ($q, $rootScope) {
function Preloader(imageLocations) {
this.imageLocations = imageLocations;
this.imageCount = imageLocations.length;
this.loadCount = 0;
this.errorCount = 0;
this.states = {
PENDING: 1,
LOADING: 2,
RESOLVED: 3,
REJECTED: 4
};
this.state = this.states.PENDING;
// When loading the images, a promise will be returned to indicate
// when the loading has completed (and / or progressed).
this.deferred = $q.defer();
this.promise = this.deferred.promise;
}
// I reload the given images [Array] and return a promise. The promise
// will be resolved with the array of image locations.
Preloader.preloadImages = function (imageLocations) {
var preloader = new Preloader(imageLocations);
return (preloader.load());
};
Preloader.prototype = {
// Best practice for 'instnceof' operator.
constructor: Preloader,
// ---
// PUBLIC METHODS.
// ---
// I determine if the preloader has started loading images yet.
isInitiated: function isInitiated() {
return (this.state !== this.states.PENDING);
},
// I determine if the preloader has failed to load all of the images.
isRejected: function isRejected() {
return (this.state === this.states.REJECTED);
},
// I determine if the preloader has successfully loaded all of the images.
isResolved: function isResolved() {
return (this.state === this.states.RESOLVED);
},
// I initiate the preload of the images. Returns a promise.
load: function load() {
// If the images are already loading, return the existing promise.
if (this.isInitiated()) {
return (this.promise);
}
this.state = this.states.LOADING;
for (var i = 0 ; i < this.imageCount ; i++) {
this.loadImageLocation(this.imageLocations[i]);
}
// Return the deferred promise for the load event.
return (this.promise);
},
// ---
// PRIVATE METHODS.
// ---
// I handle the load-failure of the given image location.
handleImageError: function handleImageError(imageLocation) {
this.errorCount++;
// If the preload action has already failed, ignore further action.
if (this.isRejected()) {
return;
}
this.state = this.states.REJECTED;
this.deferred.reject(imageLocation);
},
// I handle the load-success of the given image location.
handleImageLoad: function handleImageLoad(imageLocation) {
this.loadCount++;
// If the preload action has already failed, ignore further action.
if (this.isRejected()) {
return;
}
// Notify the progress of the overall deferred. This is different
// than Resolving the deferred - you can call notify many times
// before the ultimate resolution (or rejection) of the deferred.
this.deferred.notify({
percent: Math.ceil(this.loadCount / this.imageCount * 100),
imageLocation: imageLocation
});
// If all of the images have loaded, we can resolve the deferred
// value that we returned to the calling context.
if (this.loadCount === this.imageCount) {
this.state = this.states.RESOLVED;
this.deferred.resolve(this.imageLocations);
}
},
// I load the given image location and then wire the load / error
// events back into the preloader instance.
// --
// NOTE: The load/error events trigger a $digest.
loadImageLocation: function loadImageLocation(imageLocation) {
var preloader = this;
// When it comes to creating the image object, it is critical that
// we bind the event handlers BEFORE we actually set the image
// source. Failure to do so will prevent the events from proper
// triggering in some browsers.
var image = angular.element(new Image());
image.bind('load', function (event) {
// Since the load event is asynchronous, we have to
// tell AngularJS that something changed.
$rootScope.$apply(
function () {
preloader.handleImageLoad(event.target.src);
// Clean up object reference to help with the
// garbage collection in the closure.
preloader = image = event = null;
}
);
}).bind('error', function (event) {
// Since the load event is asynchronous, we have to
// tell AngularJS that something changed.
$rootScope.$apply(
function () {
preloader.handleImageError(event.target.src);
// Clean up object reference to help with the
// garbage collection in the closure.
preloader = image = event = null;
}
);
}).prop('src', imageLocation);
}
};
// Return the factory instance.
return (Preloader);
});
Now, when I use this directive all my other images / font awesome icons pause before they load. It causes an issue because the font awesome icons actually briefly show a rectangular box before fully loading and the profile image is blank until loaded.
If I remove my directive from my HTML everything loads fine.
Now I realise that this is happening because it has to load an array of images for the background switcher, so what I would like to do is to not start the preloading until everything else has loaded first.
Can someone tell me if there is a simple way to do this?

binding to service variable, doesnt refresh (service changes the var frequently)

In my Service i have the vars i want to display and the getters for it:
var docsLoaded = 0;
var docsToLoad = null;
pouchService.getDocsLoaded = function () {
return docsLoaded;
};
pouchService.getDocsToLoad = function () {
return docsToLoad;
};
While the service is syncing, i want to count the synced docs
pouchService.syncNow = function () {
var foundLastSeq = false;
docsLoaded = 0;
docsToLoad = null;
remoteDB.info().then(function (remoteInfo) {
function findOutDiff(localPosition) {
docsToLoad = (remoteInfo.update_seq - localPosition) + 1;
console.log("docs to load: " + docsToLoad);
}
// start Sync progress
sync = localDB.sync(remoteDB, {live: false})
.on('change', function (info) {
console.log('AI change: ');
console.log(info);
if (info.direction === 'pull') {
if (foundLastSeq === false) {
foundLastSeq = true;
findOutDiff(info.change.last_seq);
}
}
console.log(docsLoaded + " from " + docsToLoad);
docsLoaded++;
})
In my HTML i want to display the progress like this:
{{pouchService.getDocsLoaded()}} from {{pouchService.getDocsToLoad()}}
Now i get sometimes a value from getDocsLoaded, but mostly its zero. When I cancel the Syncprogress i get the value where it's stopped.
So i get the value before it really starts and when it's over, but i want it during the sync progress. (on the console my my progressinfos are working as expected)
Any ideas?
The problem is in applying scope. Jim wrote a nice article about this problem:
jimhoskins.com/2012/12/17/angularjs-and-apply.html
Solved it:
$rootScope.$apply(function () {
docsLoaded++;
});

Resources