AngularJS strange behavior with $watch - angularjs

I use broadcast to check in my controller if a directive change a variable.
Here is how my broadcast is launched (it works) :
$rootScope.$broadcast(EVENTS.RELOAD, scope.values);
And here I handle the broadcast in my controller :
$scope.$on(EVENTS.RELOAD, reloadCurves);
function reloadCurves(e, data){
console.log(exp.curves);
/* console.log('reload asked : ', data); */
exp.plate.experiments.forEach(function (element) {
if (data[element.well] === true){
exp.curves[element.well] = element;
}
});
console.log(exp.curves);
}
And after in another directive I check if my curves variable change with the $watch. This watch work too, when I change manually the variable the watch is triggered.
My problem is strange, in my reloadCurves I have two console.log and they always show the same object, but normally the first console.log have to output en empty array (or an array different of the second console.log) and the second one the result of the foreach.
So angular think the curves variable is not changed and don't throw the watch of my second directive.
What to do ?
EDIT :
I change my reloadCurves to this :
function reloadCurves(e, data){
console.log('reload asked : ', data);
console.log(exp.curves);
// Récuperer toute les experiments cochés (celle en true)
var tab = [];
exp.plate.experiments.forEach(function (element) {
if (data[element.well] === true){
tab[element.well] = element;
}
});
exp.curves = tab;
console.log(exp.curves);
}
So now the first and second console.log are different, but the $watch on the curves doesn't throw

After looking at your code, I think you're assuming that your forEach loop is iterating at least one time or more. As you're saying you are getting both console.log output as same so problem is simple. Your forEach loop is not iterating any time. just put console log inside the forEach loop, you won't get that log. Just check what do you have in exp.plate.experiments and you'll be able to fix it.
Edit
Looks like you're updating model outside angular world. Just wrap it in $scope.$apply(fn)
function reloadCurves(e, data){
console.log('reload asked : ', data);
console.log(exp.curves);
// Récuperer toute les experiments cochés (celle en true)
$scope.$apply(function() {
var tab = [];
exp.plate.experiments.forEach(function (element) {
if (data[element.well] === true){
tab[element.well] = element;
}
});
exp.curves = tab;
});
console.log(exp.curves);
}

Related

Prevent search changes from spamming history

I'm using links like #!/OrderList?id=123, which shows the list of all orders and more details of the order 123. With reloadOnSearch=false and watching $routeUpdate it works fine, except for one thing: All such links get put into the browsers history, while I'd prefer to have only one such link there. For example, instead of
#!/OrderList?id=123
#!/OrderList?id=124
#!/OrderList?id=125
#!/AnotherList?id=678
#!/AnotherList?id=679
just the last member of each group, i.e.,
#!/OrderList?id=125
#!/AnotherList?id=679
I'm aware of $location.replace(), but I can't see where to place it when the change happens via following a link. I tried to place it in $scope.$on("$routeUpdate", ...), but it did nothing, probably because it's too late when the route has already changed.
I'm not using neither router-ui nor the HTML5 mode (just plain angular-route).
I'm afraid, I wasn't clear about me insisting on using href rather than a custom handler. I want the links to work with middle mouse click and bookmarks and everything. A combination of ng-href and ng-click might do what I want, but I've found a simple solution working with plain links.
Looks like you may want to update the URL query parameter using an ng-click function instead of relying on a link, then call a function like the one below to update the parameter... With replace state, the history should only track the current value. I haven't tested this case so if you try it, let me know if it works.
function changeUrlParam (param, value) {
var currentURL = window.location.href;
var urlObject = currentURL.split('?');
var newQueryString = '?';
value = encodeURIComponent(value);
if(urlObject.length > 1){
var queries = urlObject[1].split('&');
var updatedExistingParam = false;
for (i = 0; i < queries.length; i++){
var queryItem = queries[i].split('=');
if(queryItem.length > 1){
if(queryItem[0] == param){
newQueryString += queryItem[0] + '=' + value + '&';
updatedExistingParam = true;
}else{
newQueryString += queryItem[0] + '=' + queryItem[1] + '&';
}
}
}
if(!updatedExistingParam){
newQueryString += param + '=' + value + '&';
}
}else{
newQueryString += param + '=' + value + '&';
}
window.history.replaceState('', '', urlObject[0] + newQueryString.slice(0, -1));
}
Maybe what you can do is, istead of a regular <a ng-href="#!/OrderList?id={{your.id}}">Link to your ID</a> you can create a link with an ng-clickdirective bound to a function which retrieves the data and passes it to the view.
Your HTML
`<span ng-click="loadListItem(your.id)">Link to your ID</span>`
<div id="your-item-data">
{{item.id}} - {{item.name}}
</div>
Your controller
myApp.controller('someController', function($scope) {
$scope.loadListItem(itemId) = function (
var myItem;
// Get item by 'itemId' and assign it to 'myItem' var
$scope.item = myItem;
);
});
This way instead of changing your URL, you can retrieve the item data in your controller and pass it to your view.
You don't give much detail of your controller/service implementation, but I hope this helps.
I think you were on the right track with the $scope.$on("$routeUpdate", ...) thing. Rather than $routeUpdate, however, try binding on $routeChangeStart:
$scope.$on("$routeChangeStart", function(event, nextRoute, currentRoute){
if (nextRoute.yourCriteria === currentRoute.yourCriteria){
//do your location replacement magic
}
});
If you wanted, you could even define a dontUpdateHistory boolean property in your route definitions, and then check for that property in your run block:
myApp.config(function($routeProvider){
$routeProvider.when('/whatever' {
templateUrl: 'whatever',
dontUpdateHistory: true //something like this
});
}).run(function($rootScope){
$rootScope.on('$routeChangeStart', function(event, nextRoute, currentRoute){
if (nextRoute.dontUpdateHistory){
//do your location replacement magic
}
});
I haven't tested any of this, but hopefully it gets the idea across.
I wasn't satisfied with any answer and after quite some debugging I found this solution:
.run(function($rootScope, $location) {
var replacing;
$rootScope.$on("$locationChangeStart", function(event, newUrl, oldUrl) {
if (oldUrl === newUrl) return; // Nobody cares.
// Make urls relative.
var baseLength = $location.absUrl().length - $location.url().length;
newUrl = newUrl.substring(baseLength);
oldUrl = oldUrl.substring(baseLength);
// Strip search, leave path only.
var newPath = newUrl.replace(/\?.*/, "");
var oldPath = oldUrl.replace(/\?.*/, "");
// Substantial change, history should be written normally.
if (oldPath !== newPath) return;
// We're replacing, just let it happen.
if (replacing) {
replacing = false;
return;
}
// We're NOT replacing, scratch it ...
event.preventDefault();
// ... and do the same transition with replace later.
$rootScope.$evalAsync(function() {
$location.url(newUrl).replace();
replacing = true;
});
});
})

angular `$broadcast` issue - how to fix this?

In my app, I am boradcasting a event for certain point, with checking some value. it works fine But the issue is, later on whenever i am trigger the broadcast, still my conditions works, that means my condition is working all times after the trigger happend.
here is my code :
scope.$watch('ctrl.data.deviceCity', function(newcity, oldcity) {
if (!newcity) {
scope.preloadMsg = false;
return;
}
scope.$on('cfpLoadingBar:started', function() {
$timeout(function() {
if (newcity && newcity.originalObject.stateId) { //the condition not works after the first time means alwasy appends the text
console.log('each time');
$('#loading-bar-spinner').find('.spinner-icon span')
.text('Finding install sites...');
}
}, 100);
});
});
you can deregister the watcher by storing its reference in a variable and then calling it:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
if( someCondition === true ){
myWatch(); //deregister the watcher by calling its reference
}
});
if you want to switch logic, just set some variable somewhere that dictates the control flow of the method:
var myWatch = scope.$watch('ctrl.data.deviceCity', function(){
scope.calledOnce = false;
if(!scope.calledOnce){
//... run this the first time
scope.calledOnce = true;
}
else {
// run this the second time (and every other time if you do not deregister this watch or change the variable)
// if you don't need this $watch anymore afterwards, just deregister it like so:
myWatch();
}
})

Failing To Check if Element is Disabled

I have a dropdown with elements that get disabled when conditions are met. In the test, I check them for being disabled, but all tests fail and always return the element state as enabled (Clearly incorrectly. I have ensured that this is not a timing issue - refreshed the page and gave ample wait time with browser sleep - the elements are clearly disabled on the screen). There is an anchor within a list item. Please see image:
I have tried checking both the list item and the anchor, like so:
var actionDropDownList = $$('[class="dropdown-menu"]').get(1);
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
actionsButton.click();
actionDropDownList.all(by.tagName('li')).count().then(function(count){
console.log('THE NUMBER OF ELEMENTS IN THE DROPDOWN IS...............................................................' + count);
}) //verify that I have the correct dropdown - yes
checkOutButtonState.isEnabled().then(function(isEnabled){
console.log('CHECKING checkOutButton BUTTON STATE: ' + isEnabled);
}) //log state - shows incorrectly
I have also tried checking the button itself for disabled state (the element below is what I tried checking instead of the list element):
var checkOutButton = $('[ng-click="item.statusId !== itemStatus.in || checkOut()"]');
This failed as well.
Not sure which one I should check and why both are failing. How do I correct this and get it to show that the disabled button is...well, disabled.
TEMPORARY ADD ON EDIT:
For simplicity's sake, I am trying:
var hasClass = function (element, cls) {
return element.getAttribute('class').then(function (classes) {
return classes.split(' ').indexOf(cls) !== -1;
});
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
expect(hasClass(checkOutButtonState, 'disabled')).toBe(true);
It still fails, however, despite the element clearly having the class. Alec - your solution throws "function is not defined," I am not sure if I need something else for it to see jasmine. Tried, but can't find anything wrong with it, not sure why I can't get it to work.
Edit:
If I run...since it only appears to have one class:
expect(checkOutButtonState.getAttribute('class')).toBe('disabled');
I get "expected 'ng-isolate-scope' to be 'disabled'"
In a quite similar situation I've ended up checking the presence of disabledclass:
expect(checkOutButtonState).toHaveClass("disabled");
Where toHaveClass() is a custom jasmine matcher:
beforeEach(function() {
jasmine.addMatchers({
toHaveClass: function() {
return {
compare: function(actual, expected) {
return {
pass: actual.getAttribute("class").then(function(classes) {
return classes.split(" ").indexOf(expected) !== -1;
})
};
}
};
},
});
});

Caret position in textarea with AngularJS

I am asking myself if I am doing it right. The problem I have is that I want to preserve caret position after AngularJS update textarea value.
HTML looks like this:
<div ng-controlle="editorController">
<button ng-click="addSomeTextAtTheEnd()">Add some text at the end</button>
<textarea id="editor" ng-model="editor"></textarea>
</div>
My controller looks like this:
app.controller("editorController", function($scope, $timeout, $window) {
$scope.editor = "";
$scope.addSomeTextAtTheEnd = function() {
$timeout(function() {
$scope.editor = $scope.editor + " Period!";
}, 5000);
}
$scope.$watch("editor", function editorListener() {
var editor = $window.document.getElementById("editor");
var start = editor.selectionStart;
var end = editor.selectionEnd;
$scope.$evalAsync(function() {
editor.selectionStart = start;
editor.selectionEnd = end;
});
});
});
Let say I start typing some text in textarea. Then I hit the button which will soon add " Period!" at the end of $scope.editor value. During the 5 seconds timeout I make focus on textarea again and write some more text. After 5 seconds my textarea value is updated.
I am watching for $scope.editor value. The editorListener will be executed on every $digest cycle. In this cycle also happens two-way data binding. I need to correct caret position right after data binding. Is $scope.$evalAsync(...) the right place where should I do this or not?
Here is a directive I use to manipulate the caret position; however, like I stated in a comment, there is an issue with IE.
Below is something that may help you plan this out. One thing I noticed in your question is that you mention a condition where the user may re-focus the input box to type additional text, which I would believe resets the timeout; would this condition be true?
Instead of using a button to add text, would you rather just add it without? Like running the addSomeTextAtTheEnd function whenever a user un-focuses from the input box?
If you have to use the button, and a user re-focuses on the input box and types more into, you should cancel your button timeout.
Like:
myVar = setTimeout(function(){ alert("Hello"); }, 3000);
// Then clear the timeout in your $watch if there is any change to the input.
clearTimeout(myVar);
If you do it this way, perhaps you will not even need to know the cursor position as the addSomeTextAtTheEnd function timeout will just reset on any input change before the 5 second timeout. If the 5 second timeout occurs then the addSomeTextAtTheEnd will run and "append text to the end" like it's supposed to do. Please give more information and I will update this as needed.
app.directive('filterElement', ['$filter', function($filter){
return {
restrict:'A', // Declares an Attributes Directive.
require: '?ngModel', // ? checks for parent scope if one exists.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return }
var conditional = attrs.rsFilterElement.conditional ? attrs.rsFilterElement.conditional : null;
scope.$watch(attrs.ngModel, function(value){
if( value == undefined || !attrs.rsFilterElement ){ return }
// Initialize the following values
// These values are used to set the cursor of the input.
var initialCursorPosition = elem[0].selectionStart
var initialValueLength = elem[0].value.length
var difference = false
// Sets ngModelView and ngViewValue
ngModel.$setViewValue($filter( attrs.rsFilterElement )( value, conditional ));
attrs.$$element[0].value = $filter( attrs.rsFilterElement )( value, conditional );
if(elem[0].value.length > initialValueLength){ difference = true }
if(elem[0].value.length < initialValueLength){ initialCursorPosition = initialCursorPosition - 1 }
elem[0].selectionStart = difference ? elem[0].selectionStart : initialCursorPosition
elem[0].selectionEnd = difference ? elem[0].selectionEnd : initialCursorPosition
});
} // end link
} // end return
}]);

need help to understand angular $watch

say I have a album list and user can add album
controller.albumList = function($scope, albumService) {
$scope.albums = albumService.query();
$scope.$watch('$scope.albums',function(){
$scope.albums.$save($scope.albums)
})
$scope.addalbum= function(){
$scope.albums.objects.push(album);
}
};
get a album list from server and show them
user can add album
watch the albums list ,when change happend save them to the server.
the problem is the $watch always fired, I did not even trigger the addalbum method, and every time I refresh the page a new album is created.
I follow the the code in todeMVC angular
here is the example code
var todos = $scope.todos = todoStorage.get();
$scope.newTodo = '';
$scope.editedTodo = null;
$scope.$watch('todos', function () {
$scope.remainingCount = filterFilter(todos, { completed: false }).length;
$scope.completedCount = todos.length - $scope.remainingCount;
$scope.allChecked = !$scope.remainingCount;
todoStorage.put(todos);
}, true);
please help me understand this
this is a solution:
$scope.$watch('albums', function(newValue, oldValue) {
if (angular.equals(newValue, oldValue)) {
return;
}
$scope.albums.$save($scope.albums);
}
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
More about a $watch listener: $watch at angularjs docs
Firstly, you do not have to specify the scope object when referencing to a property of the scope. So, replace:
$scope.$watch('$scope.albums', ...)
with the following:
$scope$watch('albums', ...)
Now about your issue. $watch is triggered each time the value of the object / property being watched changes. This includes even those cases when the values are yet to be assigned, such as undefined and null. Thus, if you wish that the save should happen only when a new album is added, you can have code similar to:
//Makes assumption that albums has a length property
$scope.$watch('albums.length', function () {
//Check for invalid cases
if ($scope.albums === undefined || $scope.albums === null) {
return;
}
//Genuine cases.
//Proceed to save the album.
});
With this, the $watch is still triggered in unwanted scenarios but with the check, you avoid saving when the album has not changed. Also, note that your $watch triggers only when the length of the albums object changes. So, if an album itself is updated (say an existing album name is changed), then this watch is not triggered. You can change the watch property based on your needs. The watch property mentioned here works only when a new album is added.

Resources