Directive, Link Function, Array, DataBinding Order of things is ambiguous - angularjs

I have a problem which is very challenging, that i don't even know how to define, so I'm going to give an example and hopefully you will be able to explain what am i doing wrong.
My HTML is:
<tr ng-repeat="object in Objects">
<td>
<div table-checkbox row-index="{{$index}}" bind-model="selectedChecks[$index]" selected-Checks="selectedChecks">
</div>
</td>
</tr>
The selectedChecks array is define the controller like this: $scope.selectedChecks = {}
and the result of the markup is a column of checkboxes
Here is my directive's code:
return {
restrict: 'EA',
scope: {
rowIndex: '#',
selectedChecks: '=',
bindModel: '='
},
template:'<input type="checkbox" ng-model="bindModel" ng-change="checkit(rowIndex)">',
link: function(scope, elem, attrs) {
scope.checkit = function(i){
alert(scope.bindModel + " " + scope.selectedChecks[i]);
};
}
};
The problem is that when the first checkbox is checked the alert will output: "true undefined"
form the second check/uncheck of any checkbox from now and own the output will be: "true false" and "false true"
My expectation was that the two variables will be the same since they should have point to the same value since:
scope.bindModel is sent to the directive from selectedChecks[$index] and scope.selectedChecks[i] should be the same.
Please help me understand what am i doing wrong here.
It seems that checkIt functions runs before Angular does the data-binding between the checkboxes and the array.

It seems that checkIt functions runs before Angular does the
data-binding between the checkboxes and the array.
No, checkIt function is not running before Angular dose the data binding. The reason why you get true undefined at the first time you check the first one is that $scope.selectedChecks = {} is empty. That's mean there is no property like:
$scope.selectedChecks = {
0: false
};
So, the time you check first checkbox, ng-model="bindModel" will update $scope.selectedChecks = {} like so:
$scope.selectedChecks = {
0: true
}
And the same with other checkboxes.

Related

directive's scope inside ng-repeat angularjs

I'm trying to understand directive's scope inside ng-repeat, still wondering if it comes from ng-repeat but it looks like.
Here is my code
directive.js
myApp.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="name" />',
replace: true,
restrict: 'AE'
}
});
mainController.js
angular.controller('mainController',function($scope){
$scope.name = 'name'
});
index.htm
<div ng-repeat="i in [1, 2, 3, 4]">
<dir-sample></dir-sample>
</div>
<dir-sample></dir-sample>
<dir-sample></dir-sample>
When i make a change in one of the last two directives (which are not inside ng-repeat) it works well, changes on one are reflected on the other.
Problem :
1 - if i change an input value of a directive generated by ng-repeat , changes are not reflected anywhere else.
2 - if i change value of input on one of the two last directives , the directives inside ng-repeat change too, but if touch ( change input value ) of any directive , changes will not be reflected on that directive but will keep being reflected on the other directives.
Can someone please explain why the scope has that behavior ?
Thanks.
Binding primitives is tricky, as is explained here: Understanding scopes. It has to with how Javascript works. Your 'name' variable will get shadowed once it is altered within the ng-repeat block. The suggested fix (from the link above):
This issue with primitives can be easily avoided by following the
"best practice" of always have a '.' in your ng-models
They also provide a link to a video explaining exactly this problem: AngularJS MTV Meetup
So a fix looks like this:
app.controller('mainController',function($scope){
$scope.attr= {}
$scope.attr.name = 'name'
});
app.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="attr.name" />',
replace: true,
restrict: 'AE'
}
});

Angular "bind twice"

I'm trying to keep my watches down by using one-time binding (::) in most places.
However, I've run into the situation where I need to wait for one property of an object to arrive from our server.
Is there someway I can make Angular bind twice (first to a placeholder and second to the actual value)?
I tried accomplishing this using bindonce but it did not seem to work (I am guessing this is because bindonce wants to watch an entire object, not a single property).
Another solution would be if I could somehow remove a watch from the templates after the value comes in, if that is possible.
My objects look something like this:
{
name: 'Name',
id: 'Placeholder'
}
And my template:
<div ng-repeat="object in objects">
{{::object.name}}
{{::object.id}}
</div>
Id will change once and only once in the application life time, having a watch forever for a value that will only change once feels wasteful as we'll have many of these objects in the list.
I think this is what you are looking for! Plunkr
I just wrote a bind-twice directive and if I did not completely missed the question it should solve your problem:
directive("bindTwice", function($interpolate) {
return {
restrict: "A",
scope: false,
link: function(scope, iElement, iAttrs) {
var changeCount = 0;
var cancelFn = scope.$watch(iAttrs.bindTwice, function(value) {
iElement.text(value === undefined ? '' : value);
changeCount++;
if (changeCount === 3) {
cancelFn();
}
});
}
}
});
What I do is, I add a watcher on the scope element we need to watch and update the content just like ng-bind does. But when changeCount hit the limit I simply cancel $watch effectively cleaning it from watchlist.
Usage:
<body ng-controller="c1">
<div ng-repeat="t in test">
<p>{{ ::t.binding }}</p>
<p bind-twice="t.binding"></p>
<p>{{ t.binding }}</p>
</div>
</body>
Please see Plunkr for working example.

AngularJS: how to compile form in a directive

I'm trying to create a custom directive that set 'required' and 'disabled' attributes to the element (form input). The directive gets this data from the scope object. But clearing of required field doesn't set my form to invalide state. I think it's about form compilation after changing the attribute. I tried to do that but got an infinite loop :(
How to compile my form correctly?
Here is plunker
You could just use ng-disabled and ng-required, instead of adding the attributes in a directive.
<div>
<label for="username">username2</label>
<input ng-model="data.account.username2"
ng-disabled="paintData['data.account.username2'] == 'RO'"
ng-required="paintData['data.account.username2'] == 'R'" />
</div>
Alternatively, you could define a directive template that uses ng-disabled and ng-required, and replace the original element with it:
.directive('metaValidate', function($compile) {
return {
restrict: 'A',
replace: true,
scope: {
model: '=',
paint: '='
},
template: '<input ng-model="model" ng-disabled="readonly" ng-required="required"/>',
link: function(scope, element, attrs) {
scope.required = scope.paint[element.attr('model')] === 'R';
scope.readonly = scope.paint[element.attr('model')] === 'RO';
}
};
});
Then, use it like this:
<input model="data.account.username2" meta-validate paint="paintData"/>
I prefer the first approach, because it can respond to changes to paintData. But, you have to repeat the property name several times.
If you want to try this code, here is an update of your Plunker.
Recompiling the element could work, but in order to avoid the infinite loop you need to first remove the meta-validate attribute (which would cause yet more compiling etc):
/* At the end of the directive's link function */
attrs.$set('metaValidate', undefined);
$compile(element)(scope);
See, also, this short demo.

AngularJS - Give checkbox an integer?

I was able to see true or false and return strings in checkboxes with ng-true-value and ng-false-value, but in additional, I want to give the checkbox an integer and if it's checked, it returns an integer.
Which should be similar to when I get a value from a selectbox: http://jsfiddle.net/PuDRm/
But I can't seem to get the same idea in a checkbox: http://jsfiddle.net/PuDRm/1/
<input type="checkbox" ng-model="testc" ng-true-value="Yes" ng-false-value="No" ng-change="processtest()" /> Yes
I'm a total newbie and trying to understand AngularJS.
It sounds like you are trying to bind to two separate values with the checkbox.
In the select the view is the name you gave e.g. 'test1', and the model is the number, e.g. '1'. For the checkbox, the view is a check or a blank square and the model is Yes/No (or whatever you want to put in the ng-true-value and ng-false-value). There's no room to have it produce a number too.
It would be simpler to let the checkbox model be the default (boolean) type by omitting the ng-true-value and ng-false-value attributes. You can then display the number and text based on this like in this updated fiddle: http://jsfiddle.net/D5DGf/1/
The {{ ... }} bindings take care of updating when the function values change, so you don't need to use ng-change either.
What was wrong with the original checkbox fiddle?
In the processtest function you are taking the model (Yes/No) and changing it to always be either 8 or 1. The checkbox (in fact, ngModel) doesn't understand this as it expects to always see Yes/No. As it doesn't know what to do, it falls back to being unchecked.
This is why the checkbox in the original fiddle is uncheckable.
If you still want to set use a number for your model and checbox value, you could do it with a custom directive to parse the string back to integer.
Demo: http://jsfiddle.net/qw5zS/
html
<input type="checkbox" ng-model="test" parse-int ng-true-value="1" ng-false-value="0" />
js
app.directive('parseInt', [function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, controller) {
controller.$formatters.push(function (modelValue) {
console.log('model', modelValue, typeof modelValue);
return '' + modelValue;
});
controller.$parsers.push(function (viewValue) {
console.log('view', viewValue, typeof viewValue);
return parseInt(viewValue,10);
});
}
}
} ])
But this might not be a good practice using checkbox, you should stay with the default behavior and use a boolean, then format the view like you want, but keep your data clean.
If you change the column in database from integer to char, it works with
<input type="checkbox" ng-model="testc" ng-true-value="1" ng-false-value="0" ng-change="processtest()" /> Yes
Here is the solution which worked best in my trials :
<input type="checkbox"
ng-true-value="{{ 1 | json }}" ng-false-value="{{ 0 | json }}"
ng-model="myVariable">
Another approach without the needs of ng-model, ng-true-value and ng-false-value. Let's keep those the way they are.
js
angular.module('whateverModule')
.directive('toggleInt', function () {
function link ($scope, $element, attr) {
$element.on('click', function () {
$scope.$apply(function() {
$scope.toggleModel = +!$scope.toggleModel;
});
});
$scope.$watch('toggleModel', function (value) {
$element.prop('checked', !!value);
});
}
return {
restrict: 'A',
scope: {
toggleModel: '='
},
link: link
};
});
html
<input type="checkbox" toggle-int toggle-model="intModel">
<span>Value: {{intModel}}</span>
What I ended up doing was just using an if statement on the controller.
Example:
$scope.status = ($scope.status === 1 ? true : false);

AngularJS - Isolate scope binding to parent scope

If I have an html element in my "parent" page like so:
<div ng-include"'Go-To-Child-Page.html'" />
Any my child/include page is like so:
<some-directive two-way-binding="$parent.SomeParentScope"></some-directive>
Why is this not working for my directive? Or better yet, how do I make it work?
app.directive ('someDirective', function(){
return {
retrict: 'E',
replace: true,
scope: {
myBinding : "=twoWayBinding", <- this is what is not working
},
template: '<select ng-model="myBinding" ng-options="myType.Description for myType in myTypes"></select>'
};
}
Edit Update:
Why did I post this question?
After completing a very lengthy form, I immediately noticed how I had quite a number of similar controls that the coder in me said I should abstract out. One of those was the select control. Two scenarios were involved with this control:
(1) Where the user had to choose a filter before the select control was populated; and
(2) Where the code pre-defined the filter for the select control.
The solutions for both those scenarios are shown below. I hope this helps everyone because I truly enjoy using Angular and the directive functionality it provides to create "Html-magic" is amazing.
You seem to be doing a lot of things unnecessarily, but that might be because I'm misunderstanding your goal.
I've fixed your plnkr here: http://plnkr.co/edit/FqpzC5p1Ogm4attYiArV?p=preview
The basic changes that were necessary seem to be:
Pass the selected filter (Rear/Front) into your directive
Replace ngOptions with ngRepeat and a filter
There's really no need for your directive to have a controller (and generally most directive's should use a linker function). I stripped out some bits to make it simpler, but you can still wire up $scope.filterTypes as you were (pulling available Types from $scope.myTypes) and it'll still work the same.
Update
Since you didn't spell out all of your requirements, I may be missing some, but this implementation is what I gathered you are looking for:
http://plnkr.co/edit/pHgJ84Z35q5jxCpq3FHC?p=preview
It's got dynamic filtering, it's not unnecessarily using a controller, it's got two-way binding. The only problem is that it's referencing the "Description" field (as your original was). You can work that in to be configurable in HTML if you like.
Scenario 1 : Let the user filter:
Filter:
<input type="radio" ng-model="choice.type" value="Rear"> Rear
<input type="radio" ng-model="choice.type" value="Front"> Front
<br>
Select:
<name-value-select selected-item="selected.item" choice="choice.type" options="axleTypes"></name-value-select>
Scenario 2 : pre-filter in code:
<name-value-select preselected-filter="Front" selected-item="selected.item" options="axleTypes"></name-value-select>
The directive for both scenarios:
.directive('nameValueSelect', function () {
return {
replace: true,
restrict: "E",
scope: {
selectedItem: "=",
choice: "=",
options: "=",
preselectedFilter: "#"
},
controller: function ($scope) {
$scope.$watch('choice', function (selectedType) {
$scope.selectedItem = ''; // clear selection
$scope.filterTypes = [];
angular.forEach($scope.options, function (type) {
if ($scope.preselectedFilter !== undefined) {
selectedType = $scope.preselectedFilter;
}
if (type.Type === selectedType) {
this.push(type);
}
}, $scope.filterTypes);
$scope.filterTypes.sort(function (a, b) {
return a.Description.localeCompare(b.Description);
});
});
},
template: '<select ng-model="selectedItem" ng-options="o.Description for o in filterTypes"><option value="" selected="selected">Please Select </option></select>'
};
});
And the proverbial obligatory plunker:
http://plnkr.co/edit/tnXgPKADfr5Okvj8oV2S

Resources