clearing / disabling a filter in AngularJS - angularjs

I am curious if there is a way to disable a filter once its been applied.
for instance:
<h3 class="mention" ng-bind-html="mention | getPlace" place-directive > <span>#</span>{{mention}}</h3>
filter:
hungryApp.filter('getPlace', function($stateParams) {
return function(place) {
return '<a>' + '#' + place.placeinfo.placename + '</a>';
}
});
Once the element is rendered, is there an angular way for that filter to not effect the elements it has already filtered?
I was able to come up with a dirty solution of writing a directive that basically replaces the element with a cloned copy, but definitely not the cleanest solution.
Thanks for any suggestion.

Answer is accepted but actually not good. As in Angular 1.4 one time binding was introduced:
{{::mention | getPlace}}

Accept a boolean parameter in your filter, and make it return the input, untouched, if the boolean is true (or false).
Then pass a boolean variable as argument to your filter, and toggle it to change the behavior of the filter:
hungryApp.filter('getPlace', function($stateParams) {
return function(place, ignore) {
if (ignore) {
return place;
}
else {
return '<a>' + '#' + place.placeinfo.placename + '</a>';
}
};
});
In the controller:
$scope.placeFilteringIgnored = false;
$scope.togglePlaceFiltering = function() {
$scope.placeFilteringIgnored = !$scope.placeFilteringIgnored;
}
And in the view:
<h3 class="mention" ng-bind-html="mention | getPlace:placeFilteringIgnored"></h3>
<button ng-click="togglePlaceFiltering()">Toggle place filtering</button>

Related

AngularJS: Apply filter to template

I created a simple filter to format a number based on the current locale.
angular.module('myApp').filter('doubleFilter', DoubleFilter);
function DoubleFilter($translate) {
return function(val, decimalPlaces) {
if (val && (typeof val === 'number')) {
val = val.toFixed(decimalPlaces);
if ($translate.use() === 'de_DE') {
val = val.replace('.', ',');
}
}
return val;
}
};
I call this filter in my template like this and it works fine:
{{dog.weight | doubleFilter : 2}}
However when I change the language using $translate.use('en_US') the format of the numbers in my template are not updated. Obviously I am missing out on something here.
How can I update the view when the language changes?
I think your directive needs to listen for 'local changed notification', for example :
scope.$on('$localeChangeSuccess', function () {
// code to execute the filter
});
Making your filter stateful may help ( see https://docs.angularjs.org/guide/filter )

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

Is there a way I can speed up the way my grid reacts when I have many rows?

I have code that creates a grid like this:
<div ng-repeat="row in home.grid.data track by row.examId">
<div>{{ row.examId }}</div>
<div>xxxx</div>
</div>
I have more columns after these.
Is there a way I can speed up the way my page reacts? It seems that when
I have a lot of data in the grid then the pages reacts slowly. Would it
make a difference if I used ng-model in an input type field for the row.examId. Note that
some of the fields that follow can be edited but most are just display only.
I believe bindonce does exactly what you need.
By reducing the number of watchers it allows the page to become more responsive. Check their demos.
This is what I have done. There are two ways. Irrespective of both solutions, use bindOnce. Keep a look out on the number of watchers on the page. Look at the end of this solution - how to keep track of watchers on a page.
I have added a solution 3 and this is working awesome, styling is a bit difficult
Solution 1:
Use a pagination control with bind once.
Solution 2
This is what worked for me and it is very elegant. You repeat with bindonce and then implement infinite scrolling. I have followed this blog post and it works like a charm. The idea is you limit the number of rows and change the limit as you scroll.
ng-repeat="item in items | orderBy:prop | filter:query | limitTo:limit"
Essentially, your html would look like this. I have modified the OP's code to use bindonce.
<div id="estates-listing" extend-height>
<div class="content" infinite-scroll="addMoreItems()" infinite-scroll-distance="2">
<div class="content-wrapper">
<div class="house" bindonce="estate" ng-animate="'animate'" ng-class="{inactive: (selectedEstate != null || selectedEstate != undefined) && estate.id!=selectedEstate.id , active:(selectedEstate != null || selectedEstate != undefined) && estate.id==selectedEstate.id}" ng-repeat="estate in estates | orderBy: orderProp : orderReverse | limitTo: config.itemsDisplayedInList track by estate.id" ng-mouseover="highlightMarker(estate)" ng-mouseleave="leaveMarker(estate)" ng-click="showDetailview(estate.id)" >
<div id="l-el{{estate.id}}">
</div>
</div>
</div>
</div>
</div>
Here is the infinite scroll directive from the post. Add this to your app, please don't use the standard infinite scroll using bower install.
app.directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
var checkWhenEnabled, handler, scrollDistance, scrollEnabled;
$window = angular.element($window);
elem.css('overflow-y', 'scroll');
elem.css('overflow-x', 'hidden');
elem.css('height', 'inherit');
scrollDistance = 0;
if (attrs.infiniteScrollDistance != null) {
scope.$watch(attrs.infiniteScrollDistance, function(value) {
return scrollDistance = parseInt(value, 10);
});
}
scrollEnabled = true;
checkWhenEnabled = false;
if (attrs.infiniteScrollDisabled != null) {
scope.$watch(attrs.infiniteScrollDisabled, function(value) {
scrollEnabled = !value;
if (scrollEnabled && checkWhenEnabled) {
checkWhenEnabled = false;
return handler();
}
});
}
$rootScope.$on('refreshStart', function(event, parameters){
elem.animate({ scrollTop: "0" });
});
handler = function() {
var container, elementBottom, remaining, shouldScroll, containerBottom;
container = $(elem.children()[0]);
elementBottom = elem.offset().top + elem.height();
containerBottom = container.offset().top + container.height();
remaining = containerBottom - elementBottom ;
shouldScroll = remaining <= elem.height() * scrollDistance;
if (shouldScroll && scrollEnabled) {
if ($rootScope.$$phase) {
return scope.$eval(attrs.infiniteScroll);
} else {
return scope.$apply(attrs.infiniteScroll);
}
} else if (shouldScroll) {
return checkWhenEnabled = true;
}
};
elem.on('scroll', handler);
scope.$on('$destroy', function() {
return $window.off('scroll', handler);
});
return $timeout((function() {
if (attrs.infiniteScrollImmediateCheck) {
if (scope.$eval(attrs.infiniteScrollImmediateCheck)) {
return handler();
}
} else {
return handler();
}
}), 0);
}
};
}
]);
Solution 3:
Be adventurous and use UI-Grid, UI Grid is the new ng-grid. It is not production ready, but we are playing around in production in a table where we have over 1000 records- out of the box it is awesome. The tutorials are extensive but not much SO support. It has virtualization in built and since it is an extension of ng-grid, it has a lot of backward compatibility. Here is a example with 10,000 rows
Number of watchers on the page:
Here is a function to track the number of watchers on the page. The thumb rule is never exceed 2500 watchers, but we restrict ourselves to < 1000.
$scope.TotalWatchers = function () {
var root = $(document.getElementsByTagName('body'));
var watchers = 0;
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
watchers += (element.data().$scope.$$watchers || []).length;
}
angular.forEach(element.children(), function (childElement) {
f(angular.element(childElement));
});
};
f(root);
return watchers;
};
The biggest thing I've found to help with performance of large tables is to limit event binding to the parent object and make use of bubbling to capture the events of the children.
In the event of the parent you can get which target was hit. I use the following code.
obj.onclick = function (e) {
e = window.event || e;
var t = e.target || e.srcElement;
}
in this event e is your regular event object and t is the object that was the initial target before the event bubbled. You need to use t as 'this' references the object that the event is bound to not the object that triggered the event.
Where I was using the code which was a really large table it reduced the rendering time of the table by almost 80% by moving the events to a parent node that was static. This also helps if you need to update the contents as you don't have to re-bind any events.
Hope this helps.

Looking for a better way to loop through and display one element at a time in AngularJS

I'm building an app that has animations in it and I need it to work better. I want to be able to go through a series of divs at a specified animation interval and display one at a time. And I want each series to have its own speed and its own divs.
Here's what I have so far (also copied below): http://jsfiddle.net/ollerac/shkq7/
Basically, I'm looking for a way to put the setInterval on a property of of the animatedBox so I can create a new animatedBox with custom properties. But every time I try to do this it breaks.
HTML
<div ng-app ng-controller="BoxController">
<div class="layer" ng-repeat="layer in animatedBox.layers" ng-style="{ 'backgroundColor': layer.color}" ng-show="layer == animatedBox.selectedLayer"></div>
</div>
JAVASCRIPT
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
var i = -1;
setInterval(function () {
$scope.$apply(function() {
i++;
if (i < $scope.animatedBox.layers.length) {
$scope.animatedBox.displayThisLayer = $scope.animatedBox.layers[i];
} else {
i = 0;
$scope.animatedBox.selectedLayer = $scope.animatedBox.layers[i];
}
});
}, 500);
}
CSS
.layer {
width: 30px;
height: 30px;
position: absolute;
}
*Update*
Here's more along the lines of what I want to do:
updated jsFiddle: http://jsfiddle.net/ollerac/shkq7/2/
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null,
selectedLayerIndex: -1,
updateSelectedLayer: function () {
var self = this;
if (self.layers.length) {
$scope.$apply(function() {
self.selectedLayerIndex++;
if (self.selectedLayerIndex < self.layers.length) {
self.selectedLayer = self.layers[self.selectedLayerIndex];
} else {
self.selectedLayerIndex = 0;
self.selectedLayer = self.layers[self.selectedLayerIndex];
}
});
}
}
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
setInterval(function () {
$scope.animatedBox.updateSelectedLayer();
}, 500);
}
So now the object updates its own selectedLayer property. But I still need to call the setInterval that calls the update separately in order to get it to update. But I'd like the object to update itself and be completely independent. Can you think of a good way to do this because I'm really stuggling with it...
I guess this is more of a general javascript question, but I thought there might be an Angular way to handle this type of situation, like maybe using a directive or something would be appropriate.
Any suggestions would be much appreciated.
You are correct, I believe a directive is the right solution here. (This one was a fun one to work on, by the way. :)
When approaching a problem like this, I usually start by writing the HTML and controller that I'd wish I could write, if everything already worked. For this example, here's what I ended up with.
<div ng-controller="BoxController">
<div animated-boxes="colors"></div>
</div>
app.value('randomColor', function() {
var red = Math.floor(Math.random() * 255);
var green = Math.floor(Math.random() * 255);
var blue = Math.floor(Math.random() * 255);
return "rgb(" + red + "," + green + "," + blue + ")";
});
app.controller('BoxController', function($scope, randomColor) {
$scope.colors = [ randomColor(), randomColor() ];
});
Here, the controller is only responsible for setting some basic data on the scope--an array of colors; the DOM is very simple, only passing in that array to something called animated-boxes. randomColor has been moved into a service so it can be reused and tested more easily. (I also changed it a bit so it doesn't result in bad hex values.)
Now, the only part that doesn't already work is this thing called animated-boxes. Any time you want to interact with the DOM, or to trigger some behavior when an HTML attribute is used, we move to a directive.
We'll define our directive, injecting the $timeout service since we know we want to do timer-based stuff. The result of the directive will just be an object.
app.directive('animatedBoxes', function($timeout) {
return {
};
});
Since we want the directive to be self-contained and not mess up the outer scope in which it's contained, we'll give it an isolate scope (see the directive docs for more information, but basically this just means we have a scope that's not attached to the scope in which the directive lives except through variables we specify.)
Since we want to have access to the value passed in to the directive via the HTML attribute, we'll set up a bi-directional scope binding on that value; we'll call it colors.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
}
};
});
We'll give it a simple template that loops over colors and outputs one of our divs per each color. Our ng-show indicates that the div should only be shown if the scope value selected is equal to $index, which is the array index of the current iteration of the ng-repeat loop.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>"
};
});
Now for the link function--the function that will handle the directive's logic. First, we want keep track of which box we're showing; in our ng-show, we used selected for this. We also want to keep track of how many boxes we have; we'll use $watch on our directive's scope to keep up with this.
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
// whenever the value of `colors`, which is the array
// of colors passed into the directive, changes, update
// our internal count of colors
if (value) count = value.length;
else count = 0; // if `colors` is falsy, set count to 0
}, true); // `true` ensures we watch the values in the array,
// not just the object reference
}
Finally, we need to cycle through each box every so often. We'll do this with $timeout, which is a version of setTimeout that includes a scope $apply call (it does some other stuff, but we don't care about that now).
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
// recursively use `$timeout` instead of `setInterval`
$timeout(nextBox, 500);
};
// kick off the directive by launching the first `nextBox`
nextBox();
If you put the entire directive so far together, you'll end up with this code (comments removed):
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>",
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
if (value) count = value.length;
else count = 0;
}, true);
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
$timeout(nextBox, 500);
};
nextBox();
}
};
});
A full working example, including comments and a little debugging area where you can see the value of colors and interact with it (so you can see how the directive responds to changes in the controller) can be found here: http://jsfiddle.net/BinaryMuse/g6A6Y/
Now that you have this, consider trying to apply this knowledge to allow the directive to have a variable speed by passing it in via the DOM, like we did with colors. Here's my result: http://jsfiddle.net/BinaryMuse/cHHKn/

Adding a computed field to every element of an array in an AngularJS model

I'm pulling an array of users into my AngularJS model from a JSON datasource. This data is being rendered in a table, and I'd like to create a column that is computed from two values of the existing user object, without modifying my underlying data service.
// My model
function UserListCtrl($scope,$http) {
$http.get('users').success(function(data) {
$scope.users = data;
});
};
In my partial template, I know I can do something like this:
<tr ng-repeat="for user in users">
<td>{{user.data / user.count | number:2}}</td>
</td>
But I'd rather add that field into the model, so I can use it like so:
<td>{{user.amplification}}</td>
How do I add the "amplification" field to every user in my model?
As an aside, is it possible to use the orderBy filter on something like this:
<td>{{user.data / user.count | number:2}}</td>
You can eather:
Just after loading user do:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification() = function() { return $scope.user.data / $scope.user.count; }
});
And use as {{user.amplification()}}
Anywhere at controller:
$scope.$watch('user', function() {
$scope.userAmplification = $scope.user.data / $scope.user.count;
}, true);
$http.get
Or if user.data/count do not change, do same as 1. but staticly calculate:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification = $scope.user.data / $scope.user.count;
});
And OrderBy could be used on any expression (uncluding result of other filter)
If you don't need your amplicification() function to update when the data and count properties on your user update, you can do something like this in your controller:
$scope.users.forEach(function(user) {
user.amplification = function() {
return user.data / user.count;
};
});
Adding a second answer as I feel it's appropriate as it's distinct from my first one.
After a little looking around, I found the method I originally posted falls over if you try to add new rows dynamically, or new elements to the array which depend on the computed value. This is because the $scope.array.forEach() will only run when the controller is created.
The best way to solve this problem is to create a properly defined object which contains the options you want. e.g.
function Task(id, name, prop1, prop2) {
this.id = id;
this.name = name;
this.prop1 = prop1;
this.prop2 = prop2;
this.computedProperty = function () {
return this.prop1 + this.prop2;
};
}
This is far more flexible as each new object created will have the new property.
The only downside is that in your ajax success callback, you'll need to pass each of your users into your 'Users()' constructor.
What worked for me was to add a loop and add the property to each item in that loop. I used a property of the controller but I am sure you can use scope the way you are approaching it in the question.
function(result) {
self.list = result;
angular.forEach(self.list, function(item) {
item.hasDate = function() {
return this.TestDate != null;
}.bind(item); // set this context
});
}
Then in my markup I just used it like this.
<div ng-repeat...>
<div ng-show="item.hasDate()">This item has a date.</div>
</div>

Resources