Compare attributes of objects, change element class/tooltip if different - angularjs

With the following code:
public class Person {
private Long id;
private String name;
private List<Dog> dogs;
}
public class Dog {
private Long id;
private Long oldId;
private Long age;
}
I have 2 objects of Person, 'person' and 'editedPerson'. I want to compare the two, and if one attribute is different, change the class of a element and create a tooltip showing the value of the other object's attribute. And I want to be able to do this inside a ng-repeat too, compare Dog attributes inside persons list based on their id/oldId (compare the dog inside the ng-repeat with the dog that has the same oldID as dog's id)
This is an example of how I've been doing so far:
<b ng-class="{ 'different' : person.name != editedPerson.name)}"
tooltip="{{(person.name != editedPerson.name) ? 'New: ' + editedPerson.name : ''}}">
Name:</b>
<p>{{person.name}}</p>
The problem is that I'll have a LOT of attributes, and some of them are inside Lists of different types. The sollution I have for the List so far is to create one function for each attribute, for example
compareDogAge = function(dog, dogs) {
// Foreach on dogs until dogs[i].oldId == dog.id, return true if age is equal
}
I would like to know if I should keep my current solutions, or try to make/find a directive that can solve my problem (I have very little experience on making directives).
Thanks in advance
EDIT
I came up with the following function so far, but haven't tested it yet
equals = function(fieldName, originalObj, newObj) {
if (newObj instanceof Array) {
for (var i = 0; i < newObj.length; i++) {
if (originalObj.id == newObj[i].oldId) {
return originalObj[fieldName] == newObj[i][fieldName];
}
}
} else if (newObj instanceof Object){
return originalObj[fieldName] == newObj[fieldName];
}
}
I still think a directive would be better

Angular has a equals function, isn't that what you a looking for?
https://docs.angularjs.org/api/ng/function/angular.equals
angular.equals(person, editedPerson);
Edit
A directive do add this logic to you component would look like this:
Html
<div ng-app="myApp">
<div class="myDir">
<span different old-value='foo' new-value='bar'>FooBar</span>
</div>
</div>
Angular
var app = angular.module("myApp", []).directive('different', function () {
return {
restrict: 'A',
link: function(scope, element, attrs){
//Add your logic to check the differences here
//You can use the function you already have
if (attrs.oldValue !== attrs.newValue){
//Do other stuff you want to do with the element here
element.addClass('different');
}
}
}
});
Fiddle: http://jsfiddle.net/marcosspn/8ucakhk5/

Related

Angular 2 keep input form value in bind with the model using getter and setter

I would like to bind an input value to the model using getter and setters. In this way i can prevent and/or manipulate the input's value while writing inside it.
For example i want the prevent numbers inside an input box. So, if write 'abc' all is ok, then if I start writing a number nothing should happen (to the model and to the input's value). The issue is that with the following code i'm able to write anything inside the input box (but the model it's correct). This means that the input box value is not really representing my model.
NOTE: The reason beyond this questions is that I want to use my models to validate forms, preventing for example specific characters. I would like to not use reactive forms as i want to keep my validations inside my models not components. Also note that in a real scenario i would have a UserModel class with inside name and other fields with their validations.
#Component({
selector: 'my-app',
template: `
<div>
<h2><input type="text" [(ngModel)]="name"> {{name}}</h2>
</div>
`,
})
export class App {
_name:string = 'ss';
constructor() {
}
// In real scenario those 2 methods are in a separate class UserModel
get name() {
return this._name;
}
set name() {
if ((new RegExp(/^[a-zA-Z]*$/).test(val))) {
this._name = val;
}
}
}
If you manipulate the value in the setter, this can cause issues with change detection, so that ngModel doesn't pick up the changes and doesn't update the <input>
To work around you can use
export class App {
_name:string = 'ss';
constructor(private cdRef:ChangeDetectorRef) {}
get name() {
return this._name;
}
set name(value:String) {
this._name = value + 'x';
this.cdRef.detectChanges()
}
}
if you reset the value to the previous value, you might need to pass an artificial different value first, otherwise change detection won't detect a change and even detectChanges() won't update the input.
set name(value:String) {
var oldVal = this._name;
this._name = null;
this.cdRef.detectChanges()
this._name = oldVal;
this.cdRef.detectChanges()
}
Based on #Günter Zöchbauer answer i made a workaround. It's not definitive and could be more abstract, but for now it's ok.
export class App implements OnInit {
#Input() userModel: UserModel = null;
public _vm;
constructor(private _changeDetectionRef: ChangeDetectorRef) {
}
/**
* Initalize view model, it's important to keep names specular
*/
ngOnInit() {
this._vm = {
name: this.userModel.name,
surname: this.userModel.surname,
};
}
/**
* Helper for avoid detectchanges inside the modal, and reduce boilerplate. We could also ad an interface/type of the possibile field value, ie type fieldT= 'name' | 'surname';
* #param field
* #param val
*/
protected updateModel(field, val: string): void {
this._vm[field] = null;
this._changeDetectionRef.detectChanges();
this.userModel[field] = val;
this._vm[field] = this.userModel[field];
this._changeDetectionRef.detectChanges();
}
}
In userModel:
....
public get name(): string {
return this.name';
}
public set name(val: string) {
if ((new RegExp(/^[a-zA-Z]*$/).test(val))) {
this.name = val;
}
}
In template:
<input type="text" name="userName" [ngModel]="_vm.name" (ngModelChange)="updateModel('name', $event)">
You can use (ngModelChange) and [ngModel] to test the content of your model upon change.
As you can see in this Plunker the model wont change if it is not valid.
#Component({
selector: 'my-app',
template: `
<div>
<h2><input #input type="text" [ngModel]="name" (ngModelChange)='valid(input.value)'> {{name}}</h2>
</div>
`,
})
export class App {
name:string = 'ss';
constructor() {
}
valid(value){
if(value){ //<--- Your test here
this.name = value;
}
}
}

OR logic on builtin filters in AngularJs

I have an object that looks something like this:
{HospitalName: "hospital name", DoctorDetails: {Name: "doctor name"}}
I am using ng-repeat to iterate over a list of such objects. At the same time, I am applying two filters with an OR logic such that at least one of them should be return a match(if any).
<input type="text" ng-model="searchQuery" placeholder="Search by hospital or doctor name">
<div ng-repeat="hospital in list | filter: {HospitalName : searchQuery} || {DoctorDetails : {Name : searchQuery}}">
<div>{{hospital.HospitalName}}</div>
</div>
However, only the first filter gets triggered. Even though there are objects that have doctor names that match the search query, no matches are returned. Any ideas where I am going wrong?
I was using this post as reference for the conditional logic on the filter: Making an Angular Filter Conditional
UPDATE:
I was not able to figure out why the conditional logic was not working. So, taking #Walfrat's advice,I had to resort to using a custom filter and it gets the job done. If anyone is interested, here is the code I used:
angular.module('TestApp')
.filter('searchBox', function($filter, $rootScope) {
return function(input, searchParam) {
if(input) {
var searchResults = null;
//check if a valid search query has been entered
if(searchParam){
//use $filter to find hospital names that match query
searchResults = $filter('filter')(input,{HospitalName: searchParam});
//if no hospital names match, fill searchResults with doctor name results(if any) that match query
//otherwise just append the results to searchResults
if(searchResults.length == 0){
searchResults = $filter('filter')(input,{DoctorDetails : {Name: searchParam}});
}
else{
angular.forEach($filter('filter')(input,{DoctorDetails : {Name: searchParam}}), function(result){
searchResults.push(result);
});
}
//if there are no doctor or hospital names that match,
// set searchResult to approprriate message to denote no results found
if(searchResults.length == 0){
searchResults = ["Nothing"];
}
return searchResults;
}
//console.log(out);
return input;
}
return [];
}
});
For more concise code, see #Alex Chance's answer. Cheers!
In your situation, it may be easier to apply a custom filter function.
You apply the filter in your ng-repeat like so
<div ng-repeat="hospital in list | filter:filterFn ">
The filter function would look like this:
$scope.filterFn = function(hospital)
{
return hospital.HospitalName.search($scope.searchQuery) != -1 ||
hospital.DoctorDetails.Name.search($scope.searchQuery) != -1;
}
Here is a working fiddle.

AngularJS: Using class and ng-class in the same element and other unsuccessful attempts

I have the div below which HAS to have the frmBar class assigned and also a second class depending on the value of Request_Type (SAP - clr-purple, WEB - clr-darkblue, and three others).
When I try either of the following, neither class is assigned.
<div ng-class="{frmBar SAP:'clr-purple', Web:'clr-darkblue'}[myrequest.request_type]">
<div ng-class="{SAP:'frmBar clr-purple', Web:'frmBar clr-darkblue'}[myrequest.request_type]">
This one applies frmBar but not the ng-class:
<div class="frmBar" ng-class="{SAP:'clr-purple', Web:'clr-darkblue'}[myrequest.request_type]">
Any thoughts?
ng-class's object form takes an object where the keys are the class names and the values are boolean expressions that say whether to apply that class.
<div class="frmBar" ng-class="{SAP: myrequest.request_type == 'SAP', Web: myrequest.request_type == 'Web'}">
The values of the object can be scope values, so you could, for instance, have:
app.myController = function($scope) {
$scope.isSAP = function() {
return $scope.myrequest.request_type == 'SAP';
};
$scope.isWeb = function() {
return $scope.myrequest.request_type == 'Web';
};
}
and
<div class="frmBar" ng-class="{SAP: isSAP(), Web: isWeb()}">

Always display a Key First with Angular

I have an ng-repeat like:
<div ng-repeat="(k, v) in query_p[$index].tags">
I would like it so that if the key (k) is a certain string, say "foo", that string always appears first in the list. It seems the getter option or orderBy only works with arrays. Anyone have an example of how they might achieve this?
Basically you have an unordered object, and you want it to have some kind of order.
To do that you need to create a function that returns some ordered object.
myApp.filter('promote_foo', function() {
return function(object, comp) {
console.log(object);
console.log(comp);
var ordered = [];
for (var key in object) {
var obj = {
key: key,
value: object[key]
};
if (key === comp)
ordered.splice(0,0,obj);
else
ordered.push(obj);
}
console.log(ordered);
return ordered;
};
});
the function takes a parameter which will promote and object if the key matches it. Now I only call it in the controller directly, but you could use it just like any angular filter.
$scope.order = $filter('promote_foo')($scope.data, 'foo');
Also, you can play with the fiddle here.
Hope this helped!

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