Providing dynamic value to directive without causing reevaluation in AngularJS - javascript

I want to use an AngularJS directive as HTML element, and provide it with two values: One is retrieved by iterating through a collection, and the other one is calculated from that value:
<mydirective myval="val" mymagic="getMagic(val)" ng-repeat="val in values" />
It works as expected when the getMagic(val) function always returns the same value. In my case the result values are Arrays, and as each call results in a new value, I will end up in a
[$rootScope:infdig] 10 $digest() iterations reached. Aborting!
error. You can find a fiddle with my example here (if the function is defined like it is done in the commented line, it works).
Is there any way to not reevaluate or "watch" the mymagic parameter? I do want the value to be recalculated when the values collection changes, but the function should not be called apart from that.
Is there any way to achieve this?

Try this
(UPDATED JSFIDDLE)
http://jsfiddle.net/yogeshgadge/6T8mr/6/
notice the change - your getMagic() got called multiple times as the values returned also triggered the change and cause inifinite....10 digest
app.directive("mydirective", function () {
return {
restrict: "E",
transclude: false,
scope: {
myval: "=",
mymagic: "&" //passing the function
},
template: "<li>{{myval}} -- {{mymagic()}}</li>",
///mymagic() executing this function here
replace: true
};
});

I think you are concerned that getMagic(val) is being called multiple times when the page is being rendered. What's going on is that the $digest cycle is running a few times to get the page completely rendered. This is expected behavior. See more information in the Angular guide to scopes.
What you can do is generate the magic numbers in the controller and attach it to a scope. And then any time the array or magic numbers change, you explicitly call $scope.$apply().
Something like this might work:
app.controller('Controller',
function ($scope) {
var getMagic = function(val){
return val + 1;
};
$scope.values = [3,7,1,2, 100];
$scope.magic = recalculate();
// EDIT: every time $scope.values changes, recalculate $scope.magic:
function recalculate() {
return $scope.values.map(getMagic);
}
$scope.$on('somthing-changed', recalculate();
});
Now, you will still need to ensure that every time either the values or the magic arrays change, you explicitly call $digest(). This is not as elegant as using a $watch() expression, but it will be more performant since you are not reevaluating the magic array more often than you need to.

Related

How to determine if an array of string objects is empty using ngIf?

I have to write a code in which I must transfer some items between two lists and hide an error when the array of object literals to be transferred to isn't empty. I have created two controllers(to manage two separate lists) and one service to deal with common data. A list has been defined inside the service along with some functions to transfer items from one list to another. The array size never seemed to change from 0,which is the logic i am trying to use in ngIf.
My logic was to check if the array is empty, then return a value of true if it was empty and set a variable empty in the controller. Then in ng-if I will check ng-if="b.empty" and thought that that would work but it didnt. The array size would remain 0 all throughout the life cycle of my code. I used ._isEmpty(list),angular([],[]) and the most obvious, array.length but the issue was initially they showed 0, but then the array size never changed. Even after populating the target array, the size seemed to stay 0 with any/all of the above functions/methods.
l1.$inject = ['listService']
function l1(listService){
var buying = this;
buying.items = listService.display();
buying.Add = function ($index){
listService.addItem($index);
}
}; //This is the controller for the source array.
.
.
.
bought.empty = listService.checkIfFull(); //Part of the second controller which assigns empty a boolean value
.
.
.
service.checkIfFull = function (){
if(blist.length == 0){
console.log(_.isEmpty(blist))
console.log(tblist)
return false;
}
else
{
console.log("not going here");
return true;
}
}; //The service checks if the array is empty
<div class="emptyMessage" ng-if="b.empty">Nothing bought</div>
The value of the console.log statements also only seem to be executing in the true portion of the if statement. I found a solution for this, which was to simply check in the html tag itself, if the local list's(that I'm looping through which ng-repeat)length was equal to zero and that worked. But could you please explain why my attempt is wrong? I am a beginner to AngularJs and JS in general so i might have not understood some rules about js and thus written wrong code. Thank you.
Edit: Here's the link to the codepen-> https://codepen.io/meanmanmachineman/pen/RmmdjY
Your problem is caused by the line bought.empty = listService.checkIfFull();. There you are calling to the function listService.checkIfFull() and assigning the returned value to bought.empty. What you should do is to assign the function itself to bought.empty:
bought.empty = listService.checkIfFull;
This way each time bought.empty is evaluated, it returns the current real value.
EDIT:
I'll try to be more explicit about the difference between, bought.empty = listService.checkIfFull() and bought.empty = listService.checkIfFull.
The first way, bought.empty will call to listService.checkIfFull() and store the returned value as a static value and any time the variable is evaluated, the value will be the same.
By using the other method, the value of bought.empty is not a number but the listService.checkIfFull function itself. This way, each time AngularJS evaluates the variable, the function is executed and returns the corresponding value.

Modifying scope object values in checkbox using ngchange

Forgive if this is a newbie error but I think this must be really simple and I am obviously missing something..
I have the following ngrepeat:
<div class="panel-body" data-ng-repeat="participant in activity.Participants" ng-show="showp" ng-init="participant.CheckInTime ='not set'"> <--The 'init' is for debug
{{getparticipantName(participant.ParticipantID)}}
Check in = {{participant.CheckInTime}}
<input type="checkbox" ng-model="participant.CheckedIn"
ng-change="setToNowOrNull(participant.CheckedIn, 'participant.CheckInTime')">
<br />
Check in = {{participant.CheckInTime}}
</div>
Which is nested within another ng-repeat which defines the controller etc. and that works fine. But when I click the checkbox, the settoNowOrNull function gets called, changes the value as it should, but this isn't returned to the participant.CheckInTime .. here is the function:
$scope.setToNowOrNull = function (deciderbool, thingtoset) {
if (deciderbool) //its been set to true.
{
$scope[thingtoset] = Date.now();
}
else //its been cleared to false
{
$scope[thingtoset] == null;
}
}
I added the $scope[thingtoset] after reading another question on here but to no avail.. the same with the single quotes around 'participant.CheckInTime' in the ng-change line. (This is supposed to pass the object not the value?)..
I'm obviously not getting something, and I'd have thought I could have done it in the html angular anyway rather than needing to call the controller for something so trivial - I just need to record the Date.Now() into the participant.CheckInTime. Thoughts anyone?
You can try this:
<input type="checkbox" ng-model="participant.CheckedIn"
ng-change="setToNowOrNull(participant)">
$scope.setToNowOrNull = function (participant) {
if (participant.CheckedIn) //its been set to true.
{
participant.CheckInTime = Date.now();
}
else //its been cleared to false
{
participant.CheckInTime == null;
}
}
There are several problems with your approach, particularly with
$scope[thingtoset] = Date.now();
Using value being passed into function this would be
$scope['participant.CheckInTime'] = Date.now();
This isn't the same as
$scope.participant.CheckInTime;
The whole string represents one object key and would have to be parsed into parts to get 2 levels out of it. Essentially it's invalid syntax for what you had hoped to accomplish
Even if it was valid there is no such object on your scope since participant is an alias for an object in the view. That object is within the array activity.Participants.
In conclusion, pass the actual object into your function and work with the whole object
If you want a generic method for multiple properties it would need to be more like:
$scope.setToNowOrNull = function (object, key){
switch(key){
case 'CheckInTime':
// code for this key
break;
}
}
Then in markup would use:
<input ng-change="setToNowOrNull(participant, 'CheckInTime')">
I suspect that you likely don't need such a generic method and can simply pass a single argument, the participant object, for that specific change handler
Within the ng-repeat, the object has its own scope, therefore when you try to write into $scope[thingtoset], instead of overriding the parent scope's corresponding object, you just create another object with the same name in the lower scope. Instead you can create an object in the upper scope, say $scope.thingHolder, and when this function is called, it can update $scope.thingHolder[thingtoset]. Then you will be able to observe the changes correctly in the upper scope too.

Number filter on input

I am having trouble getting a filter to work on a number/text input.
<input type="number" class="text-center" ng-model="redeem.redemption.Amount | number: 2">
This throws an error: Expression 'redeem.redemption.Amount | number' is non-assignable
If I remove the filter, it works. I tried applying the same filter to the angular input demos and it works fine (https://docs.angularjs.org/api/ng/filter/number). Only difference I can see is I am binding to a an object on the scope and not a value directly. Is there something easy I am missing here?
I guess that what you are trying to do is to display the "formated value" of what they enter?
Then just remove the filter from the ng-model and in your controller watch the redeem.redemption.Amount and format it when the watch gets triggered. you will also need to set a timeout in order to allow the user to type, otherwise every time that the user hits a number the watch will try to format the number and the user won't be able to type anything.
The code that you have posted will never work because ng-model establishes a 2 way data binding with the property of the scope that you indicate, that's why you can not set filters there, it will only accept a property of the scope, that will be updated and from where it will read the value when it changes. The filter is a function with one input and retrieves a different output, think about it, if you set the filter, then Angularjs won't know where it has to set the changes of of the input.
Something like this:
app.controller('MyCtrl', function($scope,$filter,$timeout) {
$scope.testValue = "0.00";
var myTimeout;
$scope.$watch('testValue', function (newVal, oldVal) {
if (myTimeout) $timeout.cancel(myTimeout);
myTimeout = $timeout(function() {
if($filter('number')(oldVal, 2)!=newVal)
$scope.testValue = $filter('number')($scope.testValue, 2);
}, 500);
});
});
http://plnkr.co/edit/WHhWKdynw0nA4rYoy6ma?p=preview
Try tweaking the delay for the timeout, right now its been set to 500ms.
you cant put filter in ng-model directive.you can do it controller using this:
redeem.redemption.Amount=redeem.redemption.Amount.toFixed(2);
or you can use jquery to prevent keypress two digits after a decimal number like this:
$('.decimal').keypress(function (e) {
var character = String.fromCharCode(e.keyCode)
var newValue = this.value + character;
if (isNaN(newValue) || parseFloat(newValue) * 100 % 1 > 0) {
e.preventDefault();
return false;
}
});
<input type="number" id="decimal" class="text-center" ng-model="redeem.redemption.Amount | number:">

Using a watch inside a link is causing an infinite digest cycle.

I'm trying to write a directive that associates a score with a color.
I've made an attempt already, and the Plunker is here. The directive itself is here:
.directive('scorebox', function () {
function link ($scope, $elem, $attr) {
var one = 1;
$scope.$watch('[score,ptsPossible]', function (newValue) {
pctScore = newValue[0] / newValue[1]
if (pctScore <= 0.4) {
rating = 'low';
} else if (pctScore <= 0.6) {
rating = 'med';
} else if (pctScore <= 0.8) {
rating = 'high';
} else if (pctScore == 1) {
rating = 'perfect';
}
$elem.removeClass();
$elem.addClass('scorebox');
$elem.addClass(rating);
$elem.text(newValue[0] + "/" + newValue[1]);
});
};
return {
restrict: 'E',
scope: {
score: "=",
ptsPossible: "="
},
link:link
}
})
I've got a couple of problems.
First, it's pretty obvious to me that I'm not supposed to do a $watch inside a link function. I'm creating an infinite digest cycle, and that's not good. I'm still not sure why, though.
I'm not manipulating the DOM correctly. Even though I'm calling $elem.removeClass(), it's not working--the element retains any classes it had before.
What is the right way to do this?
As #miqid said, no need to $watch both score and ptsPossible, since you only want to react when score changes (at least in this situation you are presenting).
The problem here, is you are using jqLite's removeClass function instead of jQuery's. If jQuery is not included before Angular in the code, Angular will instead use jqLite functions, which is like a smaller, much simpler version of jQuery. It is also, slightly different. jQuery's removeClass(), will remove all classes is no parameter is passed. jqLite will not do the same, it will just remove those classes that you pass as parameter.
You never included jQuery at all, so that's what's happening. Here is the edited Plunker. You can check jQuery is now included in the top, and everything works as expected. And also the $watch is much simpler.
Just a suggestion to get things working:
There's no need to $watch both score and ptsPossible since the latter never changes after the value is loaded from its corresponding attribute value. You also have access to scope variables inside the $watch callback function.
That's unusual as I would've expected your removeClass() to work as well. You could instead try removeAttr('class') here in the meanwhile.
Here's a Plunker with the suggested changes.
You need to use $watchCollectionand not $watch. You are passing in an array to $watch which is how $watchCollection expects.

Variable Dependency with knockoutJS

I'm building an application with KnockoutJS with a component that essentially acts as a sequential spreadsheet. On different lines users may define variables or use them to represent a value.
So for example
x =2
x //2
x = 4
x //4
I have this working in the straightforward case of continuing adding new lines. The output function for each line checks and iterates backwards to see if the variable was ever defined previously. If it was it uses the first example it finds and sets that as the value. This works when initially defining the lines, and also works when you edit a line after a previous line has changed.
However, I would like variables to update if a previous definition of that variable has changed, been removed, or been added. That behavior does not exist right now. I have tried adding my own custom dependency handling code using a map to track the variables, but it badly impacted performance. I would like to tap into Knockouts dependency management to solve this, but I'm not sure of the best way to do so. Here is a brief summary of my code structure, I would be happy to add more detail if needed.
calcFramework is the view-model object I bind to the map. It consists of an observable list of Lines, a varMap, and other unrelated properties and functions
Line is a custom object. The relevant code is below
var Line = function (linenum,currline) {
var self = this;
self.varMap = {};
self.input = ko.observable("");
self.linenum = ko.observable(linenum);
self.lnOutput = ko.computed({
read:function(){
return outputFunction(self,self.input());
},
write:function(){},
owner:self
});
};
function outputFunction(self,input) {
try{
var out = EQParser.parse(input,10,self);
return out.toString();
}
catch(ex){
//error handling
}
}
Line.prototype.getVar = function (varName, notCurrentLine) {
if(typeof varName === "undefined"){
return null;
}
//Actually don't want ones set in the current varMap, only past lines
if(varName in this.varMap && notCurrentLine){
return this.varMap[varName];
}
if (this.linenum() > 0) {
var nextLine = calcFramework.lines()[this.linenum() - 1];
return nextLine.getVar(varName,true);
} else {
//eventually go to global
return calcFramework.varMap[varName];
}
};
Line.prototype.setVar = function(varName,value){
this.varMap[varName] = value;
};
SetVar and getVar are passed to eqParser, which gets the value of the expression, calling those functions as needed if a variable is referenced. So the variable value is not explicitly passed to the function and thus knockout does not view it as a dependency. But I'm not sure how I would pass the variable as a parameter without traversing the list every time.
So my question is, given this setup, what is the best way to track changes to a variable assignment (and/or new assignments) and update the lines that reference that variable, while maintaining good performance.
I recognize my question is lengthy and I have attempted to trim out all unnecessary detail. Thanks for your patience in reading.
I would be tempted to use a publish/subscribe model, using something like Peter Higgins' PubSub jquery plugin
Your overall app would subscribe/listen out for lines publishing an event that they have a variable definition. This would store any variable names in a standard javascript hashtable, along with the value. When a variable found event is published by a line, the app would check through all the known variables, and if it finds that it is a change to an existing variable value, it would publish a variable changed event. All the lines would subscribe to that event. They can then check whether they have a variable matching that name, and update the value accordingly.
Here's some untested code to give you an idea of what I mean:
var app = function()
{
var self = this;
self.variables = {};
$.subscribe('/variableAssigned', function (key, value)
{
// I think that this is the best way of checking that there is a variable
// in the object
if(self.variables.hasOwnProperty(key))
{
if(self.variables[key] !== value)
{
$.publish('/variableChanged', [ key, value ]);
}
}
});
}
In your Line object:
$.subscribe('/variableChanged', function (key, value)
{
// loop through varMap and see if any of them need updating.
});

Categories

Resources