AngularJS Directive - Template Select Not Updating Ng-Model - angularjs

I have two directives in my module. The first is from https://github.com/banafederico/angularjs-country-select. I used it as a model for the second. The two directives represent input fields, but the second one is not storing a value. The link function is not updating the ng-model value.
link: function(scope, elem, attrs) {
if (!!attrs.ngModel) {
var assignDivision = $parse(attrs.ngModel).assign;
elem.bind('change', function(e) {
assignDivision(elem.val());
});
scope.$watch(attrs.ngModel, function(division) {
elem.val(division);
});
}
}
In my ide (netbeans) the 2nd (dependent) drop-down does not display the selected value in the drop-down or update the model. In this fiddle (http://jsfiddle.net/676mp/), the display updates, but the model does not. I am unsure how to update the value of the model to the selected value.

Its because in the second directive you defined scope: {...} when passing in the country value. This creates an isolate scope in the directive and thus ng-model on the element doesn't really work, you need to create a two way binding to the parent variable in the directive's new isolate scope
http://jsfiddle.net/676mp/2/
scope: {
country: '#',
myDivision: '=division'
}
And then in the HTML
<division-select division="myDivision" country="{{myCountry}}"></division-select>
EDIT: Also note that in the fiddle I changed your template for the directive to include its own ng-model for its own scope

Related

Angular directive 1 way binding is working but 2 way binding is not

I am not using scope in my controller as i use controller as etc..
I have proof of concept that plunker without scope is working
1 way binding is working
2 way binding is NOT working - shows literal value
HTML page WORKING
Here: {{detail.program.ldcCode}} SHOWS "Here: PNLC"
<lcd-code code="{{detail.program.ldcCode}}"></lcd-code>
Above passes in 1 way binding of that object/value of PNLC to Directive !
Directive :
return {
replace: true,
restrict: "EA",
scope: {
code: "#"
},
link: function (scope, element, attrs) {
console.log('ldcCode', attrs.code); // PRINTS out PNLC
Thus the above 1 way binding works with passing in {{detail.program.ldcCode}} as an expression , and then in directive the code: "#" along with console.log of console.log('ldcCode', attrs.code); // PRINTS out PNLC
So HERE IS THE PROBLEM, when i switch to my much needed two way data binding
Next is the issue:
Pass from HTML to directive WITHOUT Expression
<lcd-code code="detail.program.ldcCode"></lcd-code>
Directive
scope: {
code : "="
},
link: function (scope, element, attrs) {
console.log('ldcCode', attrs.code);
LITERALLY this prints to chrome dev console as below in bold
ldcCode detail.program.ldcCode
What is going on?
It seems that the attr link function parameter is showing the raw value given to the attribute.
Angular, when using the isolated scope and the two way biding '=' operator, take that value and interpolate it on the parent scope to get the actual value that you can access via the scope parameter on the link function.
Reference to the $compile.directive.Attributes in Angular docs:
A shared object between directive compile / linking functions which
contains normalized DOM element attributes. The values reflect current
binding state {{ }}
If you want to get the interpolate value of the attribute, even not on the isolated scope, you can use the $observe method on it:
function linkingFn(scope, elm, attrs, ctrl) {
// get the attribute value
console.log(attrs.ngModel);
// change the attribute
attrs.$set('ngModel', 'new value');
// observe changes to interpolated attribute
attrs.$observe('ngModel', function(value) {
console.log('ngModel has changed value to ' + value);
});
}

Two way binding from the link function

Can someone tell me why I am not able to two way bind from the link function?
Please refer to this plunk: http://plnkr.co/edit/RI1ztP?p=preview
The below watch successfully adds the collection to attrs.ngModel but I dont see it reflecting in the parent controller
scope.$watchCollection("selectedItems",function(collection){
attrs.ngModel = [];
for(var i=0;i<collection.length;i++){
attrs.ngModel.push(collection[i]);
}
console.log("ngModel",attrs.ngModel);
});
Cant see the collection over here (selectedUsers):
<body ng-controller="mainCtrl">
<div multi-select-search-box ng-model="selectedUsers" label="name" my-options="state in states"></div>
{{selectedUsers}}
If you look at the above html, I am binding the selectedUsers array to ng-model. In my link function, i add the selected users to attrs.ngModel array. When I look at the console, the selectedUsers are added to attrs.ngModel but the array isn't reflected back on the html {{selectedUsers}}
The data bound to the ng-model of your multi-select-search-box is $scope.selectedUsers.
Therefore to register a change in the DOM you have to update that variable rather than ng-model.
scope.$watchCollection("selectedItems",function(collection){
for(var i=0;i<collection.length;i++){
scope.myNgModelVar.push(collection[i]);
}
});
Since ng-model is a string that gets $parse()/$eval() called on it to evaluate it as an expression, updating that ng-model value won't do you any good.
EDIT:
After some clarification it appears that this is a custom directive designed to be reusable. So therefore we do not want to stick variables from your controller inside the directive. Instead, you should bind a directive attribute to your directives scope.
// Directive Def. Object:
return {
restrict: "AE",
scope: {
myNgModelVar: "=",
bindModel: "=ngModel" //This is the alternate method aliasing ngModel var with a scope var.
},
template: "<input ng-model='myNgModelVar' />"
};
Although you could use ngModel by using an alias scope: {bindModel:'=ngModel'}, this gives you an isolated scope variable that you bind to ngModel instead. Therefore keeping your directive reusable.
The solution was to require the ng-model controller and sync changes using the viewValue array:
scope.$watchCollection("selectedItems",function(collection){
ctrl.$viewValue.splice(0,ctrl.$viewValue.length);
for(var i=0;i<collection.length;i++){
ctrl.$viewValue.push(collection[i]);
}
});
and
require: 'ngModel'

Angular directive check if optional binding is set in isolate scope

I'm writing an angular directive to wrap up logic for some custom dropdowns. My directive has 3 dropdowns, any number of which may actually be used.
My directive (stripped down) looks like this:
app.directive('dropdowns',
['$http', '$filter', ...
function($http, $filter, ...) {
return {
restrict: 'E',
templateUrl: '/Some_template',
scope: {
customer: '=?',
warehouse: '=?',
location: '=?',
link: function (scope, elm, attrs) {
//How do I tell if scope.customer is set to a binding?
}
}
}]);
How do I check whether the dropdown bindings are actually bound to some other variable? To be clear, I can't check whether the variable is truthy because undefined values are fine. For example, if my HTML looks like this:
<dropdowns customer="customer" warehouse="warehouse"></dropdowns>
how can I tell that customer and warehouse are set, but location isn't? Ultimately I'm using that information to show/hide the relevant dropdowns. I'd rather just check these bindings instead of just adding another few bindings to my isolate scope.
You can use the attrs parameter for that. The attrs parameter will show you the raw values in all of the attributes of your element (with the exception that double curlies will have their values resolved first).
//create the dropdowns if the attribute was present
if(attrs.customer){ /* create the dropdown */}
if(attrs.warehouse){ /* create the dropdown */}
if(attrs.location){ /* create the dropdown */}
Here's a jsfiddle showing the differences.
https://jsfiddle.net/L2j4ecd8/

Value of input field is being removed by directive

I have a simple Angular Problem - I think it's probably a case of can't see the wood for the trees here.
I have an input field with a directive attached. The purpose is eventually to compare new with old data and show a popup. However, as soon as I add the directive attribute to the input field, the value disappears:
Plunk here: http://plnkr.co/edit/BQvKGe6kjuD0ThPBYJ4d?p=preview
HTML:
First Name:
<input type='text' ng-model='currentEditItem.strFirstName' name='strFirstName' id='strFirstName'
cm-popover="currentEditItem.personOldData.strFirstName"/>
<br><br>
ngModel: {{currentEditItem.strFirstName}} <br>
cmPopover: {{currentEditItem.personOldData.strFirstName}}
JS
var app = angular.module('app', []);
app.controller('Ctrl', function ($scope) {
$scope.currentEditItem = {};
$scope.currentEditItem.strFirstName = "Bob";
$scope.currentEditItem.personOldData = {};
$scope.currentEditItem.personOldData.strFirstName = "Roger";
});
app.directive("cmPopover", function () {
return {
scope: {
ngModel: "=",
cmPopover: "="
},
link: function (scope, elem, attrs) {
console.log("ngModel", scope.ngModel);
console.log("cmPopover", scope.cmPopover);
}
}
});
If you go to the Plunk and remove the cm-popover attribute, the input field is filled with the value from the model. When the attribute is added the value disappears although the model is still in the scope with the correct value.
In your directive you declare an isolate scope. This input's scope is now this isolate scope since it's the directive element. It's looking for the currentEditItem object which doesn't exist in the isolate scope
ngModel does not create a new isolated scope for itself so it can $watch without having to hardcode a $parent in it's internal code.
But then you add another directive on the same DOM node that creates an isolated scope for itself. Couple this with the fact that you can only have a single isolated scope on a DOM node and you basically force ngModel to use/work with the same scope cmPopover created.
So when writing ng-model="currentEditItem.strFirstName" you are actually addressing the $scope inside the cmPopover directive, no the one in the (parent) controller. You can check this is the case by using ng-model="$parent.currentEditItem.strFirstName" - and it will work.
There's quite a lengthy conversation here with a lot of possible workarounds and solutions that leads to an actual fix in release 1.2.0.
So long story short: update to at least AngularJS 1.2.0 and this will work.

AngularJS - directive components dropdown not getting initialized for edit

I am new to Angular JS.
What I am trying to do is create a reusable component using Directive. As of now it just has one drop-down. While doing Add the drop-down is getting populated with the values in permissionValues array and binding with the empty model with the selected value is happening. But when I try to Edit the drop-down is not getting initialized with existing model value value.
The directive code.
directive('userPermissions', function() {
return {
restrict: 'E',
template:'<div><select ng-model="ngModel" ng-options="abc [optValue] as abc [optDescription] for abc in array"></select>{{tab}}</div>',
replace: true,
transclude: true,
scope:{ ngModel: '=', tab:'='},
link: function (scope, element, attrs) {
scope.optValue = attrs.optValue;
scope.optDescription = attrs.optDescription;
scope.$watch(attrs.array, function(newVal, oldVal){
if(newVal != oldVal){
scope.array = newVal;
}
});
}
};
});
The HTML code
<user-permissions tab="tab"
ng-model="newUser.canCheckout1"
array="permissionValues"
opt-value="value"
opt-description="label"></user-permissions>
in main controller
$scope.permissionValues = [{label:'Standard', value:'true'},{label:'Restricted', value:'false'}];
As you can see I have add "tab" for testing. I am changing the value of tab on load function which is getting called when I click on Edit. New value of tab is getting printed but the drop-down is not getting initialized.
You're trying to two-way bind your array attribute from the controller using:
scope: {array: '=' ... }
The problem is that you don't have any $scope.array in your controller. So you need to somehow link the two. try this:
link: function(scope, element, attrs){
scope.optValue = attrs.optValue;
scope.optDescription = attrs.optDescription;
scope.array = scope.$eval(attrs.array);
}
This will work but wouldn't create dynamic binding. It will only read the value once and then it won't be updated if the scope[attrs.array] ever changes. In order to do that, you'd have to do a $watch to listen for changes:
scope.$watch(attrs.array, function(newVal, oldVal){
if(newVal != oldVal){
scope.array = newVal;
}
});
Hope that helps.
UPDATE:
To clarify, there are multiple ways to do this, so its up to you for how you want to do it. But the idea behind the scope: { ... } binding in a directive is that you're specifying which properties of the scope you want to be bound and how you want them bound (one-way or two-way). You were telling angular to bind the $scope.array property from your controller into your directive. But because there wasn't any $scope.array property, nothing was being bound. So you can either change your binding to say scope: {permissionValues: '='} or you can change your $scope.permissionValues in your controller function to be $scope.array. Either should work.
But since you're trying to use the array="permissionValues" attribute on your directive to signify which $scope property to use, I figured you would want to do it the way I specified above. Using scope.$watch means you don't have to use any scope: { ... } binding at all. The $watch function manually creates that binding for you and allows you to use the attrs.array value to dynamically bind to the proper scope parameter at runtime. Using scope: {} requires that you know what the scope property is called when the directive is created, and not when it is read from the DOM.
Does that make more sense?

Resources