Delayed search using Angular - javascript

I am working with a search control for one of my project using Angular. I was trying for delayed response like the search box should wait until 2 seconds before calling the relevant function.
When I search the web, most of the suggestion was to use a directive. I cannot use directive since clicking the search result in turn shows another set of results. I got another solution after pondering over in the web.
My code:
<div ng-controller="ReportCtrl" ng-app="pReport">
<input type="text" ng-model="searchString" placeholder="Search"/>
<div ng-show="display == 'search'">
<ul>
<li ng-repeat="i in batches">
<a ng-click='selectBatch(i.batchnumber)'>{{i.batchnumber}}</a>
</li>
</ul>
</div>
<div ng-show="display == 'repdetail'">
<ul>
<li ng-repeat="i in reports">
<a href='{{i.filename}}'>{{i.screenname}}</a>
</li>
</ul>
</div>
</div>
Angular Expressions
var app = angular.module('pReport', []);
app.controller('ReportCtrl', function ($scope, $http, $timeout) {
var timer = false;
$scope.batches = [];
$scope.reports = [];
$scope.$watch('searchString', function () {
if (timer) {
$timeout.cancel(timer);
}
timer = $timeout(function () {
$http({ }). //POSTING DETAILS
success(function (data, status, headers, config) {}); //SEARCH RESULTS
}, 1000)
});
$scope.selectBatch = function (batchNumber) { } // SEARCH FURTHER
});
Is there anything wrong in this approach of delaying?
Should I use a directive?
Is there any other most efficient approach?
Thanks in advance for your help!

What you're looking for is a debounce. Lodash provide this capability out of the box. The good news is Angular is getting this functionality in an upcoming version built-in.
It prevent execution of a function more than once per given time period. This is ideal for situations like a typeahead search box where you'd like to wait before firing off another request to the server and event handling in general.
In this case, I'd remove the watch and the move the $http call to its own function wrapped in a debounce:
$scope.search = function() {
_.debounce($http(...), 1000, { leading: false, trailing: true })
}
The leading and trailing just configure when the debounce will occur, in our case, we want it to happen after the initial 1000 wait.
And then change the input box to use ng-change so it triggers the function when a user types in something, but only once every 1000ms:
<input type="text" ng-model="searchString" placeholder="Search" ng-change="search()"/>

Related

Why ng-class is not updating using setTimeout? Angular

I have a function 'highlightBookmark' that should change the background color of a list item after 2 seconds. But it doesn't work!!!
It changes 'li' background only if the function is called by click event. It doesn't change it automatically after time is out, even if it actually calls the function.
Here is my code:
Controller.js
//markers
$scope.markers = [
{
time: 9.5,
text: "Bookmark 1",
class: false
},
{
time: 106,
text: "Bookmark 2",
class: false
}
]
$scope.currentBookmark = -1;
function highlightBookmark(index) {
$scope.markers[index].class = true;
}
var interval = setInterval(checkTime, 100);
function checkTime(){
if(Math.floor(player.currentTime()) == 2){
highlightBookmark(1)
clearInterval(interval);
}
}
$scope.jumpTo = function (index) {
highlightBookmark(index);
}
The highlight function, takes in an integer, looks for object at that position and updates set 'class' parameter to true. Example, if I pass number 1 to the function, it will look for object at index 2 and set the 'class property' to be true.
Then, after 2 seconds I want to call the highlightBookmark function. IT IS CALLED but it doesn't update the class, thus the background doesn't update.
I call the same with click event and it works.
HTML file
<ul id = "bookmarkList">
<li ng-repeat="bookmark in markers" ng-class="{'active': bookmark.class}" ng-click="jumpTo($index)">{{bookmark.text}}</li>
</ul>
This li has the ng-class property that I want to change after 2 seconds.
Here is a link to a similar code I did on codepen. It changes button color on click, but doesn't change on setTimeout even if method is called
https://codepen.io/Octtavius/pen/wgzORv
Could somebody help with this simple issue?
The vanilla setInterval function doesn't update scope variables. Try with the $interval API by Angular:
var interval = $interval(checkTime, 100);
function checkTime(){
if(Math.floor(player.currentTime()) == 2){
highlightBookmark(1)
interval.cancel();
}
}
Notice clearInterval(interval) changes to interval.cancel()
Also don't forget to inject it as dependency.
Fair point by charlietfl: Also cancel the interval if the scope gets destroyed.
Place this inside your controller:
$scope.$on("$destroy", function( event ) {
interval.cancel( timer );
});
More info: https://docs.angularjs.org/api/ng/service/$interval
setInteral and setTimeout run outside of the angular digest cycle, so they will not be properly picked up by Angular.
Consider using the $timeout object in your controller -- this gives you the timeout functionality but allows angular to keep an eye on it.
You should consider use angular $timeout instead of setInverval or setTimeout.
Because:
This functions don't $digest the $scope variables;
$timeout in this case requires less memory to do the exactly same thing that $interval would do.
So, this part of your controller will look like this:
//markers
$scope.markers = [
{
time: 9.5,
text: "Bookmark 1",
class: false
},
{
time: 106,
text: "Bookmark 2",
class: false
}
]
$scope.currentBookmark = -1;
function highlightBookmark(index) {
$scope.markers[index].class = true;
}
$timeout(checkTime, 2000);
function checkTime(){
highlightBookmark(1);
}
$scope.jumpTo = function (index) {
highlightBookmark(index);
}
Remember to inject the $timeout as a dependency in your controller.
P.S.
This code will mark a default after 2 seconds, you don't give enough details so I can know what the player is doing. So if you want to improve this, give more details and we can make it happen.
You should consider use "controller as". Here is a link to John Papa's article about it. https://johnpapa.net/angularjss-controller-as-and-the-vm-variable/
And if you want to toggle the background on click event you should use this code. Cause the one you did is only adding the background, but not removing from the others li. To do this we need to modify the html and the controller a little bit:
<div ng-app="classApp" ng-controller="classCtrl">
<ul id = "bookmarkList">
<li ng-repeat="bookmark in markers" ng-class="{'active': selectedMarker === bookmark}" ng-click="jumpTo(bookmark)">{{bookmark.text}}</li>
</ul>
</div>
$scope.selectedMarker = null;
function highlightBookmark(marker) {
$scope.selectedMarker = marker;
}
$timeout(checkTime, 2000);
function checkTime(){
highlightBookmark($scope.markers[0])
}
$scope.jumpTo = function (marker) {
highlightBookmark(marker);
}
Cya.
var classApp = angular.module('classApp', []);
classApp.controller('classCtrl', function ($scope, $timeout) {
$scope.isActive = false;
$scope.activeButton = function () {
$scope.isActive = !$scope.isActive;
}
function checkTime() {
$scope.isActive = !$scope.isActive;
}
$timeout(checkTime, 2000)
});

AngularJS needs to execute a function two times to show the results

I have a navigation bar which has a drop-down menu item Log Out which calls the ngPostLogOut() function.
In app.js
.when("/logout", {
controller: 'AuthController',
templateUrl: "client/html/auth/logout.html"
});
AuthController
$scope.ngPOSTLogOut = function() {
if ($rootScope.user) {
$rootScope.user = null;
}
if ($window.sessionStorage) {
$window.sessionStorage.clear();
alert('Entered..');
}
alert('Before HTTP');
$http.post('server/auth/logout.php')
.then(function(result) {
$scope.logout = result.data;
});
alert('After HTTP');
/*
$timeout(function() {
$location.path('/');
}, 10000);
*/
};
logout.html
<div ng-controller="AuthController as auth">
<p ng-show='user == null' class="text-center">{{logout}}</p>
<br>
<button ng-click='ngPOSTLogOut()' class="btn btn-default btn-block">Angular To PHP</button>
Now, if a person clicks the Log Out item from the drop-down in the navbar then the function is called. I know this because I have set up alerts and they do pop up. But, the 'echo' from the login.php doesn't get featured. But, the odd thing is is that if I press the Angular to PHP button which also calls the ngPostLogOut() function, the function completes perfectly and as intended.
My guess
My guess is that ngRoute forces Angular to prioritize the HTML template switch making it so that the ngPOSTLogOut() function's parametres get ignored or dismissed.
The entire project on GitHub
https://github.com/AquaSolid/RAMA_Angular_PHP
Change this:
<li ng-click='ngPOSTLogOut()' ng-show='user != null'>Log Out</li>
to this:
<li ng-show="user != null"><a ng-click="ngPOSTLogOut()">Log Out</a></li>
And consider using ng-if rather than ng-show.

AngularJS : Removing rootScope listener doesn't work

I've got the following code in a controller :
var deregisterSetPermissions = $rootScope.$on("setPermissions", function () {
[... some code used when the user click on a "create account" button...]
});
$scope.$on('$destroy', function() {
deregisterSetPermissions;
[... and other listeners are unregistered ...]
}
When I leave the page, all the listeners are unregistered : I can see through the chrome developer console that $rootScope.$$listeners.setPermissions[] contains a single value, which is null.
When I come back to the page and after it loaded, $rootScope.$$listeners.setPermissions[0] contains null and $rootScope.$$listeners.setPermissions[1] contains the function the listener will have to call.
$rootScope.$$listenerCount.setPermissions is 1.
But when I click on the "create account" button, the function used by the "setPermissions" listener is called twice !
How can it be ?
Here's what I tried :
1- Checked that there are no other listeners created with the name "setPermissions"
2- Delete the content of the listener when the scope is destroyed :
$scope.$on('$destroy', function() {
$rootScope.$$listeners.setPermissions=[];
$rootScope.$$listenerCount.setPermissions = 0;
});
But still after that, if I leave the page and come back 3 times, a click on the "create account" button will cause the "setPermissions"-listener-function to be called 3 times...
It would be nice to understand why the listener's function is called more than once; and if you have a solution or an idea to help me going on, I am looking forward to reading it !
deregisterSetPermissions is a function. To unsubscribe the listener, you have to call it.
$scope.$on('$destroy', function() {
deregisterSetPermissions();
}
Generally, any time you find yourself using a function object that begins with the double dollar signs ($$) in Angular, it means you're doing something wrong - that's Angular's naming convention for something that isn't part of their public API!
I found some useful information from this question:
how to unsubscribe to a broadcast event
Which I used to construct an example:
var app = angular.module('app', [])
.controller('AppController', function($rootScope) {
var appCtrl = this;
$rootScope.manageNumbers = false;
appCtrl.addNumber = function() {
$rootScope.$broadcast('addNumber');
};
})
.controller('NumbersController', function($rootScope, $scope) {
var numCtrl = this;
numCtrl.numbers = [];
numCtrl.number = 0;
// Register and set handle
console.log('Register listener');
var registerListener = $rootScope.$on('addNumber', addNumber);
logListenerData();
// Unregister
$scope.$on('$destroy', function() {
console.log('Unregister listener');
registerListener();
// $rootScope.$$listeners.addNumber.length = 0;
logListenerData();
});
function addNumber() {
numCtrl.numbers.push(numCtrl.number);
numCtrl.number++;
}
function logListenerData() {
console.log('$rootScope.$$listeners');
console.log($rootScope.$$listeners);
console.log('$rootScope.$$listenerCount');
console.log($rootScope.$$listenerCount);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="app">
<div ng-controller="AppController as appCtrl">
<button ng-click="$root.manageNumbers=true;" ng-if="!$root.manageNumbers">
Start Number Management
</button>
<button ng-click="$root.manageNumbers=false;" ng-if="$root.manageNumbers">
Stop Number Management
</button>
<button ng-click="appCtrl.addNumber()">
Add Number
</button>
</div>
<div ng-controller="NumbersController as numCtrl" ng-if="$root.manageNumbers">
<p>
<b>Numbers:</b>
<span ng-repeat="number in numCtrl.numbers">
{{ $last ? number : number + ', ' }}
</span>
</p>
</div>
</body>
Notice a few things. Even when you unregister an event, unless that event was triggered while it was registered, it will have a "ghost" null value in the $$listeners but will not show up in the $$listenerCounts. With the commented out line that sets the length to 0 I was able to remove these ghosts, however; it causes no actual difference as far as I can tell.
I finally found the root of the problem.
There was an other rootScope-listener in a service which wasn't destroyed, which is emitting the "setPermissions" message. Thus, I could try all I wanted, but I was looking at the wrong place..

angular ui-router stuck in between states

I am using UI-Router in a project. I am using the state resolve functionality provided by the router. When I want to display a loader in between two states and I change states very often the spinner pops up but don't disappear anymore.
The same effect can be found here.
http://rp.js.org/angular-ui-view-spinner/example/index.html
If one clicks fast between the two states the loader won't hide.
I am waiting for the stateChangeStart broadcast and display the loader and on statChangeSuccess, stateChangeError and viewContentLoaded I want't to hide the loader. The stateChangeStart gets fired but stateChangeSuccess doesn't. Any idea why this behaviour appears?
I am using angular-ui-router in version 0.2.18
Here the Code for show and hide:
$rootScope.$on('$stateChangeStart', showLoading);
$rootScope.$on('$stateChangeSuccess', hideLoading);
$rootScope.$on('$stateChangeError', hideLoading);
$rootScope.$on('$viewContentLoaded', hideLoading);
function showLoading() {
$timeout(function () {
angular.element('.loading-indicator').show();
}, 50);
}
function hideLoading() {
$timeout(function () {
angular.element('.loading-indicator').hide();
}, 50);
}
You need to cancel previous timer. See documentation here: https://docs.angularjs.org/api/ng/service/$timeout.
var yourTimer;
function showLoading() {
if (yourTimer) {$timeout.cancel(yourTimer);}
yourTimer = $timeout(function () {
angular.element('.loading-indicator').show();
}, 50);
}
function hideLoading() {
if (yourTimer) {$timeout.cancel(yourTimer);}
yourTimer = $timeout(function () {
angular.element('.loading-indicator').hide();
}, 50);
}
I can't understand for which reason you use a timer for loading spinner, i recommend use only css if you use for the animation.
Here a little of my code:
HTML
<div ui-view class="fade container-fluid"></div>
<div class="spinner modal-viewer" hidden>
<div class="transparente"></div>
<div class="contenedor">
<div class="center-middle">
<div class="sk-circle">
<div class="sk-circle1 sk-child"></div>
<div class="sk-circle2 sk-child"></div>
...
<div class="sk-circle11 sk-child"></div>
<div class="sk-circle12 sk-child"></div>
</div>
</div>
</div>
</div>
You need manage them out of the uiView, because when you let out of a state, you will lose control of the state controller, then you can't disappear the spinner.
For that, put the spinner out of the uiView element, as above example.
JS
document.querySelector('.spinner')['style'].display = 'none';
My apologies for my poor previous answer.

Prevent digest of parent scopes in Angular

I am investigating Angular as a potential framework to use for an upcoming project. The test app I am making consists of an unordered list of which list items can be added to via an add link. Each list item contains a number of checkboxes. The number of checkboxes for a particular list item can be increased or decreased with plus and minus links next to each list item. Observe:
Hopefully that makes sense. Each checkbox has an ng-model directive binding the value of the checkbox to a property in an object. When the application is in the state above clicking any of the checkboxes fires six checks (one for each checkbox) -- the entire $scope of the root controller is checked for changes. Ideally, only the $scope of the relevant list item would be checked for changes. How can I accomplish this? I've attached my test code for reference. I've tried adding ng-click="$event.stopPropagation()" to the input node as well as to the li node but this appears to increase (double) the number of checks in the digest.
HTML:
<div ng-app ng-controller="App">
<ul>
<li ng-repeat="line in lines" ng-controller="LineController">
<input type="checkbox" ng-repeat="box in line.boxes" ng-model="box.on" />
<a ng-show="line.boxes.length > 1" ng-click="removeBox()">-</a>
<a ng-click="addBox()">+</a>
</li>
</ul>
<a ng-click="addLine()">Add</a>
</div>
JavaScript:
function App($scope) {
$scope.lines = [];
$scope.addLine = function () {
$scope.lines.push({
boxes: []
});
};
}
function LineController($scope) {
$scope.addBox = function () {
var box = {};
Object.defineProperty(box, 'on', {
enmerable: true,
get: function () {
console.log('Get!');
return this._on;
},
set: function (on) {
this._on = on;
}
});
$scope.line.boxes.push(box);
};
$scope.removeBox = function () {
$scope.line.boxes.pop();
};
}
If your concern is that AnguarJS dirty checking is going to be too slow for your needs, your question really need to be "is AngularJS going to be to slow to build X?" If X is a 3D game with lots of constant rendering then the answer is probably yes, AngularJS is not what you want. If X is "a scalable business/consumer oriented single page application", then the dirty checking algorithm is not going to be your bottle neck.
This SO answer has a good explanation of how data binding works and talks a bit about performance concerns.
What about to use $watch. We can invoke watch only for specific row. That means if you have 4x4 matrix (4 rows , 4 columns) on any checkbox state change we call watch 4 times
var webApp = angular.module('myModule', []);
webApp.controller('App', function ($scope) {
$scope.lines = [];
$scope.addLine = function () {
console.log("addLine");
$scope.lines.push({
boxes: []
});
};
});
webApp.controller('LineController', function ($scope) {
$scope.addBox = function () {
var box = {};
/* Object.defineProperty(box, 'on', {
enmerable: true,
get: function () {
console.log('Get!');
return this._on;
},
set: function (on) {
this._on = on;
}
});*/
$scope.line.boxes.push(box);
$scope.$watch(function () {
return $scope.line.boxes;
},
function (newValue, oldValue) {
if(newValue == oldValue) return;
console.log('Get new checkbox!');
}, true);
};
$scope.removeBox = function () {
$scope.line.boxes.pop();
};
});
Demo Fiddle

Categories