AngularJS: how to avoid the nonassign error when providing default values to missing attributes? - angularjs

This is a question & answer case, because I struggled a bit with the issue, so I wish to keep track of it and its solution.
Hoping it can help somebody, and perhaps other people can provide alternative solutions.
My case: I had to work on a large AngularJS 1.4.6 project (I will name it prj), written in TypeScript, defining a number of directives (prj-ddd) with various attributes (prj-aaa), some of them being boolean.
They implemented these boolean attributes with string binding: prjEnabled: "#" or prjRequired: "#".
So they tested the values like so: if ($scope.prjEnabled === "false"), or in templates: <div ng-show="prjRequired === 'true'"> (yeah, no controllerAs usage as well...).
These attributes are optional: obviously, here, prjEnabled defaults to "true", and prjRequired defaults to "false", among others, because the tests are false when these values are undefined.
This is verbose, inelegant (IMO), prone to errors (lot of raw strings, implicit default value), and not very efficient (although probably not in a perceptible way).
So I started to replace these bindings with expression / two-way binding: prjEnabled: "=" and prjRequired: "=".
Thus, when AngularJS sees <prj-component prj-enabled="false">, it provides directly a boolean in the directive's scope.
Nice touch: with such literal values, it creates no binding, so no performance hit.
I have to cope with absent attribute, so I added in the link function of the directives something like:
if (scope.prjEnabled === undefined) {
scope.prjEnabled = true;
}
And the usages become: if (!$scope.prjEnabled), or in templates: <div ng-show="prjRequired">.
So far, so good. But we also have explicit bindings: <prj-component prj-enabled="{{someScopeValue}}"> or even <prj-component prj-enabled="{{foo.canEdit && bar.valid}}">.
Since we have two-way bindings, I just replaced them with: <prj-component prj-enabled="someScopeValue"> or <prj-component prj-enabled="foo.canEdit && bar.valid">.
Well, if someScopeValue is undefined, the directive will detect that and provide the default value, which will go up to the parent scope. Annoying, but in most cases, probably harmless. The real value is generally provided later (eg. after a network request).
But sometime, I have the following error in the console:
https://code.angularjs.org/1.4.6/docs/error/$compile/nonassign?p0=foo.canEdit%20%26%26%20bar.valid&p1=prjComponent
I followed the advice given in the corresponding page, replacing the bindings with "=?", but I still have the error.
Ideally, I would replace these bindings with "<?", avoiding the first issue (overwriting the value in the parent's scope) and the second one (no assignment to an expression).
But this needs AngularJS 1.5+, and I cannot upgrade the version in the current project.
So how can I solve this problem?

It was tricky and I had to try various solutions, until I saw the light and remembered that with "=?" binding, there is a way to distinguish an absent attribute from one with an undefined value.
I rewrote the default value assignment as such:
if (!("prjEnabled" in scope)) { scope.prjEnabled = true; }
If you are not familiar with the in operator, it is roughly equivalent to scope.hasOwnProperty("prjEnabled"), but it also checks the prototype hierarchy. I use it mostly because it is nicer / terser than the function call...
I made a Plunkr application (out of an official AngularJS one) to reproduce the error, to see when it happens, and to explore various cases: http://plnkr.co/edit/Z2AIag?p=preview
It shows that with "=" binding and assigning default value if undefined, the error appears when the attribute is empty (an unlikely case) or when AngularJS silently captures an exception (here wrong.scopeValue && wrong.unknown where $scope.wrong doesn't exist, causing the ReferenceError exception) and provides undefined as value.
With "=?", we still have the second issue (the empty attribute value case doesn't throw).
If we test with the "x" is scope method, we get rid of this annoying case, quite common when wrong is a variable filled later (network update, etc.).
The only case where the error can still happen is if we assign a value to the bound attribute, and only in the wrong case.

How to manually create one-way binding with default value:
app.directive("myDirective", function() {
return {
scope: {
//oneWay: "<?"
},
link: postLink
};
function postLink (scope, elem, attrs) {
if (!attrs.oneWay) {
scope.oneWay = defaultValue;
} else {
scope.$watch(attrs.oneWay, function(value) {
scope.oneWay = value;
});
};
}
})
For versions of AngularJS (<1.5) that do not have one-way binding, it can be implemented with a watcher.

Related

Format model value as date angularjs

I'm improving and automating certain things in an old web app. One of them is the date format. We used to handle these with jquery functions and now we are doing it with angularjs.
For the input we use a directive and it works perfect. The problem occurs when it is not used, the directive is not executed and the value of the model is left without the proper value.
Directive:
app.directive('formatDate', function($filter) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
// format text (model to view)
modelCtrl.$formatters.push(function(value) {
if(value !== "" && typeof value !== "undefined" && value !== null){
return value.split("-").reverse().join("/");
}
});
// format text (view to model)
modelCtrl.$parsers.push(function(value) {
if(value !== "" && typeof value !== "undefined" && value !== null){
var date = new Date(value.split("/").reverse().join("-"));
date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
return date;
}
});
}
};
});
Issue:
When you load the value from webservice, for example: "invoice.date" comes from the database with the format "yyyy-mm-dd". It is loaded in the input with format "dd/mm/yyyy" and if the input is edited the model value is a "Date object" thanks to the directive. But if no field is edited, the value remains "yyyy-mm-dd" and that string causes errors. (Note: they use webservices with jpa and I can not change anything in backend)
How to format the value before sending it without doing a manual verification and analyzing the value in each function? Can I use $watch over each of the variables without causing a conflict with the directive or an infinite loop? Angular has some other way to automate this?
Thanks for any help.
I have seen this many times, it is quite common and you are using the time because jpa or the database changes the date because Timezone, right?
ok, then it comes from a webserice so it probably comes in json format. You can simply convert to a date object before assigning it to the model value and thus always use this format. Another way is to convert it before sending it and you've already said that's what you want to avoid.
There are many things that can be done. To sum up:
(choose one)
Convert the format upon receipt and before assigning it. either
manual, by simple javascript or any other method.
On the reverse, before sending, make the format change.
To avoid doing it manually, look for it with functions or events, if
$ watch can be used to detect in which format it comes and change it
correctly. So if it is assigned programmatically, it will also work.
There are other methods to filter and elaborate the response but I
think it would be too much for this case. As Jorge said, there are some plugins, tools and more that can be added. But I always try to avoid overloading with so many things.
The problem is to try to automate with angularjs, because there are many ways and finding the "right" way is difficult since each project is different and each person has a different way of thinking.

angular object transferred via $http differs from beforehand output unless timeout is used

I created a nested directive structure with 2-way-object-databinding.
Everything worked like a charm on js side, until I connected it to my services which send the data to my api.
Because in the post request the object has old values, it always ignores the latest change. But the real problem is... outputting the object right before I pass it to the request it has the correct updated values.
service.updatePerson = function(person) {
console.log(person); //this outputs the correct up to date data
$http.post(Routing.generate('update_person'), person); // this works not
//After trying some more (see update below) - this works as well
$timeout(function() {
$http.post(Routing.generate('update_person'), person);
}, 1000);
};
(I am using the fos routing bundle for symfony)
Inspecting the request with my developer tools I can see that the submitted object has old values. But its not like the values never change, its always the last but not the current change.
I'm quite new to angular and might overlook something special about the $http service - I did not enable cache, so that should not be the problem.
Also the problem occurs only with the updated objects, not if I am sending a complete new entity.
The only difference I can see and I am suspecting to cause the issue is the depth of the nesting.
I have a directive called 'collapsedTable' which takes different data. Not just the data also the update, add and delete method are passed to the directive.
This is the usage of the directive:
<collapsed-table ng-if="formModel" ng-if="persons" view-data="persons" form-model="formModel" add-object="newPerson" add-item="addPerson(item)" update-item="updatePerson(item)" delete-item="deletePerson(item)"></collapsed-table>
Adding and deleting items happens directly in the directive:
<button href="#" class="btn btn-success pull-right" ng-click="addItem({item : newItem})">
Add
</button>
(delete button looks the same, but calls the delete function)
But updating data does not happen via one button, it is bound directly to the form fields and the form fields are its own directive:
<form-field ng-if="column.type" form-field-bind="item[column.name]" form-field-action="updateItem({item:item})" form-field-action-param="item" form-select-options="column.data" form-template="{{column.type}}"></form-field>
(I know, generic shit going on here)
And within the directive (editableTextarea for an example):
{{formFieldBind || 'eintragen'}}
(using the x-editable module for angular here)
I think posting the whole content of my directives is going too far. But I think the scope settings are relevant to unterstand how functions and variables are passed.
collapsedTableDirective.js
.directive('collapsedTable',['$filter',function($filter) {
return {
restrict: 'E',
templateUrl: 'templates/collapsedTableView.html',
scope: {
data: "=viewData",
newItem: '=addObject',
formModel: '=',
deleteItem: '&',
updateItem: '&',
addItem: '&'
}
}
}]);
formFieldDirective.js
.directive('formField',['formTemplateFactory', '$filter',function(formTemplateFactory, $filter) {
return {
restrict: 'E',
scope: {
formFieldBind: '=',
formFieldActionParam: '=',
formFieldAction: '&',
formTemplate: '#',
formSelectOptions: '='
},
transclude: true,
template: '<div ng-include="getTemplate()"></div>'
}
}]);
I'm also showing that form field templates are pulled via ng-include, which creates a new scope and which is the reason why the bound var is referenced with "parent" inside the form field template.
But with all questions that can be possibly discussed here, do not forget:
console.log outputs the correct data within the service. Only inside the request we have old data. Thats also the reason ist kinda difficult to debug... from the debugger side my objects always look fine.
Any help appreciated!
UPDATE - with $timeout it works
After trying some stuff out, I've got the idea to try a timeout (from a completely unrelated post). And yeah, a timeout does the trick.
But I am not answering the question because I really do not have an explanation for this behaviour. The console.log does not need a tiemout, though it should be executed before the post request.
Also there is probably a more apropriate solution than using a timeout.
person is not updated at the time when service.updatePerson runs, and it looks like it is triggered by non-Angular code which isn't adapted to digest cycles. In this case this
$scope.$apply();
// or $rootScope.$apply() if this is a service and not a controller
$http.post(Routing.generate('update_person'), person);
may help, though doing $apply in caller function and not callee is always a better habit, so external JS should trigger a wrapper instead:
scope.externalAction = function (obj) {
scope.$apply(function () {
scope.formFieldAction(obj);
};
};
console.log doesn't output the data that was actual at that moment because it was supplied with object reference, and it may reflect the changes that took place after console.log was called. This is a useful feature if taken into account, otherwise it's harmful.
To log the current object state always use
console.log(JSON.stringify(person));

What is the best-practice alternative for $scope.$watch?

I'm looking for a best-practice solution to avoid using $scope.$watch, given the following use-case:
1) directiveA has the following isloated scope:
{ sharedModel : '=' }
2) for its own usage, directiveA needs to change its inner state variable named modelA, based on sharedModel.
3) directiveB uses directiveA, and binds sharedModel to modelB (its internal model).
<directive-a
shared-model="vm.modelB" />
4) Whenever modelB/sharedModel changes, I would like modelA to be updated (remember, modelA data is only derived fromsharedModel).
In order to accomplish #4, I could add $scope.$watch on sharedModel. However, $watch is performance-expensive and not testable. Any best-practice reccomendations?
EDIT:
for live code example see jsfiddle.
notice $scope.$watch on line #10 which I wish to replace.
In this case it's possible to do in without additional pricy watch. You can make use of old-good ES5 property getters, it's going to be very efficient. So instead of $watch try something like this:
Object.defineProperty($scope, 'modelA', {
get() {
return $scope.sharedModel * 10;
}
});
Demo: http://jsfiddle.net/mggc611e/2/

New to Angular - Computed Variables

I am moving to Angular from Knockout, and I have a few issues. I'm assuming that I must be doing something a non-angular type of way.
http://jsfiddle.net/LostInDaJungle/BxELP/
I linked to jsfiddle so I didn't have to include my code here
Stack Overflow will not let me post my question without a code block.
Here is a very basic fiddle that outlines two of my major problems...
Problem 1: val1 and val2 are initialized as 3 and 4, and add up to 7 properly. However, if you change either of the values in the text boxes, the new value is treated as a string and I get concatenation instead of addition. Change val1 to 4 and you get 44 when it should be 8. What is the best way around this behaviour?
Problem 2: Calculated fields. I can get a calculated field by using the curly brackets like {{val1 + val2}} and have the calculated fields auto update when the underlying model changes, but this is totally unacceptable. In my full fledged app, we generate a "cost" that is used several times throughout and having to put in the cost calculation each and every time is a pain. Not to mention that when this calculation changes, I now have the unenviable task of finding 15 different places that use the cost calculation and updating them all.
In addition, if I try to put an ng-model="cost" on the input with the curly brackets, then the curly brackets don't work. So nothing jumps out at me as a way to bind cost.
http://jsfiddle.net/LostInDaJungle/QNVwe/
This example is more like the structure I desire. However, unlike a ko.observable, the calculated fields do not update when the values that generate them change. The boilerplate solution everyone has foisted on me is to write a bunch of ng-change handlers... But that is awful. If width changes change the cost and change the payback calculations, etc... It quickly becomes a tangled mess.
Both of these methods fail as far as separating logic from presentation. Method one has my business logic embedded in my HTML. Method two puts a whole bunch of ng-change handlers in my code which isn't that much different from having to write a whole mess of onChange handlers in plain ol' HTML. If I HAVE to do a bunch of ng-change handlers, I would just as soon do an onChange handler in Javascript because I can at least declare them outside of my presentation layer.
Here's a knockout version of the same:
http://jsfiddle.net/LostInDaJungle/eka4S/2/
This is more like what I would expect... Nothing but data-binds on my inputs, all program logic nicely contained within the view model. Also, since my computable is a Javascript function, I don't have to scratch my head about how to ensure my two values are numeric.
So....
Computed variables: Is there a way to watch the underlying variables and update the computed amount automatically? Without having to bury my program logic in my HTML?
Is there a good way to keep my numbers from turning into strings?
Thank you for your help.
FYI, also posted to Google Groups: https://groups.google.com/forum/#!topic/angular/0dfnDTaj8tw
For a calculated field, add a method to your controller . . .
$scope.cost = function() { return $scope.val1 + $scope.val2 };
and then bind to it directly. It will know when it needs to recalculate as its constituent values change.
<div>{{cost()}}</div>
Ok,
A few hours later and I think I have my answer.
Using $scope.$watch.
$scope.$watch('(height * width) * 40', function(v) {$scope.cost = v;});
or
$scope.$watch('height + width', function() {$scope.cost = (Number(height) * Number(width)) * 40;});
This auto-updates any computables for watched variables. And it gives me a way to work with these without having to live inside curly brackets.
Also, the computed values can be reused and tracked for cascading updates:
$scope.$watch('height * width', function(v) {$scope.dim = v;});
$scope.$watch('dim * 40', function(v) {$scope.cost = v;});
So if height and/or width change, dim is updated, and since dim has changed, cost is updated.
I changed your third input to:
<input type="text" value="{{val1 * 1 + val2}}" />
which causes Angular.js to treat the values as numbers, not strings.
Here is the fiddle. I gleaned the answer from here.
About problem 1:
You should use input type="number" if possible. That would take care of parsing numbers properly. Even if you have an older browser angular would take care of formatting them as numbers.
About problem 2:
Your answer is good Jason if you just need to show plain text on the screen. However if you would like to bind an input with a model to an arbitrary expression, you need something else.
I wrote a directive you can use to bind an ng-model to any expression you want. Whenever the expression changes the model is set to the new value.
module.directive('boundModel', function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
scope.$watch(attrs.boundModel, function(newValue, oldValue) {
if(newValue != oldValue) {
ngModel.$setViewValue(newValue);
ngModel.$render();
}
});
}
};
})
You can use it in your templates like this:
<input type="text" ng-model="total" bound-model="value1 + value2">
Or like this:
<input type="text" ng-model="total" bound-model="cost()">
Where cost() is a simple function of the scope like this:
$scope.cost = function() { return $scope.val1 + $scope.val2 };
The good thing is that you keep using a model for your input and you don't have to dinamically update your value attribute, which doesn't work well in angular.
I'm new to AngularJS but I think that $parse could be used:
http://docs.angularjs.org/api/ng/service/$parse
This is interesting if you have the expression as a string. You can use a path of properties and that string can be generated dynamically. This works if you don't know the expression at compile time, a lot like eval() but probably a lot faster and maybe more secure(?).
Here's an example:
function Ctrl($scope,$parse) {
var expression = 'model.val1 + model.val2';//could be dynamically created
$scope.model = {
val1: 0,
val2: 0,
total: function() {
return ($parse(expression))($scope);
}
};
}
u can bind to a function
function CTRL ($scope) {
$scope.val1 = 3;
$scope.val2 = 4;
$scope.sum = function(){
return ($scope.val1 *1 + $scope.val2 *1);
};
}
it will work the same
the binding expression will work but in much more complex cases we need functions
The $watch function that is made available through the $scope variable is best for this job in my opinion.
$scope.$watch(function(scope) { return scope.data.myVar },
function(newValue, oldValue) {
document.getElementById("myElement").innerHTML =
"" + newValue + "";
}
);
The $watch function takes in a:
value function
& a listener function
The above example is taken from this awesome article: http://tutorials.jenkov.com/angularjs/watch-digest-apply.html
After reading through it, I learnt a lot and was able to implement the solution I was looking for.

AngularJS $watch inexplicably changing value of the watched value

I'm trying to implement an animdated version of ng-show and ng-hide; I originally tried to use jQueryUI.toggle('slide', …), but since $watch fires multiple times, my elements were toggling in and then immediately toggling out (sometimes more than once). But I saw in AngularJS's github issues that that is $watch's intended behaviour (dirty checking).
So I thought, okay this little eff-you be damned, I'll explicitly show or hide instead of the simple toggle: I broke it down to check the value of $watch's newValue like so:
scope.$watch(condition, function myShowHideAction(newValue,oldValue) {
if ( newValue !== oldValue ) {
if (newValue) {
elm.show("slide", { direction: direction }, "slow");
} else {
elm.hide("slide", { direction: direction }, "slow");
}
}
}
Fire it up, and what do I see? Toggling!
After putting in a few console logs, I discover somehow my condition's value is being changed during $watch's loop (which iterates like 6 times instead of just once, but that should be neither here nor there). However, the actual scope parameter's value does not change (as it shouldn't) half way thru like newValue's does.
What the heck is going on?
Plunker
The reason this happens is because all your infoboxes share the same scope. If you put a console.log(scope) statement in your mySlide directives linking function, you'll see that it's created several times with the same scope. So you have multiple watches for the same condition in the same scope.
Your code isn't the easiest to follow I'm afraid, but the issue seems to be inside my-map.js on the line 87. Instead of doing $compile(elm.contents())(scope);, you should probably do $compile(elm.contents())(scope.$new()); to create an isolated scope for that info box.

Resources