ui-select2 inside directive isn't updating controller model - javascript

I have a directive that takes in a collection and builds out a dropdown.
.directive("lookupdropdown", function () {
return {
restrict: 'E',
scope: {
collectionset: '=',
collectionchoice: '='
},
replace: true,
template: '<select class="input-large" ui-select2 ng-model="collectionchoice" data-placeholder="">' +
' <option ng-repeat="collection in repeatedCollection" value="{{collection.id}}">{{collection.description}}</option>' +
'</select>',
controller: ["$scope", function ($scope) {
$scope.repeatedCollection = new Array(); //declare our ng-repeat for the template
$scope.$watch('collectionset', function () {
if ($scope.collectionset.length > 0) {
angular.forEach($scope.collectionset, function (value, key) { //need to 'copy' these objects to our repeated collection array so we can template it out
$scope.repeatedCollection.push({ id: value[Object.keys(value)[0]], description: value[Object.keys(value)[1]] });
});
}
});
$scope.$watch('collectionchoice', function (newValue, oldValue) {
debugger;
$scope.collectionchoice;
});
} ]
}
});
This works fine. It builds out the drop down no problem. When I change the dropdown value, the second watch function gets called and I can see that it sets the value of collection choice to what I want. However, the collectionchoice that I have put into the directive doesn't bind to the new choice.
<lookupDropdown collectionset="SecurityLevels" collectionchoice="AddedSecurityLevel"></lookupDropdown>
That is the HTML markup.
This is the javascript:
$scope.SecurityLevels = new Array();
$scope.GetSecurityLevelData = function () {
genericResource.setupResource('/SecurityLevel/:action/:id', { action: "#action", id: "#id" });
genericResource.getResourecsList({ action: "GetAllSecurityLevels" }).then(function (data) {
$scope.AddedSecurityLevel = data[0].SCRTY_LVL_CD;
$scope.SecurityLevels = data;
//have to get security levels first, then we can manipulate the rest of the page
genericResource.setupResource('/UserRole/:action/:id', { action: "#action", id: "#id" });
$scope.GetUserRoles(1, "");
});
}
$scope.GetSecurityLevelData();
Then when I go to post my new user role, I set the user role field like this:
NewUserRole.SCRTY_LVL_CD = $scope.AddedSecurityLevel;
but this remains to be the first item EVEN though I have updated the dropdown, which according the watch function, it has changed to the correct value. What am I missing here?

You faced this issue because of the prototypical nature inheritance in Javascript. Let me try and explain. Everything is an object in Javascript and once you create an object, it inherits all the Object.Prototype(s), which eventually leads to the ultimate object i.e. Object. That is why we are able to .toString() every object in javascript (even functions) because they are all inherited from Object.
This particular issue on directives arises due to the misunderstanding of the $scope in Angular JS. $scope is not the model but it is a container of the models. See below for the correct and incorrect way of defining models on the $scope:
...
$scope.Username = "khan#gmail.com"; //Incorrect approach
$scope.Password = "thisisapassword";//Incorrect approach
...
$scope.Credentials = {
Username: "khan#gmail.com", //Correct approach
Password: "thisisapassword" //Correct approach
}
...
The two declarations make a lot of difference. When your directive updated its scope (isolated scope of directive), it actually over-rid the reference completely with the new value rather then updating the actual reference to the parent scope hence it disconnected the scope of the directive and the controller.
Your approach is as follows:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>
The problem with this approach is that although it works, but it not the recommended solution and here is why. What if your directive is placed inside another directive with another isolated scope between scope of your directive and the actual controller, in that case you would have to do $parent.$parent.AddedSecurityLevel and this could go on forever. Hence NOT a recommended solution.
Conclusion:
Always make sure there is a object which defines the model on the scope and whenever you make use of isolate scopes or use ng directives which make use of isolate scopes i.e. ng-model just see if there is a dot(.) somewhere, if it is missing, you are probably doing things wrong.

The issue here was that my directive was being transcluded into another directive. Making the scope im passing in a child of the directive it was in. So something like $parent -> $child -> $child. This of course was making changes to the third layer and second layer. But the first layer had no idea what was going on. This fixed it:
<lookupDropdown collectionset="SecurityLevels" collectionchoice="$parent.AddedSecurityLevel"></lookupDropdown>

Related

Angular Directive Template Doesn't Bind To Angular Controller Variable

I have a curious case that I can't figure out...
I have a directive on my app like so:
app.directive('cartTotal', function() {
return {
template: "<i ui-sref='cart' class='fa fa-shopping-basket'></i><span class='items'>#{{cartQTotal.total}}</span>"
};
});
When I load the page, this function fires:
if(localStorage.getItem("cart") != null)
{
console.log("makeacart");
var cart = JSON.parse(localStorage.getItem("cart"));
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
$('.fixed-cart').animateCss('bounce');
}
This works.
But if I modify $scope.cartQTotal outside of this, such as in a function (still in the parent controller but derived from an ng-click()) for example:
$scope.add2Cart = function(name){
var cart = JSON.parse(localStorage.getItem("cart"));
for(var zz = 0;zz<cart.length;zz++)
{
if(cart[zz].item == name)
{
console.log("already in cart");
$('.fixed-cart').animateCss('bounce');
return;
}
}
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
}
On //The Number I Want line I get the proper number, as in the variable is correct but my directive template doesn't update. I don't understand why not.
Please assist.
Edit (from the docs):
Observing directives, such as double-curly expressions {{expression}},
register listeners using the $watch() method. This type of directive
needs to be notified whenever the expression changes so that it can
update the view.
So I guess the question is how do I notify the directive properly?
EDIT 2:
Looking at it using the nginspector extension, it appears I have two scopes with cartQTotal rather than one, this remains constant whether or not I have the directive.
I am very confused because I have my controller scope and then a duplicate scope with all the same variables but the cartQTotal changes in one scope and not the other. Why would I have a duplicate but unnamed controller scope?
This is because your directive and $scope and the controller where data is updating both are different..
So you need to pass your controller data to your directive so that it will get modified. For this purpose you can use $broadcast (but make sure you know about it because in large application its not good practice to use it).
So Try this
Controller
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
$rootScope.$broadcast("cartUpdated",$scope.cartQTotal);
directive
$scope.$on('eventEmitedName', function(event, data) {
$scope.cartQTotal = data;
});
It was a problem as elucidated here: How do I share $scope data between states in angularjs ui-router?
Basically I didn't realize that my ui-router configuration was creating a seperate instance of my controller. Changing my code as specified in that answer allowed it to work properly, even though I wasn't changing states it still affected the directive's ability to communicate with the proper controller instance.
Data you wish to use within your directive that is manipulated outside of the directive should be passed in using bindings. There's a great short read here that shows you how. Personally, I use method 6 the most.
The gist is - you add to your directive's returned object:
scope: {
yourVariable: '=', //use =? for optional variables
}
And then use it in your directive as such:
<span>{{your-variable}}</span>
And bind to it as such:
<my-directive your-variable="myControllerVariable"></my-directive>

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

Change parent controller model through directive $watch using controllerAs syntax

I'm new to controllerAs syntax of angular and just trying to understand how it works with directive. I've created one directive for password validation. I want to make some flag true based on conditions and those will be used in parent template for displaying error messages. I'm not getting how can I achieve this!
JSFiddle
VIEW
<div ng-app="myapp">
<fieldset ng-controller="PersonCtrl as person">
<input name="emailID" type="text" ng-model="person.first" >
<input name="pass" type="password" ng-model="person.pass" password-validator>
<p ng-show="person.showMsg">Password validation message here.</p>
</fieldset>
</div>
Directive
myapp.directive('passwordValidator',function() {
return {
controller : PasswordCtrl,
controllerAs : 'dvm',
bindToController : true,
require : ['ngModel','passwordValidator'],
link : function(scope,ele,attrs,ctrls) {
var person = ctrls[1];
var ngModelCtrl = ctrls[0];
scope.$watch(function() {
return ngModelCtrl.$modelValue;
},function(newVal) {
if(newVal!='') {
person.showMsg = true;
} else {
person.showMsg = false;
}
console.log(person.showMsg);
});
}
}
function PasswordCtrl() {
}
});
Specially I want to understand why and how below watch is working fine!
// Why this below is also working, can anyone explain what's going behind!!
scope.$watch('person.pass',function(newVal) {
console.log("Watch fires");
});
This is just for learning purpose so please explain how controllerAs and bindToController works!
I know this was not part of your question, I will get to it, but using directive 'ng-controller' is an anti-pattern. If if are interested why I can explain in a separate post but in short it makes code much harder to follow.
Now, to get to the heart of your question.
From reading the Angular documentation for bindToController it would appear that if you are not also creating an isolated scope, i.e. scope: true or scope: {} it does not do anything.
Personally I have never used it before and does not seem particularly useful.
Using ng-controller is in essence adding a property to the current scope with that controller object.
So:
<fieldset ng-controller="PersonCtrl as person">
Is effectively saying, (in a contrived way):
$scope.person = new PersonCtrl();
Your directive passwordValidator which is using the controllerAs syntax within it is basically doing:
$scope.dvm= new PasswordCtrl();
In this case you effectively have a scope object that looks like:
$scope = {
person = new PersonCtrl(),
dvm: new PasswordCtrl()
}
Your person controller and dvm controller are sibling objects.
Within your passwordValidator directive you are requiring in its controller, which is the dvm object. Using that dvm object are you setting person.showMsg which is the same as doing:
$scope.dvm.person.showMsg = <value>
The dvm object does not have a way to access the person object, as they are siblings, on the $scope. So you need to use the $scope itself to access the person object. You would need to do:
$scope.person.showMsg = <value>
Although this assumes that person exists on the scope, which is a dangerous assumption.
your example is kinda messy but ill try to answer your questions.
// Why this below is also working, can anyone explain what's going behind!!
scope.$watch('person.pass',function(newVal) {
console.log("Watch fires");
});
this works because your directive is using SAME scope and variables on it as its parent controller.
this.checkDirCtrl = function() {
console.log($scope.dvm);
}
this is undefined because you are using controllerAs on SAME scope so new variable called dvm is not created and all your dvm controller variables are initialized on parent scope.
That means that you dont need to 'inject' person controller into directive because you already have controller instance on directive scope. So just set your variable 'showMsg' like this and it will work like a magic!
if(newVal!='') {
scope.person.showMsg = true;
} else {
scope.person.showMsg = false;
}
console.log(scope.person.showMsg);
I made a fiddle for you: JSFiddle

AngularJs Directive: How to dynamically set attributes on ng-repeat element

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

AngularJS : Two-Way Binding an Unknown Property Inside a Directive

I've built a directive that basically creates a markdown-friendly text editor. It will be used at various places throughout my site, anywhere an end user wants to use markdown to do some basic content styling (product descriptions, that sort of thing). The challenge is that as this directive will be deployed in multiple places, it won't be editing the same property on every model. For example, in one spot it may edit the LongDescription of a product, whereas in another spot it may edit the ShortDescription of an ad campaign, or the Bio of a user.
I need to be able to pass in the property that I want to edit to the directive using the scope '=' method that permits two-way data binding, so the property is changed both in the directive and on the original controller, allowing the user to save those changes. The problem that I'm having is that if I pass the property itself to the directive:
<markdown-editor model="product.Description"></markdown-editor>
two-way data binding doesn't work, since this passes the value of the Description property. I know that for the '=' method to two-way bind in a directive, I have to pass an object as the attribute value from my HTML. I can easily pass the entire object:
<markdown-editor model="product"></markdown-editor>
and then access the Description property within the directive:
<textarea ng-model="model.Description"></textarea>
but this hardcodes Description into the directive, and I may not always want that property.
So my question is, how can I two-way bind to a single property of my object, without the directive knowing ahead of time what that property is? I've come up with a workaround but it's pretty ugly:
HTML:
<markdown-editor model="contest" property="Description"></markdown-editor>
Directive JS:
angular.module('admin.directives').directive('markdownEditor', [
'admin.constants.templateConstants',
'$sce',
function (Templates, $sce) {
var directive = {
restrict: 'E',
replace: true,
templateUrl: Templates.Directives.MarkdownEditor,
scope: {
model: '=',
property: '#'
},
controllerAs: 'markdownEditor',
controller: markdownEditorController
}
function markdownEditorController($scope) {
var vm = this;
vm.display = { markdown: true };
vm.content = { markdown: '', html: '' };
console.log($scope.model);
vm.setDisplay = function (type) {
vm.display = {};
vm.display[type] = true;
}
$scope.$watch('model', function (newModel, oldModel, $scope) {
vm.content.markdown = $scope.model[$scope.property];
});
$scope.$watch('markdownEditor.content.markdown', function (newDescription, oldDescription, $scope) {
$scope.model[$scope.property] = newDescription;
if (newDescription !== "" && newDescription !== null && newDescription !== undefined) {
vm.content.html = $sce.trustAsHtml(marked(newDescription));
}
});
}
return directive;
}
]);
Relevant part of the directive template:
<textarea class="ad-basic-input" ng-model="markdownEditor.content.markdown" ng-if="markdownEditor.display.markdown"></textarea>
Notice that the directive uses a watch to look for changes on the content.markdown field, then pushes those back into model[property] manually (the second $watch near the bottom). It also has to $watch for changes to the model being passed in from the controller above because that's being loaded asynchronously, and needs to be assigned to the content.markdown field initially.
This code works, but having these two watches, especially the one that looks for changes on the model, seems like a big code smell to me. Surely there must be a better way to pass in, edit, and two-way bind a single property of an object on the controller, when that property is unknown?
Thanks!

Categories

Resources