Two-way binding within nested directives and ui-select - angularjs

Problem I have been working on:
The original problem (which only addressed one-way binding) can be found here:
Unable to pass/update ngModel from controller to directive
I have managed to get the one-way binding from inside the directive out to the view working. But if I want push in a selection when the page loads, for example, I cannot get the two-way binding to function. I can see that the ng-model inside the first directive is getting the data, but I tried various scope settings with the child directive and either it breaks it or it does the same thing as before - nothing.
I have a basic $watch function set up, so that when I push a simple object into the binding that is attached to ng-model in the view, the watcher assigns the $viewValue to the directive's scope object. It does this, and the directive responds only by having any existing selection wiped out, even though I can clearly see the objects inside ng-model binding assigned to ui-select.
Here is the watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
console.log(newVal, scope.options);
scope.options.selected = newVal;
});
I use this function to update the view whenever we interact with the ui-select (which works fine):
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected);
console.log(scope.options.selected);
};
A Plunker for tinkering
So the expected behavior is:
selections from the ui-select should be displayed in the select, and also get passed to the view
by clicking the 'Change' button, data should be displayed in the view, and get passed to the ui-select

The situation was that I had a directive with ui-select inside it. The way I was able to get data from the main controller scope through the directive scope and down to ui-select's scope was by putting a watch function on ngModel.$viewValue in the link function of the directive.
I then assigned that new value, whenever there was a change, to an object on that directive's scope, to hold it for me. Then I set a watch function in the link function of ui-select to watch scope.$parent.myVal so that if anything was ever pushed to that variable from the main controller, ui-select would see it. When that happened, I would assign that new value to $select.selected which is the container for selected items in the ui-select directive.
It should be noted that ui-select uses scope: true in it's directive, and thus becomes a child of whatever scope it is instantiated in, which allows you to access the scope of it's parent with $parent.
Watch function for the directive:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
Watch function to be added to ui-select:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
Plunker Demo

Related

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'

How to include data/scope from controller in a dynamically added directive?

I'm trying to figure out how to include scope with a directive that I add to the dom on a click event in a controller.
Step 1. On a click event, I call a function in my controller that adds a directive like this
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$(e.currentTarget).append($compile("<my-directive mydata='instanceOfAnObjectPassedInClickEvent'/>")($scope));
}
//I'm trying to take the `instanceOfAnObjectPassedInClickEvent` and make it available in the directive through `mydata`
The above, part of which I got from this SO answer, successfully adds the directive (and the directive has a template that gets added to the dom), however, inside the directive, I'm not able to access any of the scope data mydata it says it's undefined.
My directive
app.directive('myDirective', function(){
return {
restrict: 'AE',
scope: {
mydata: '='
//also doesn't work if I do mydata: '#'
},
template: '<div class="blah">yippee</div>',
link: function(scope,elem,attrs) {
console.log(scope) //inspecting scope shows that mydata is undefined
}
}
}
Update
I changed the name of datafromclickedscope in the OP to make it more clear. In the controller action addMyDirective (see above) instanceOfAnObjectPassedInClickEvent is an instance of an object passed into the controller method on a click event that I try to pass into the directive as mydata='instanceOfAnObjectPassedInClickEvent'. However, even if I change = to # in the directive and I try to access scope.mydata in the link function of the directive, it just shows a string like this "instanceOfAnObjectPassedInClickEvent", not the actual object data that is available to me in my method that handles the click event
When you use mydata='instanceOfAnObjectPassedInClickEvent' in a template you need instanceOfAnObjectPassedInClickEvent to defined in $scope. So before compiling you should assign a variable in $scope. I will rename this variable in code below, so that same names would not confuse you and it would be clear that a formal parameter of a function cannot be visible in a template.
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$scope.myEvent = instanceOfAnObjectPassedInClickEvent;
$(e.currentTarget).append($compile("<my-directive mydata='myEvent'/>")($scope));
}
EDIT: slightly adapted jsfiddle not using JQuery no manipulate DOM

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.

Angular Directive Two-Way Binding Issue

I am creating a custom input directive for some added features.
app.directive('myTextInput', [
function () {
var directive = {};
directive.restrict = 'A';
directive.scope = {
textModel: '='
};
directive.link = function ($scope, element, attrs) {
// FUnctionality
};
directive.template = '<input ng-model="textModel" type="text">';
return directive;
}]);
and used the isolated scope property textModel for two way binding.
And the parent scope HTML is:
<div my-text-input text-model="myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
The controller scope has variable $scope.myScopeVariable=""; defined.
When i do this.
what ever typed in the directive input is able to print in the <span> tag.
Which tells it is updating.
The issue is.
in the parent scope. i have a method that will execute on a button click.
$scope.doSomething = function(){
console.log($scope.myScopeVariable);
};
On click on the button. the log is empty. which is supposed to be what is typed in the directive input.
THIS IS STRANGE
Now if define the scope variable as
$scope.myScopeVariable = {key:''};
and use in HTML as
<div my-text-input text-model="myScopeVariable.key"></div>
<span>TYPED : {{myScopeVariable.key}}</span>
then it is working every where. Even in the previously said function doSomething().
Any idea what happening here ?
This is happen because myScopeVariable contain value as String.
So if you change the value of from textbox of directive. It wont be reflected.
And in second method, you are referring Object. so whenever you change value of object , its relevant watch method is called. And value will be updated.
my-text-input is a directive, so he has his own scope, so the model myScopeVariable is not watch by the parent.
try with this :
<div my-text-input text-model="$parent.myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
It's not the best practice but it might works.

AngularJS : ng-repeat list not updating - directives

I am new to angular and I have the following code:
Here is my html:
<drawer title="'Add a new Item'"
visible="showAddItemDrawer"
on-close="showAddApplicationItem = false">
<!-- items list refresh does not work from inside the drawer. -->
<add-item></add-item>
</drawer>
<!--list refresh works if i place the code out here.
<add-item></add-item> -->
<div ng-repeat="item in itemList">
<item-list></item-list>
</div>
Here is my directive.js:
directive("additem", function () {
return {
restrict:"E",
transclude:true,
controller:"ItemsCtrl",
templateUrl:"partials/add-item.html" //contains a form to add item
};
})
.directive("itemlist", function () {
return {
restrict:"E",
transclude:true,
templateUrl:"partials/item-list.html" //contains code to display the items in a list
};
})
I have a form in add-item.html to add an item. The form shows up when you click on add button(like an accordion). I call the push() to add a new item to the scope.
The list update works if i place the directive call outside the drawer..
If i place it inside the drawer,the scope is not getting updated until i hit refresh.
Can anyone point me what I am doing wrong with the directives? Thanks a lot!
Edit: Added additional code :
In the form to add an item:
<button type="submit"
ng-click="addItemService()">
Add Item
</button>
addItemService() code:
$scope.addItemService = function () {
var data = {
"name": $scope.itemName,
};
ItemService.addItem(data, $scope.listgroupid)
.success(function (result) {
$scope.itemName = "";
viewList(); //The function that sets the scope of the list
})
.error(function () {
});
};
viewlist() code:
var viewList = function () {
ListService.getList($scope.listgroupid)
.success(function (result) {
$scope.itemList = result;
//In the angular inspector, I am able to see the new item in the variable result
})
.error(function () {
});
};
EDIT: function scope is different from variable scope, use $scope.$parent.itemList when referencing new variable value
i had an issue where the directive scope (while debugging) and the batarang inspected scope (when selecting an element in the dev tools and typing $scope.varname in the console) were not matching after a successfully resolved promise.
the promise updates a list for a ng-repeat which contains a directive.
the debugged scope held the values from the last data before the promise was loading new data.
so i added a timeout around the data-loaded event broadcast which pushes the broadcast into the next digest cycle and angular gets informed about the update.
$scope.$apply most often throws the "apply already in progress" error.
the timeout solves this a little more softly...
$timeout( function () {
// broadcast down to all directive children
$scope.$broadcast('dataLoaded');
}, 1);
need to see the code wherein you are calling push for item.
In the absence of that I suspect this is scope issue. Remember that ngRepeat and drawer reside in difference scopes by default and until you have explicitly made them share certain variables , changes inside drawer won't be reflected in ngRepeat. For more on scopes inside directives read http://docs.angularjs.org/api/ng.$compile.
The 'isolate' scope takes an object hash which defines a set of local
scope properties derived from the parent scope. These local properties
are useful for aliasing values for templates. Locals definition is a
hash of local scope property to its source:
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings. If no attr name is specified then the attribute name is
assumed to be the same as the local name. Given and widget definition of scope: { localName:'#myAttr' },
then widget scope property localName will reflect the interpolated
value of hello {{name}}. As the name attribute changes so will the
localName property on the widget scope. The name is read from the
parent scope (not component scope).
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value
of the attr attribute. If no attr name is specified then the attribute
name is assumed to be the same as the local name. Given and widget definition of scope: {
localModel:'=myAttr' }, then widget scope property localModel will
reflect the value of parentModel on the parent scope. Any changes to
parentModel will be reflected in localModel and any changes in
localModel will reflect in parentModel. If the parent scope property
doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION
exception. You can avoid this behavior using =? or =?attr in order to
flag the property as optional.
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name. Given and widget definition of scope: {
localFn:'&myAttr' }, then isolate scope property localFn will point to
a function wrapper for the count = count + value expression. Often
it's desirable to pass data from the isolated scope via an expression
and to the parent scope, this can be done by passing a map of local
variable names and values into the expression wrapper fn. For example,
if the expression is increment(amount) then we can specify the amount
value by calling the localFn as localFn({amount: 22}).
You can either use a $rootScope.$broadcast() to fire an event that your directive would capture, and then let the directive update the list with ng-repeat or you could call a $scope.$apply() with the updation wrapped in it within the controller to indicate angular that he list is updated, but when using $scope.$apply() be careful, and make sure it that it is delayed/deferred until the next digest cycle.
A library called Underscore.js lets you delay $apply until the next digest cycle.

Resources