I'm trying to disable some input boxes while a loop is run and then enable it after it is finished. I would like to do this with angularjs. I set a scope variable to true before the loop starts and then to false when it completes.
When doing this, nothing is disabled during the loop. It's like it's not digesting the function until after the loop completes when it's set to false anyway.
I've tried using jquery as well and it reacted in the same way.
HTML
<input class="form-control" type="text" ng-model="lineToChange.QtyOrd" ng-disabled="disableLinePartial" />
JS
$scope.updateFreight = function (firstLineIndex) {
//jQuery method acts the same
//$("input[ng-disabled='disableLinePartial']").attr("disabled");
$scope.disableLinePartial = true;
angular.forEach($scope.data.SalesOrder.Lines, function (e, i) {
//great functions here
});
$scope.disableLinePartial = false;
};
Am I missing something with how JS parses? What's a better way to do this?
Angular update the HTML on a loop: digestion cycles they call it. The change you made on $scope is not reflected in HTML while the updateFreight() function is not yet returned.
You can:
1) first $scope.disableLinePartial = true
2)
$timeout(function() {
// do your loop here
$scope.disableLinePartial = false;
});
The $timeout will allow the $scope change to apply, and only do the loop after at least one digest cycle.
You may also want to read about $scope.$applyAsync() and $scope.$evalAsync()
see:
http://blog.bguiz.com/post/60397801810/digest-cycles-in-single-page-apps/
Related
I have a controller where I need to load content using ajax. While it's loading, I'd like a spinner to appear in the interim. The code looks something like the below:
<i class="fa fa-2x fa-spin fa-spinner" ng-show="isLoadingContent"></i>
And the corresponding js:
$scope.isLoadingContent = true;
$q.all(promises).then(function (values) {
$scope.isLoadingContent = false;
// more code - display returned data
However, the UI the spinner does not appear where/when I expect it to appear when I step through the code.
$scope.isLoadingContent = true;
debugger; // the spinner does not appear on the UI
$q.all(promises).then(function (values) {
debugger; // the spinner finally does appear in the UI at this point
$scope.isLoadingContent = false;
// more code - display returned data
I have tried stepping through the code but came up short as to what's going on --
and I am sure I am misunderstanding the sequence of events happening in the Event Loop and where the angular-cycle plays it's role in all of this.
Is someone able to provide an explanation as to why the spinner is set to appear within the promise's method rather than where I set $scope.isLoadingContent? Is it not actually getting set but rather getting queue'd up in the event-loop's message-queue?
------------ EDIT ------------
I believe I came across an explanation as to what's going on. Thanks in large part to, #jcford and #istrupin.
So a little tidbit missing in the original post, the event firing the promise calls and the spinner update was actually based around a $scope.$on("some-name", function(){...}) event - effectively a click-event that is triggered outside of my current controller's scope. I believe this means the $digest cycle doesn't work as it typically does because of where the event-origination is fired off. So any update in the $on function doesn't call $apply/$digest like it normally does, meaning I have to specifically make that $digest call.
Oddly enough, I realize now that within the $q.all(), it must call $apply since, when debugging, I saw the DOM changes that I had expected. Fwiw.
tl;dr - call $digest.
A combination of both answers will do the trick here. Use
$scope.$evalAsync()
This will combine scope apply with timeout in a nice way. The code within the $evalAsync will either be included in the current digest OR wait until the current digest is over and start a new digest with your changes.
i.e.
$q.all(promises).then(function (values) {
$scope.$evalAsync($scope.isLoadingContent = false);
});
Try adding $scope.$apply() after assigning $scope.isLoadingContent = true to force the digest. There might be something in the rest of your code keeping it from applying immediately.
As pointed out in a number of comments, this is absolutely a hack and is not the best way to go about solving the issue. That said, if this does work, you at least know that your binding is set up correctly, which will allow you to debug further. Since you mentioned it did, the next step would then be to see what's screwing up the normal digest cycle -- for example triggering outside of angular, as suggested by user JC Ford.
I usually use isContentLoaded (as oposite to isLoading). I leave it undefined at first so ng-show="!isContentLoaded" is guaranteed to show up at first template iteration.
When all is loaded i set isContentLoaded to true.
To debug your template you need to use $timeout
$timeout(function () { debugger; })
That will stop the code execution right after first digest cycle with all the $scope variable values reflected in the DOM.
Currently I have a text input attached to a model with a $scope.watch statement observing the model. This is all used to achieve a type of auto complete / typeahead feature.
<!-- HTML -->
<input type="text" ng-model="search.mySearchText">
// JS
var deregister = $scope.$watch('search.mySearchText', doSearch);
function doSearch() {
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
// do something with the data
});
}
This works fine. However, occasionally in my .then function I want to make changes to search.mySearchText. Obviously this would cause the watcher to be fired again, but I don't want this.
What I'm hoping to do is find a way to suppress the $watch from firing that next time. Maybe by somehow telling Angular that that particular watched model property is no longer dirty?
I tried removing the $watch by de/re- registering the watch at appropriate times, but that didn't work either.
function doSearch() {
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
deregister(); // shut off the watch
search.mySearchText = 'some new string'; // manipulate the model property that I don't want to cause a search
deregister = $scope.$watch('search.mySearchText', doSearch);
});
}
However, this didn't prevent the event firing like I expected, which is why I'm now looking for a way to suppress the event.
You could have a variable that determines whether doSearch exits early, like so:
var searchActive = true;
function doSearch() {
if (!searchActive) return;
mySearchService.executeSearch(search.mySearchText)
.then(function(res) {
searchActive = false;
// do manipulation of search.mySearchText
searchActive = true;
});
}
I have a search box on my web page that has check boxes in order for the user to filter their results. Only one check box can be checked at once.
When a check box is clicked my code runs off and applies the filter to the list and returns the correct results.
The problem I have is that when a check box is clicked multiple times in quick succession, it queues the requests and pulls them back one by one. This can take a while if a check box is checked and then un-checked multiple times.
Is there any way in Javascript to inform the function that it has been called again and it should stop everything other than this last request?
You want to wrap your onclick callback in a debouncing function like
http://underscorejs.org/#debounce
Say you have this
function search() {
// ...
}
$jquery(".myFilterCheckboxes").click(search);
You should be able to just change the above to:
// Only allow one click event / search every 500ms:
$jquery(".myFilterCheckboxes").click(_.debounce(search, 500));
There are tons of debouncing functions out there, and writing your own isn't a big deal really if you can't or don't want to include underscore.js.
My first thought was towards debouncing because you mentioned multiple clicks creating multiple events in a short period. Debouncing is used really often for things like type-ahead search or autocomplete to provide a little space between key presses for thinking time.
As others have mentioned it may make more sense to simply disable the checkboxes / click event while your search is running. In that case, try something like this:
function disableClick(elem) {
elem.unbind("click");
elem.attr("disabled", true);
}
function enableClick(elem, onclick) {
// Enable click events again
elem.live("click", search);
// Enable the checkboxes
elem.removeAttr("disabled");
}
function search() {
var boxes = $jquery(".myFilterCheckboxes");
disableClick(boxes);
$.get(...).always(function() {
enableClick(boxes, search);
});
}
$jquery(".myFilterCheckboxes").live("click", search);
Why disable the click event, add the disabled attribute to the checkboxes instead of just a global lock variable? Well, global locks can be somewhat error prone, but more than that, we already have a global object that matters in the DOM. If we just modify the DOM state we get the right behavior and signal to our users that they should chill out on the checkboxes until the search completes.
That said, it probably makes sense with any kind of locking / unbinding scenario to indicate to the user with a loading spinner or something that you're doing work.
You can use a lock pattern:
http://jsfiddle.net/RU6gL/
HTML
<input type="checkbox" onclick="fire()" >CB1
<br />
<input type="checkbox" onclick="fire()" >CB2
JS
function_lock = false
fire = function() {
// First, check the lock isn't already reserved. If it is, leave immediately.
if (function_lock) return;
// We got past the lock check, so immediately lock the function to
// stop others entering
function_lock = true;
console.log("This message will appear once, until the lock is released")
// Do your work. I use a simple Timeout. It could be an Ajax call.
window.setTimeout(function() {
// When the work finishes (eg Ajax onSuccess), release the lock.
function_lock = false;
}, 2000);
}
In this example, the function will only run once, no matter how many times the checkboxes are clicked, until the lock is released after 2 seconds by the timeout.
This pattern is quite nice, because it gives you control opver when you release the lock, rather than relying on a timed interval like 'debounce'. For example, it will work with Ajax. If your checkbox is triggering an Ajax call to do the filtering, you can:
On first click, set the lock
Call the Ajax endpoint. Subsequent clicks won't call the Ajax endpoint.
In the Ajax success function, reset the lock.
The checkboxes can now be clicked again.
HTML
<input type="checkbox" onclick="doAjax()" >CB2
JS
ajax_lock = false
doAjax: function() {
// Check the lock.
if (ajax_lock) return;
// Acquire the lock.
ajax_lock = true;
// Do the work.
$.get("url to ajax endpoint", function() {
// This is the success function: release the lock
ajax_lock = false;
});
}
The issue here is that the checkbox is repeatedly clicked on. You should instead disable your checkbox(which would also disable the click event on the element) when you are processing and then re-enable your checkbox when you're done processing.
The debouncing is a great idea, but you don't always know how long it will take for your processing function to finish.
Here's a simple example using jquery promise to re-enable the checkbox after some processing
http://jsfiddle.net/94coc8sd/
with the following code:
function processStuff() {
var dfd = $.Deferred();
// do some processing, when finished,
// resolve the deferred object
window.setTimeout(function(){
dfd.resolve();
}, 2000);
return dfd.promise();
}
function startProcessing() {
$('#processingCheckbox').attr('disabled', 'disabled');
var promise = processStuff();
promise.done(enableCheckbox);
}
function enableCheckbox() {
$('#processingCheckbox').removeAttr('disabled');
}
$('#processingCheckbox').on('click', startProcessing);
I have a function within my angular controller, I'd like this function to be run on document ready but I noticed that angular runs it as the dom is created.
function myController($scope)
{
$scope.init = function()
{
// I'd like to run this on document ready
}
$scope.init(); // doesn't work, loads my init before the page has completely loaded
}
Anyone know how I can go about this?
We can use the angular.element(document).ready() method to attach callbacks for when the document is ready. We can simply attach the callback in the controller like so:
angular.module('MyApp', [])
.controller('MyCtrl', [function() {
angular.element(document).ready(function () {
document.getElementById('msg').innerHTML = 'Hello';
});
}]);
http://jsfiddle.net/jgentes/stwyvq38/1/
See this post How to execute angular controller function on page load?
For fast lookup:
// register controller in html
<div data-ng-controller="myCtrl" data-ng-init="init()"></div>
// in controller
$scope.init = function () {
// check if there is query in url
// and fire search in case its value is not empty
};
This way, You don't have to wait till document is ready.
Angular has several timepoints to start executing functions. If you seek for something like jQuery's
$(document).ready();
You may find this analog in angular to be very useful:
$scope.$watch('$viewContentLoaded', function(){
//do something
});
This one is helpful when you want to manipulate the DOM elements. It will start executing only after all te elements are loaded.
UPD: What is said above works when you want to change css properties. However, sometimes it doesn't work when you want to measure the element properties, such as width, height, etc. In this case you may want to try this:
$scope.$watch('$viewContentLoaded',
function() {
$timeout(function() {
//do something
},0);
});
Angular initializes automatically upon DOMContentLoaded event or when
the angular.js script is evaluated if at that time document.readyState
is set to 'complete'. At this point Angular looks for the ng-app
directive which designates your application root.
https://docs.angularjs.org/guide/bootstrap
This means that the controller code will run after the DOM is ready.
Thus it's just $scope.init().
The answer
$scope.$watch('$viewContentLoaded',
function() {
$timeout(function() {
//do something
},0);
});
is the only one that works in most scenarios I tested. In a sample page with 4 components all of which build HTML from a template, the order of events was
$document ready
$onInit
$postLink
(and these 3 were repeated 3 more times in the same order for the other 3 components)
$viewContentLoaded (repeated 3 more times)
$timeout execution (repeated 3 more times)
So a $document.ready() is useless in most cases since the DOM being constructed in angular may be nowhere near ready.
But more interesting, even after $viewContentLoaded fired, the element of interest still could not be found.
Only after the $timeout executed was it found. Note that even though the $timeout was a value of 0, nearly 200 milliseconds elapsed before it executed, indicating that this thread was held off for quite a while, presumably while the DOM had angular templates added on a main thread. The total time from the first $document.ready() to the last $timeout execution was nearly 500 milliseconds.
In one extraordinary case where the value of a component was set and then the text() value was changed later in the $timeout, the $timeout value had to be increased until it worked (even though the element could be found during the $timeout). Something async within the 3rd party component caused a value to take precedence over the text until sufficient time passed. Another possibility is $scope.$evalAsync, but was not tried.
I am still looking for that one event that tells me the DOM has completely settled down and can be manipulated so that all cases work. So far an arbitrary timeout value is necessary, meaning at best this is a kludge that may not work on a slow browser. I have not tried JQuery options like liveQuery and publish/subscribe which may work, but certainly aren't pure angular.
I had a similar situation where I needed to execute a controller function after the view was loaded and also after a particular 3rd-party component within the view was loaded, initialized, and had placed a reference to itself on $scope. What ended up working for me was to setup a watch on this scope property and firing my function only after it was initialized.
// $scope.myGrid property will be created by the grid itself
// The grid will have a loadedRows property once initialized
$scope.$watch('myGrid', function(newValue, oldValue) {
if (newValue && newValue.loadedRows && !oldValue) {
initializeAllTheGridThings();
}
});
The watcher is called a couple of times with undefined values. Then when the grid is created and has the expected property, the initialization function may be safely called. The first time the watcher is called with a non-undefined newValue, oldValue will still be undefined.
Here's my attempt inside of an outer controller using coffeescript. It works rather well. Please note that settings.screen.xs|sm|md|lg are static values defined in a non-uglified file I include with the app. The values are per the Bootstrap 3 official breakpoints for the eponymous media query sizes:
xs = settings.screen.xs // 480
sm = settings.screen.sm // 768
md = settings.screen.md // 992
lg = settings.screen.lg // 1200
doMediaQuery = () ->
w = angular.element($window).width()
$scope.xs = w < sm
$scope.sm = w >= sm and w < md
$scope.md = w >= md and w < lg
$scope.lg = w >= lg
$scope.media = if $scope.xs
"xs"
else if $scope.sm
"sm"
else if $scope.md
"md"
else
"lg"
$document.ready () -> doMediaQuery()
angular.element($window).bind 'resize', () -> doMediaQuery()
If you're getting something like getElementById call returns null, it's probably because the function is running, but the ID hasn't had time to load in the DOM.
Try using Will's answer (towards the top) with a delay. Example:
angular.module('MyApp', [])
.controller('MyCtrl', [function() {
$scope.sleep = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
angular.element(document).ready(function () {
$scope.sleep(500).then(() => {
//code to run here after the delay
});
});
}]);
Why not try with what angular docs mention https://docs.angularjs.org/api/ng/function/angular.element.
angular.element(callback)
I've used this inside my $onInit(){...} function.
var self = this;
angular.element(function () {
var target = document.getElementsByClassName('unitSortingModule');
target[0].addEventListener("touchstart", self.touchHandler, false);
...
});
This worked for me.
$scope.$on('$ViewData', function(event) {
//Your code.
});
I use this code:
$scope.$watch('message', function()
{
// the code
});
Is there any way to fire change event of message manually, so the code will be executed?
Few options:
Use $scope.$apply() to run the digest loop which call all of the watch expressions
Put you inner watch code inside a function and call it manually
Change messages :)
Another option here is declaring the function separately and using $scope.watch with the pointer.
var watchFunction = function(){
// the code
}
$scope.$watch('message',watchFunction);
watchFunction();