AngularJS: Filtering array before directive - javascript

I've got a directive which I want to use in different cases, but all of them will be providing a list (array) from which it will take the information.
The problem is that every array will be / can be filtered by multiple filters (and some of them are custom filters). For example:
<select-search data-list="Ctrl.players | playersByDate:Ctrl.event.datetime | orderBy: 'name'"></select-search>
Those arrays, as I said, can have one, multiple or zero filters applied, some of them custom-made and some of them not (like orderBy). The problem is that when I do this, I get a $digest cycle error with any filters (custom-made and Angular filters aswell).
First, I've read the reasons why, but I do not understand them, because I'm using a one-way binding on the list:
scope:{
property: '#',
ref: "=",
list: "<",
change: "&?"
}
Therefore, it should only be triggering once (when it's filtered the first time), because after this no other changes are made in any of the sides.
Regarding this, I would like to know how can I filter the list/array at the moment of sending it to the directive (so, not filtering and storing it via JS in another variable) without getting any $digest error.
EDIT:
As requested, I've written the code in a snippet (JsFiddle here):
(function(){
var app = angular.module("AppMd", []);
app.controller("Control", [function(){
var vm = this;
vm.players = [
{name: 'Player 1'},
{name: 'Player 2'},
{name: 'Player 3'}
];
}]);
app.directive("selectSearch", function(){
return {
restrict: 'E',
scope:{
list: "<"
},
replace: true,
template: '<ul><li data-ng-repeat="obj in list">{{obj.name}}</li></ul>',
link: function(scope, element, attrs, controller, transclude){
return false;
}
}
});
})();
<script src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
<div ng-app="AppMd" ng-controller="Control as Ctrl">
<select-search data-list="Ctrl.players | orderBy: 'name'"></select-search>
</div>
As you can see, it works, but if you look at the console, you'll see the errors it triggers.
Thank you!

Related

Optimal way to use Angular Directives and JSON

I want an efficient way to factor an Angular Directive that is written to display a chart.
After reading other answers here, I found a nice way to create a directive that displays a single chart without any problem.
How do I reuse the same directive to display different charts? Each chart needs a JSON object that has settings and data in order to render.
I don't want to pollute my Angular View by typing 100-150 lines of JSON and passing it in via the directive.
Details:-
Each chart has some common key/value pairs that I can leave in the directive.
How do I infuse chart specific key & value pairs in each directive?
Eg:- Say I want one chart to have green bars and the other chart to have red lines.
Angular Directive
(function () {
'use strict';
angular
.module("analytics")
.directive("angularDirectiveAmcharts", angularDirectiveAmcharts);
function angularDirectiveAmcharts() {
var directive = {
link: link,
restrict: 'A',
replace: true,
scope: {
chartdata: '=',
type: '=',
customstyle: '#',
chartsettings: '=',
chartid: '#'
},
template: '<div id="{{ chartid }}" style="{{ customstyle }}"></div>'
};
return directive;
function link(scope, elem, attrs) {
AmCharts.makeChart(scope.chartid, {
"type": "serial",
"categoryField": "date",
"autoMarginOffset": 10,
"marginRight": 20,
"marginTop": 20,
//I've deleted lots of keys and values for the sake of brevity
"dataProvider": scope.chartdata
});
}
}
})();
View
<div class="chartarea" ng-controller="pcController as vm">
<div angular-directive-amcharts chartid="chartdiv" chartdata="vm.chart_data"></div>
</div>
I am particular about maintainability because a lot of changes are going to made after I'm done with my internship.
Parts of the given code in this answer are based on another answer
You could use a service to provide a standard configuration to all of your chart directives. In this service you can define this standard configuration once and merge it with a specific configuration each time, a directive is created. This way you only have to declare minor changes in your controller.
Nonrequired but possible config binding into directive:
<div ng-controller="myCtrl">
<my-chart></my-chart>
<my-chart config="conf"></my-chart>
</div>
Specific configuration in controller:
myapp.controller('myCtrl', function ($scope) {
$scope.conf = {
graphs: [{ type: 'column' }]
};
});
Service for default configuration (using jQuerys way to deep merge objects):
myapp.service('chartService', function () {
this.defaultConfig = {
"type": "serial",
// reduced object for readabilty
};
this.getConfig = function (mergeObj) {
return $.extend(true, {}, this.defaultConfig, mergeObj);
}
});
The data is get through another service, and added to the configuration after the merge:
var config = chartService.getConfig(scope.config || {});
config.dataProvider = dataProvider.getData();
chart = AmCharts.makeChart(element[0], config);
I've prepared a fiddle, so you can take a look into an example.

Fitting multiple dependent datasets into AngualrJs ngRepeat

I am looking for a little bit of help with logically fitting two objects with common reference into AngularJs ngRepeat.
Example objects (these get called from a service):
$scope.objArr1 = [
{ id: 1, Name: 'Name 1', Value: 'Value 1', },
{ id: 2, Name: 'Name 3', Value: 'Value 2', },
{ id: 3, Name: 'Name 3', Value: 'Value 3', },
];
$scope.objArr2 = [
{ id: 1, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 1, Name: 'Name 1', Value: 'Value 1', }, },
{ id: 2, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 1, Name: 'Name 1', Value: 'Value 1', }, },
{ id: 3, Name: 'Name 1', Value: 'Value 1', ObjArr1: { id: 3, Name: 'Name 3', Value: 'Value 3', }, },
];
Something along those lines. Basically if you can think of it this way; first array objects form buckets while second array objects form items that fit into corresponding bucket.
First approach
HTML:
<ul>
<li data-ng-repeat="item in objArr1 | filter : someFilter">{{item.Name}}
<ul>
<!-- how to filter objArr2 items based on objArr1 property ? -->
<li data-ng-repeat="item2 in objArr2 | filter : someOtherFilter">{{item2.Name}}</li>
</ul>
</li>
</ul>
In simple terms I was trying to filter $scope.objArr2 items that correspond to the current repeater item in the inner repeater. I tried various things with someOtherFilter but I was unable to reference the item from outer repeater.
Problem
I couldn't figure out how get this filtering bit to work.
Second approach
When all else failed I decided to combine the data structures into one like so:
// deep copy to avoid dependency
angular.copy($scope.objArr1, $scope.objArr3);
// loop over objArr3 and add empty array objArr2
// which we will populate a bit later
angular.forEach($scope.objArr3, function (val, key) {
$scope.objArr3[key]["objArr2"] = [];
});
Then I setup a $watch-er`to monitor both objArr1 and objArr2 because I don't know when these will return.
$scope.$watchGroup(['objArr1', 'objArr2'], function (newVals, oldVals) {
// check to make sure there is stuff to loop over
// i am wrongly assuming there will be items in both objArr1 and objArr2
// i'll worry about what to do when there is no data a bit later
if(newVals[0].length > 0 && newVals[1].length > 0) {
angular.forEach($scope.objArr1, function (val1, key1) {
angular.forEach($scope.objArr2, function (val2, key2) {
if (val1.Id === val2.objArr1.Id) {
$scope.objArr3[key1].objArr2.push(val2);
}
});
});
}
});
HTML:
<ul>
<li data-ng-repeat="item in objArr1 | filter : someFilter">{{item.Name}}
<ul>
<li data-ng-repeat="item2 in item.objArr2">{{item2.Name}}</li>
</ul>
</li>
</ul>
Problem
While this has worked just fine on the surface I get a lovely Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! in the console.
I am a bit puzzled what would cause for $digest to fire so many times.
However, by commenting update line $scope.objArr3[key1].objArr2.push(val2); of my watcher the error goes away. But then I don't understand how this would result in extra digest iterations.
Halp
In the end either of the approach that I came up with has some problem. While second approach actually does its job and populates my repeater correctly but there is that nasty error in the console.
Anyone with a bit more experience in this field please help.
Update
Some of the silly things I tried with someOtheFilter are:
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter"
$scope.someOtherFilter = function(item){
// item is always the current inner repeaters item2 object
// that just the way angular filter works
return item.objArr2 === $scope.objArr1.Id; // this is silly but idea is there
};
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter(item)"
$scope.someOtherFilter = function(item){
// if memory serves me right
// in this case item is always repeaters current item2 object
// with no ability to reference outer repeaters current item object
}
data-ng-repeat="item2 in objArr2 | filter : someOtherFilter(item, item2)"
$scope.someOtherFilter = function(item, item2) {
// if memory serves me right
// in this case item was always inner repeaters current item2 object
// and item2 is always undefined
// again with no ability to reference outer repeaters current item
}
At this point I gave up on first approach. But thinking about it now I might have been able to utilise $index (if inner repeater somehow or other didn't overwrite outer repeaters $index reference) to get index value of the outer repeater and try to get at $scope.objArr1[index].
No matter which scenario would have worked for someOtherFilter inner working only need to compare inner object objArr1.Id to outer objects Id.
UPDATE (learn from my mistakes)
OK, after confirming the answer as working I still had the same issue in my production example Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!.
After cooling down for a few days I decided to revisit this problem and this is what I found.
<div class="block" data-ng-repeat="team in Teams | filter : validateAgeCategory">
<div data-ng-style="getHeaderStyle()">
<span>{{team.Name}}</span>
<!-- bunch of things removed for brevity -->
</div>
<ul data-ng-style="getListStyle()">
<li data-ng-repeat="players in Players | filter : { Team: { Id: team.Id, }, }">
<a data-ng-style="getListItemStyle()" data-ng-href="#/players/{{player.Id}}">{{player.Name}}</a>
</li>
</ul>
</div>
I adapted my Team/Player example for easier understanding. Regardless notice that in production I use a few ng-style calls to retrieve CSS.
Reason why I am doing so is because IE has a tendency to remove {{}} definitions from inline style="color: {{color}};" definition during document load. It's IE bug so to speak.
Moving on, what I found is that the innermost ng-style was causing the error with $digest. By removing data-ng-style="getListItemStyle()" everything is happy. Everything bu me of course.
Looking at this as an overhead it would be better to create CSS classes and instead apply classes based on some indexing to style my HTML.
There you have it.
OK, I'll try my best to help.
I think the problem with your second approach is somehow related to this question. Read the comments there. It might be related to the list being changed by the filter.
As for your first approach, I'm still not sure what you were trying to do, but I've created this example to show you that you can filter inside nested ngRepeats.
BTW, If you need to access outer $index inside an inner ngRepeat, you can use ngInit.

Array attribute of scope in directive is emptied upon reference

My directive looks like this:
angular.module('app')
.directive('chart', function () {
return {
template: '<div class="chart"></div>',
restrict: 'E',
replace: 'true',
scope: {
data: '='
},
link: function (scope, element, attrs) {
console.log(scope);
console.log(scope.data);
}
};});
and gets passed an array from my controller in the view.
<chart data="array"></chart>
The console output looks as follows:
Scope {...}
$id: "006"
$parent: Child
$root: Scope
...
data: Array[1]
0: 10300
length: 1
__proto__: Array[0]
this: Scope
__proto__: Object
and
[]
When the scope object is displayed in the console it has an 'data' attribute with length 1 and the entry '10300', but when scope.data is printed it is just an empty array '[]'.
Why? I am very confused :)
The problem was that the array was initialized in the controller as [] and then filled through a function which was called with $watch. For some reason the two console outputs seem to print different states of the controller. Solution was to call the function once in the controller to initialize the array with values straight away.

How to use parameters within the filter in AngularJS?

I want to use parameter in filter, when I iterate some arrays with ng-repeat
Example:
HTML-Part:
<tr ng-repeat="user in users | filter:isActive">
JavaScript-part:
$scope.isActive = function(user) {
return user.active === "1";
};
But I want to be able to use filter like
<tr ng-repeat="user in users | filter:isStatus('4')">
But its not working. How can I do something like that?
UPDATE: I guess I didn't really look at the documentation well enough but you can definitely use the filter filter with this syntax (see this fiddle) to filter by a property on the objects:
<tr ng-repeat="user in users | filter:{status:4}">
Here's my original answer in case it helps someone:
Using the filter filter you won't be able to pass in a parameter but there are at least two things you can do.
1) Set the data you want to filter by in a scope variable and reference that in your filter function like this fiddle.
JavaScript:
$scope.status = 1;
$scope.users = [{name: 'first user', status: 1},
{name: 'second user', status: 2},
{name: 'third user', status: 3}];
$scope.isStatus = function(user){
return (user.status == $scope.status);
};
Html:
<li ng-repeat="user in users | filter:isStatus">
OR
2) Create a new filter that takes in a parameter like this fiddle.
JavaScript:
var myApp = angular.module('myApp', []);
myApp.filter('isStatus', function() {
return function(input, status) {
var out = [];
for (var i = 0; i < input.length; i++){
if(input[i].status == status)
out.push(input[i]);
}
return out;
};
});
Html:
<li ng-repeat="user in users | isStatus:3">
Note this filter assumes there is a status property in the objects in the array which might make it less reusable but this is just an example. You can read this for more info on creating filters.
This question is almost identical to Passing arguments to angularjs filters, to which I already gave an answer. But I'm gonna post one more answer here just so that people see it.
Actually there is another (maybe better solution) where you can use the angular's native 'filter' filter and still pass arguments to your custom filter.
Consider the following code:
<li ng-repeat="user in users | filter:byStatusId(3)">
<span>{{user.name}}</span>
<li>
To make this work you just define your filter as the following:
$scope.byStatusId = function(statusId) {
return function(user) {
return user.status.id == statusId;
}
}
This approach is more versatile because you can do comparisons on values that are nested deep inside the object.
Checkout Reverse polarity of an angular.js filter to see how you can use this for other useful operations with filter.
If you have created an AngularJs custom filter, you can send multiple params to your filter.Here is usage in template
{{ variable | myFilter:arg1:arg2... }}
and if you use filter inside your controller here is how you can do that
angular.module('MyModule').controller('MyCtrl',function($scope, $filter){
$filter('MyFilter')(arg1, arg2, ...);
})
if you need more with examples and online demo, you can use this
AngularJs filters examples and demo
This may be slightly irrelevant, but if you're trying to apply multiple filters with custom functions, you should look into:
https://github.com/tak215/angular-filter-manager
Example I have a students list as below :
$scope.students = [
{ name: 'Hai', age: 25, gender: 'boy' },
{ name: 'Hai', age: 30, gender: 'girl' },
{ name: 'Ho', age: 25, gender: 'boy' },
{ name: 'Hoan', age: 40, gender: 'girl' },
{ name: 'Hieu', age: 25, gender: 'boy' }
];
I want to filter students via gender to be boy and filter by name of them.
The first I create a function named "filterbyboy" as following:
$scope.filterbyboy = function (genderstr) {
if ((typeof $scope.search === 'undefined')||($scope.search === ''))
return (genderstr = "")
else
return (genderstr = "boy");
};
Explaination: if not filter name then display all students else filter by input name and gender as 'boy'
Here is full HTMLcode and demo How to use and operator in AngularJs example

Is it possible to switch template partial based on hash value in Mustache?

Ok, so I'm trying to get a grip on Mustache.js for rendering views in javascript. I have an API that returns a number of "events", which can be a number of different types. I want to render the events in (very) different ways, based on their type:
data : {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ] }
Ideally, I could do something like this:
{{#events}}
{{#message}}
<div class="message">{{title}}</div>
{{/message}}
{{#image}}
<span>{{title}}</span>
{{/image}}
{{/events}}
But that would (right?) force me to refactor my data into:
data : {
events: [ {message: {title: 'hello'}}, {image: {title: 'world'}} ] }
Is there a better way of solving this, without refactoring my data? Or should I just bite the bullet?
Mustache is logic-less so there's not much you can do with pure template code other than switching to Handlebars.
Your Mustache-friendly alternative would be to declare a helper and use it to select which template to render. It gets a little convoluted but you can avoid switching away from Mustache if that's something you can't change:
var base_template = '{{#events}}' +
'{{{event_renderer}}}' +
'{{/events}}';
var message_template = '<div>message: {{title}}</div>';
var image_template = '<div>image: {{title}}</div>';
var data = {
events: [ {title: 'hello', type: 'message'}, {title: 'world', type: 'image'} ],
event_renderer: function() {
return Mustache.render('{{> ' + this.type + '}}', this, {message: message_template, image: image_template});
}
}
Mustache.render(base_template, data);
The trick here is that you create a base template that will be the iterator, and pass in event_renderer as a helper. That helper will in turn call Mustache.render again, using partials to render each type of event you can have (that's the {{> partial}} notation).
The only ugly part here is that you need to add this event_renderer member to your JSON data, but other than that, it should all be fine (in Handlebars you could declare it as a helper and there's no need to merge it with your data).

Categories