10 $digest() iterations reached when using isolate scope in directive - angularjs

I want to create an AngularJs directive to authorize based on some claims.
Basically I want an attribute in any html element (button, div, a, etc.) where I specified what groups can view the item and depending on whether the current user has these groups, the element will have an ng-show set to true or false.
This is my directive in typescript. I am injecting a service that takes care of checking whether the current user has or not any of the claims:
//
// Directive to hide or show html element based on group claims for the current user
//
// Usage: <button authorize="mycontroller.authorizedGroupsArray" />
//
namespace app.blocks.directives {
"use strict";
class AuthorizeDirective implements ng.IDirective {
public restrict: string = "A";
public replace: boolean = true;
public scope: any = {
authorizedGroups: "=authorize"
};
constructor(private $compile: ng.ICompileService, private authService: services.IAuthService) {
console.log("authorize directive init");
}
public static factory(): ng.IDirectiveFactory {
const directive = ($compile: ng.ICompileService, authService: services.IAuthService) =>
new AuthorizeDirective($compile, authService);
directive.$inject = [
"$compile",
"app.services.AuthService"
];
return directive;
}
public link(
scope: ng.IScope,
instanceElement: ng.IAugmentedJQuery,
instanceAttributes: ng.IAttributes): void {
let el = angular.element(instanceElement);
let groups: Array<string> = (<any>scope).authorizedGroups || [];
let authorized: boolean = this.authService.isUserAuthorized(groups); // returns true if the user has any of the groups among its claims
el.attr("ng-show", String(authorized));
//remove the attribute, otherwise it creates an infinite loop.
el.removeAttr("authorize");
this.$compile(el)(scope);
}
}
angular
.module("app.blocks.directives")
.directive("authorize", AuthorizeDirective.factory());
}
And I am using this directive like this assuming that for example in myCtrl I have a property like:
public get groupsAuthorized(): Array<string> {
return [
"accounting"
"administrators"
];
}
So in my view:
<button type="button" class="btn btn-default" authorize="myCtrl.groupsAuthorized">TEST</button>
The problem with this code is that works but it throws an error
app.globalexceptionhandler.config.ts:28 Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!(…)(anonymous function) # app.globalexceptionhandler.config.ts:28$apply # angular.js:17792done # angular.js:11831completeRequest # angular.js:12033requestLoaded # angular.js:11966
angular.js:68 Uncaught Error: [$sce:itype] Attempted to trust a non-string value in a content requiring a string: Context: html
The directive code is executed only once, and I am removing the attribute authorize after execution to ensure the directive is not executed in a loop. So I am not too sure what's going on.
If I remove the isolated scope (comment out the following):
//public scope: any = {
// authorizedGroups: "=authorize"
//};
and I try to get the argument passed in the authorize attribute like this in my directive:
let groups: any = instanceAttributes.authorize;
then the value of this groups variable is not the array itself but the string literal "myCtrl.groupsAuthorized" which is not valid because I want an array of groups, not the name of the array.
Why the isolated scope is causing the digest error? How to resolve the issue and get the array of groups in my directive?
UPDATE WORKAROUND:
I have a workaround in which basically I pass to the directive a string instead an array.
I would have the controller return a string with the groups comma separated:
public get groupsAuthorized(): string {
let groups: Array<string> = [
"accounting",
"administrators"
];
return groups.join(",");
}
and then I would use the directive in a similar way only this time passing that "magic" string rendered first in the view.
<button type="button" class="btn btn-default" authorize="{{myCtrl.groupsAuthorized}}">TEST</button>
and finally in my directive I would convert that comma separated string into an array like this:
public link(
scope: ng.IScope,
instanceElement: ng.IAugmentedJQuery,
instanceAttributes: ng.IAttributes): void {
let groupsCommaSeparated: string = (<any>instanceAttributes).authorize;
let groups: Array<string> = groupsCommaSeparated.split(",");
let element = angular.element(instanceElement);
let authorized: boolean = this.authService.isUserAuthorized(groups);
element.attr("ng-show", String(authorized));
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}
but this is not an elegant solution and it could cause problems if the format of the comma separated groups is not correct or if it has spaces after each comma! and I still would like to have my directive argument passing directly an array.

Related

How to dynamically bind a custom AngularJS modal?

For an educational side-project I am working on, I want to avoid using AngularJS Material Design, UI Bootstrap, or any custom libraries that provide modal functionality.
However, I've hit a snag. I've created a service that is supposed to manage and dynamically create modals. It provides an open function that accepts a spec object, which it then reproduces in the DOM.
What this code actually does:
1. The modal is correctly appended to the DOM.
2. The modal controller's $onInit function fires.
What this code does not do:
1. Bind the $ctrl.message property in the markup to the instance of the controller that we know starts.
Normally, I would ask my question after providing code, however there's a good bit of code required to reproduce this problem (it's below, sans some AngularJS boilerplate.) As that's the case, though, here's my question:
In what way can I get the modals being spun off by this service to properly bind their contents to their given controller?
What I've tried:
As you can see in ModalService.bindModalDiv, I've tried a few avenues of thought, mostly using $compile. Yet, $compile and the resulting link function don't actually seem to be binding the new DOM elements to Angular.
I've tried using $controller to explicitly bind the new scope being generated to the someModalCtrl being instantiated, but that doesn't seem to help at all.
Because I can hit breakpoints on the someModalCtrl, and see the console.log message I used as a sanity check, I think I'm misunderstanding how exactly I'm supposed to bind the new DOM elements to Angular. I'm sure I'm missing something basic that I've managed to forget about or disregard, somehow.
One more note:
I'm sure my problems with getting the modal to bind to AngularJS properly aren't the only problems present. Please remember, I'm doing this partially as a learning excersize; if y'all can help me figure out my modal problem, I'll keep on doing my due diligence and hunting down the flaws I've doubtless built into this approach. Therefore, if you see something that's not a modal problem, it's OK to draw my attention to it, but I won't rewrite the question to fix whatever you find - unless it's absolutely essential that I do. As an example - I know that ModalService.open has some issues in how I'm implementing the promise setup. A $rootScope.$watch is probably more reasonable.
modalSvc.ts:
export interface IModalSpecObject {
parent?: string | Element | JQuery;
templateURL: string
controller: string;
controllerAs?: string;
data: object;
}
export class ModalInstance {
public isOpen: boolean = true;
public returnData: object = null;
public element: JQLite = null;
public $parent: JQuery = null;
public constructor(
public specObject: IModalSpecObject
) {
}
public close(returnData: object): void {
if (this.element)
this.element.remove();
this.isOpen = false;
this.returnData = returnData;
}
}
export class ModalService {
public pollRate: number = 250;
public instance: ModalInstance = null;
public static $inject: string[] = [
'$q', '$rootScope', '$compile', '$controller'
];
public constructor(
public $q: ng.IQService,
public $rootScope: ng.IRootScopeService,
public $compile: ng.ICompileService,
public $controller: ng.IControllerService
) {
}
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
const modalDiv: JQLite = this.buildModal(specObject);
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
setInterval(() => {
if (!this.instance.isOpen) {
resolve(this.instance.returnData);
}
}, this.pollRate);
});
return result;
}
private buildModal(specObject: IModalSpecObject): JQLite {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// Inject HTML template...
$modalPanel.load(specObject.templateUrl);
// Set up the angular controller...
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
$modalPanel.attr('ng-controller', `${specObject.controller} as ${controllerAs}`);
modalDiv.append($modalPanel);
this.instance.element = modalDiv;
return modalDiv;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
// !!!! !!!! I suspect this is where my problems lie. !!!! !!!!
private bindModalDiv(modalDiv: JQLite, $parent: JQuery): void {
const newScope: ng.IScope = this.$rootScope.$new(true);
// Try #1: Bind generated element to parent...
//$parent.append(this.$compile(modalDiv)(newScope));
// Try #1a: Generate bindings, then append to parent...
//const element: JQLite = this.$compile(modalDiv)(newScope);
//$parent.append(element);
// Try #2: Bind element to parent, then generate ng bindings...
//$parent.append(modalDiv);
//this.$compile(modalDiv)(newScope);
// Try #3: Well, what if we bind a controller to the scope?
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
this.$controller(`${specObject.controller} as ${controllerAs}`, {
'$scope': newScope
});
const element = this.$compile(modalDiv)(newScope);
$parent.append(element);
}
}
angular
.module('app')
.service('modalSvc', ModalService);
SomeController.ts:
SomeController.ts pretty much just controls a button to trigger the modal's appearance; I've not included the markup for that reason.
export class SomeController {
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
}
public openModal(): void {
const newModal: IModalSpecObject = {
parent: 'body',
templateUrl: '/someModal.html',
controller: 'someModalCtrl',
data: {
'message': 'You should see this.'
}
};
this.modalSvc.open(newModal)
.then(() => {
console.log('You did it!');
});
}
}
angular.module('app').controller('someCtrl', SomeController);
someModal.html:
<div class="modal-header">
Important Message
</div>
<!-- This should read, "You should see this." -->
<div class="modal-body">
{{ $ctrl.message }}
</div>
<!-- You should click this, and hit a breakpoint and/or close the modal. -->
<div class="modal-footer">
<button ng-click="$ctrl.close()">Close</button>
</div>
someModal.ts:
export class SomeModalController {
public message: string = '';
public static $inject: string[] = [ 'modalSvc' ];
public constructor(
public modalSvc: ModalService
) {
}
public $onInit(): void {
console.log('$onInit was triggered!');
this.message = this.modalSvc.instance.specObject.data['message'];
}
public close(): void {
this.modalSvc.instance.close(null);
}
}
angular
.module('app')
.controller('someModalCtrl', SomeModalController);
I figured out where I went wrong - I needed to use $().load()'s callback. JQuery load is asynchronous, which meant that $compile was working correctly; however, the HTML in my modal partial wasn't loaded by the time $compile had done its job, thus the unbound HTML.
A slight modification of my ModalService got around this, though.
Revised fragment of ModalSvc.ts:
// This is just a convenience alias for void functions. Included for completeness.
export type VoidFunction = () => void;
// ...
public open(specObject: IModalSpecObject): ng.IPromise<{}> {
if (this.instance && this.instance.isOpen)
this.instance.close(null);
this.instance = new ModalInstance(specObject);
const $parent: JQuery = this.setParent(specObject);
// open already returned a promise before, we just needed to return
// the promise from build modal, which in turn sets up the true
// promise to resolve.
return this.buildModal(specObject)
.then((modalDiv: JQLite) => {
this.bindModalDiv(modalDiv, $parent);
const result: ng.IPromise<{}> = this.$q((resolve) => {
// Also, side-note: to avoid resource leaks, always make sure
// with these sorts of ephemeral watches to capture and release
// them. Resource leaks are _no fun_!
const unregister: VoidFunction = this.$rootScope.$watch(() => {
this.instance.isOpen
}, () => {
if (! this.instance.isOpen) {
resolve(this.instance.returnData);
unregister();
}
});
});
return result;
});
}
private buildModal(specObject: IModalSpecObject): ng.IPromise<{}> {
const modalDiv: JQLite = angular.element('<div/>');
modalDiv.addClass('modal');
this.instance.element = modalDiv;
const $modalPanel: JQuery = $('<div/>');
$modalPanel.addClass('modal-panel');
// By wrapping $modalPanel.load in a $q promise, we can
// ensure that the modal is fully-built before we $compile it.
const result: ng.IPromise<{}> = this.$q((resolve, reject) => {
$modalPanel.load(specObject.templateUrl, () => {
modalDiv.append($modalPanel);
resolve(modalDiv);
});
});
return result;
}
private setParent(specObject: IModalSpecObject): JQuery {
let $parent: JQuery;
if(!specObject.parent)
$parent = $(document);
else if (typeof specObject.parent === "string"
|| specObject.parent instanceof Element)
$parent = $(specObject.parent);
else if (specObject.parent instanceof jQuery)
$parent = specObject.parent;
else
$parent = $(document);
this.instance.$parent = $parent;
return $parent;
}
private bindModalDiv(modalDiv: JQLite, parent: JQLite): void {
// parent should be a JQLite so I can use the injector() on it.
parent.injector().invoke(['$rootScope', '$compile', ($rootScope, $compile) => {
const newScope: ng.IScope = $rootScope.$new(true);
this.$controller(this.getControllerAsString(), {
'$scope': newScope
});
const element: JQLite = $compile(modalDiv)(newScope);
parent.append(element);
}]);
}
private getControllerAsString(): string {
const specObject: IModalSpecObject = this.instance.specObject;
const controllerAs: string = specObject.controllerAs
? specObject.controllerAs
: '$ctrl';
return `${specObject.controller} as ${controllerAs}`;
}
I figured this out by going back and doing the step-by-step engineering. I first ensured that $compile was working by creating an element whose contents were {{ 2 + 2 }}, compiling it, then injecting it. When I saw 4 added to my page, I knew that the compile-then-inject aspect of the program worked just fine.
From there, I started building up the modal's construction, and found it working flawlessly...up until I got to jQuery load. When I read the documentation, I saw the error of my ways.
TL;DR: Read the friendly manual!

Why must parameter to uib-typeahead method be a key-value pair?

Why do I need to place $viewValue in a key-value pair in the example below (which uses Angular 1.6.2 and Typescript)?
export class PageHeaderComponent implements ng.IComponentOptions {
public template: string = `
// ... other markup omitted
<input type="text"
name="search"
class="form-control text-field"
placeholder="{{$ctrl.searchPlaceholder}}"
ng-model="$ctrl.search"
uib-typeahead="item for item in $ctrl.getItems({value: $viewValue})"
/>
...
public bindings: any = {
headerTitle: "<",
showBackButton: "<",
entityName: "<",
addAction: "&",
adding: "<",
search: "=",
searchPlaceholder: "<",
getItems: "&"
};
}
This PageHeader component is contained within a LandingPage component. getItems is a method on the LandingPage controller.
export class LandingPageComponent implements ng.IComponentOptions {
public template: string = `
<div class="landing-page">
<page-header
header-title="'Businesses'"
add-action="$ctrl.showAddBusiness()"
entity-name="'Business'"
search-placeholder="'Search for a business'"
search="$ctrl.search.name"
adding="$ctrl.adding",
get-items="$ctrl.getItems({value})"
></page-header>
// ... other markup omitted
`;
public controller: any = LandingPageComponentController;
}
export class LandingPageComponentController {
// ... Irrelevant details omitted
public getItems(value: {value}): Promise<string[]> {
return new Promise((resolve, reject) => {
resolve(["one", "two", "three", "four"]);
});
}
}
If I simply pass $viewValue by itself (and change the signature of getItems to getItems(value: string), I get this error:
TypeError: Cannot use 'in' operator to search for '$ctrl' in o.
Obviously, was able to fix the error, I just would like to understand why my fix worked.
This isn't to do with uib-typeahead, but with the use of the Output Event binding ('&') between LandingPageComponent and PageHeaderComponent.
You can actually simplify your code a little bit:
In LandingPageComponent template:
get-items="$ctrl.getItems(value)"
In LandingPageComponentController:
public getItems(value: string): Promise<string[]> {
return new Promise((resolve, reject) => {
resolve(["one", "two", "three", "four"]);
});
}
Why?
When passing an expression to a child component with the '&' binding, the expression doesnt get evaluated until the child component calls it.
Although the expression gets called by the child component, it gets evaluated on the parent scope.
This expression has no idea about whats going on in the child component - so it has no idea of the variables/values that are present.
How does AngularJS get around this? Think of it as AngularJS using the object you pass into the binding (when you call it) to create 'local variables' to evaluate the expression with.
So in PageHeaderComponent, when you go:
$ctrl.getItems({value: $viewValue})
Think of AngularJS doing this for LandingPageComponent:
let value = $viewValue;
$ctrl.getItems(value);
This isn't actually the case, but I think it should help you in understanding why we need to pass an object back (rather than the values directly)
This post does a really good job of describing why we need to deal with Output Events in this way:
http://www.codelord.net/2016/05/13/understanding-angulars-and-binding/

Multiple directives in same element operating on visibility

I have been struggling with my approach for the following scenario.
I have a custom directive authorize where I pass the name of a group. If the current user has this group in his profile then the element will be visible, if not the element will be hidden.
Example:
<button class="btn btn-default" role="button"
ng-click="myVm.edit()"
authorize="{{myVm.groupName}}"><!--groupName = "accountants"-->
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
and my original directive in typescript authorize.ts using the link function (because I operate on the DOM)
namespace app.blocks.directives {
"use strict";
class AuthorizeDirective implements ng.IDirective {
public restrict: string = "A";
public replace: boolean = true;
constructor(private $compile: ng.ICompileService, private authService: services.IAuthService) {
}
public static factory(): ng.IDirectiveFactory {
const directive = ($compile: ng.ICompileService, authService: services.IAuthService) =>
new AuthorizeDirective($compile, authService);
directive.$inject = [
"$compile",
"app.services.AuthService"
];
return directive;
}
public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
let groupName: string = (<any>instanceAttributes).authorize;
let element = angular.element(instanceElement);
let hasGroup: boolean = this.authService.hasGroup(groupName);
element.attr("ng-show", String(hasGroup));
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}
}
}
angular
.module("app.blocks.directives")
.directive("authorize", AuthorizeDirective.factory());
}
This is working fine, the button is hidden if the authService returns false because the user does not belong to that group (i.e: "accountants").
The problem appears when my DOM element has ng-show or ng-hide directives also. Example:
<button class="btn btn-default" role="button"
ng-hide="myVm.isDeleted"
ng-click="myVm.edit()"
authorize="{{myVm.groupName}}">
<!--groupName = "accountants"-->
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
When myVm.isDeleted = true it seems that overrides the result of my directive and the DOM element is displayed (when it shouldn't because the user does not belong to the specified group as per my authorize directive).
I realize there is some priority (by default 0) in directives, when two directives have the same priority they are executed in alphabetical order according to the documentation. This post was very helpful to understand that.
So I have some options here:
Have my authorize directive evaluate the conditional in ng-hide or ng-show in order to compute (i.e: if the ng-hide says that the element should be shown but the user has not the specific group, then the element should be hidden). I could not find a way to access myVm.isDeleted within my directive link's function. If anyone know how I'd be happy with this approach.
Have my authorize directive executed BEFORE any other directive and rely on angular to later on determine visibility according to ng-show or ng-hide (i.e: if my authorize directive determines that the element should be hidden because the user does not belong to the given group, then it should transform the DOM element and make it ng-show="false" for example, so that angular hides the element later on. This approach does not seem to work, the DOM seems correct, I can see that the button has ng-show="false" but for some reason I still see the button on screen so it's as if Angular didn't know that it has to hide that element. The funny thing is that if I move to another tab, and I go back to the same tab (the view is reloaded and the directive re-executed) then it works fine. What's going on?.
I went with the option 2 and this is the code that seems to work properly manipulating the DOM, but Angular does not apply the ng-show directive afterwards therefor the result is not as expected.
public priority: number = 999; //High priority so it is executed BEFORE ng-show directive
public link(scope: ng.IScope, instanceElement: ng.IAugmentedJQuery, instanceAttributes: ng.IAttributes): void {
let groupName: string = (<any>instanceAttributes).authorize;
let element = angular.element(instanceElement);
let ngShow: string = (<any>instanceAttributes).ngShow;
let ngHide: string = (<any>instanceAttributes).ngHide;
let hasGroup: boolean = this.authService.hasGroup(groupName);
let ngHideValue = ngHide ? "!" + ngHide : "";
let ngShowValue = ngShow ? ngShow : "";
//if hasGroup, use whatever ng-show or ng-hide value the element had (ng-show = !ng-hide).
//if !hasGroup, it does not matter what value the element had, it will be hidden.
if (hasGroup) {
element.attr("ng-show", (ngShowValue + ngHideValue) || "true");
} else {
element.attr("ng-show", "false");
}
element.removeAttr("ng-hide");
//remove the attribute, otherwise it creates an infinite loop.
element.removeAttr("authorize");
this.$compile(element)(scope);
}
I'd argue that seeing as your authorize directive basically just controls whether the element that it's placed displays or not, you should just move its logic out into a service that you inject into your controller, and let ng-hide control whether the element displays like it's designed to.
This will be easier for developers who come later to understand - no one wants to go drilling down into individual directives to find various scattered bits of code that call the server, and your button then just looks like this:
<button class="btn btn-default" role="button"
ng-hide="myVm.isDeleted || !myVm.isAuthorized(myVm.groupName)"
ng-click="myVm.edit()">
<span class="fa fa-edit" aria-hidden="true"></span> Edit
</button>
Nice and simple to read.

cloning an element in angular, and tying up the attached directives

I have built a function that correctly appends a new row to a table when the last row gets the focus.
This works successfully, but the directive is no longer triggering the cloned row
How do I fix the clone so that the elements are added with directives attached.
I need to trigger the directive link after the clone is complete.
each row has a directive attached.
<tr add-table-row-empty>
<td>...
And this is the directive.
module Panda {
#directive('$log', '$compile')
export class AddTableRowEmpty implements ng.IDirective
{
public restrict: string = "A";
constructor(public $log: ng.ILogService, public $compile: ng.ICompileService)
{
}
public link: Function = ($scope: any, element: angular.IAugmentedJQuery, attrs: angular.IAttributes) => {
var inputs = $('input', element);
inputs.on('focus', () => this.addIfLastRow(element));
}
private addIfLastRow(element: angular.IAugmentedJQuery) {
if (!($(element).is(':last-child')))
return;
this.addRow(element);
}
private addRow(element: angular.IAugmentedJQuery)
{
// this should do a deep clone including all events etc.
var clone = element.clone(true, true);
$("input", clone).each((i, _) => $(_).val(""));
element.after(clone);
clone
.hide()
.fadeIn(1000);
}
}
panda.directive("addTableRowEmpty", <any>AddTableRowEmpty);
}

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

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/

Resources