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>
Related
You'll have to bear with me on this one, because it's a little involved and isn't answered by the following questions:
Two way binding Angularjs directives isn't working
AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding
Both of those answers are more than 3 years old and no longer apply. Please do not mark this as a duplicate of those questions.
I wish that I could make a simple, reproducible version of this problem, but my Angular is honestly not good enough, and it is a fairly complex setup with lots of meddling from the outside. But the title is correct.
TL;DR version:
User presses escape from input box made by custom directive
Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
This update is not reflected in the data displayed by the directive
Putting code to do that update inside the directive itself, which directly updates its OWN bound model, to escape the problem of parent-scope bound-model changes not working, does not work.
In fact, even though it is changed inside its own directive controller, it sets the parent-scope model and its own model to the input value instead of the one I'm trying to overwrite with.
And it still doesn't work.
???
Long version with code:
I had a bunch of code I was repeating over and over again in my HTML, and decided to make a custom directive. The code uses many functions from the parent scope. You don't need to know what all these functions do, just note there's a ton of them that happen on click, on blur, etc. So, it looks like this:
template html file
<div ng-if="should_display" class="myclass" ng-class="cssSizeClass">
<div ng-if="allow_edits">
<span
class="text myapp-edit-hover"
ng-click="showEditField = true; captureValue(propertyString,binValue, $event)"
ng-hide="showEditField"
{{ binValue | customNumber: numberPlaceholder }}{{ numberType }}
</span>
<input
ng-show="showEditField"
class="myapp-editable-input"
type="text"
focus-on-show
ng-focus="selectText($event)"
ng-model="binValue"
ui-number-mask="numberPlaceholder"
ui-hide-group-sep
ng-blur="showEditField = false; inputUpdate(propertyString, binValue, 27, false )"
ng-keyup="inputUpdate(propertyString, binValue, $event, false)">
<span class="numberSuffix">{{ numberSuffix }}</span>
</div>
In my directive controller, in order to use those pesky parent-scope functions...
.directive('appDetailCardInput', function() {
return {
restrict: 'E',
templateUrl: '/static/js/directives/mytemplate.html',
scope: { // # = string, = = object, & = function
propertyName: '#',
cssSizeClass: '#',
numberPlaceholder: '#',
numberType: '#',
binValue: '=',
editable: '=',
numberSuffix: '#'
},
controller: function($scope) {
// define local properties
$scope.should_display = $scope.$parent.thing;
$scope.propertyString = 'thing.' + $scope.propertyName;
// HERE, outside functions are "captured"
$scope.captureValue = $scope.$parent.captureValue;
$scope.inputUpdate = $scope.$parent.inputUpdate;
$scope.selectText = $scope.$parent.selectText;
// check if field is editable
if (typeof $scope.editable === 'undefined') {
$scope.allow_edits = true;
}
else {
$scope.allow_edits = $scope.editable;
}
}}
};
});
This actually works 90%! When the bound model in the parent scope is updated outside the Angular digest cycle (the usual case is during a callback from an async messaging service), it updates the displayed value properly. Other things that occur "outside" the directive update it properly.
However. When escape is pressed or a blur happens, or any code is called that updates the value from "inside" the custom directive, no such luck.
I'd like to take a moment here to note that the $event target will blur properly when programmatically told to.
In the case of the escape key, the inputUpdate() function is supposed to set the value back as if someone hadn't typed something in the input. The problem comes here:
// (in inputUpdate function)
$event.target.blur(); // This works
$event.target.value = $scope.previouslyCapturedValue; // This works
setstr = "$scope.thing." + ser + " = " + $scope.previouslyCapturedValue + ";"; // This does not work
eval(setstr);
The eval() changes the parent scope model back, but that isn't reflected in the child scope.
So I tried both methods from the answers above. Adding a $watch in a link function only fires the SECOND time escape is pressed, not the first, so the first time someone hits escape on a field, it doesn't help! Fail.
The Angular docs suggest wrapping the changes to the value in a $timeout, so that the changes are applied outside of the digest cycle. Doing so doesn't help...
$timeout(function() {
console.log('evaluating outside digest supposedly');
$scope.nexttarget = $scope.nextval;
eval($scope.nextstr);
}, 0, false);
Nothing changes, the same behavior happens. The displayed value does not change (although the value that you change when clicking on it does).
So with some more digging, I find out that $event.target.value = $scope.previouslyCapturedValue; is just setting the value of the input, and not updating the actual bound model. That's fine. The bound model just needs to be updated too.
If I could somehow directly overwrite the text displayed on the , that would almost be a worthwhile solution compared to going back to how things were before.
But like I said before, when it is updated exactly the same as eval($scope.nextstr); from an external messaging service, the update happens perfectly correctly.
So at this point in the story I get it into my head: why not 'intercept' the ngKeyup directive and set the model right there inside the directive? I put this in my controller:
$scope.inputUpdate = function(ser, value, $event, dropdown) {
var keyCode = $event.keyCode;
console.log('got inputupdate!');
if ((parseInt(keyCode) == 27)
|| (typeof keyCode === 'undefined')) {
console.log('escape or deblur!');
if (ser !== $scope.dontBlur) {
console.log('going to set it!!!')
$scope.binValue = $scope.$parent.previouslyCapturedValue; // This is the correct value confirmed by console logs
}
}
$scope.$parent.inputUpdate(ser, value, $event, dropdown);
}
Nope! It gets there in code, but it still doesn't display the correct value! In fact, the bound model is updated to what had been typed in, even though it had been programmatically changed back right there!
Why does it update fine from outside its own code from another controller, but NOT from its own code, even if it's being done directly in the directive controller itself in the "intercepted" function?
Can't wrap my head around this. It's like the bound model is "locked" during this time period or something.
Thanks for any guidance.
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.
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
};
}]
});
Suppose I have a module with a directive as follows (this is a rough not tested)
I need to implement 3 basic things
Configuration for the element that will appear
Event listeners that the base controller can use
Public methods that the base controller can call
angular.module("componentModule",[]) .directive("myComp",function(){
return{
replace:true,
template:'<h2>This is my component</h2>',
scope:{config= "#"},
link:function(scope,element,attr){
this.deleteElement = function(id){
//writing the code to delete this component
//This is a API function that the user can call to delete
}
if (!scope.config.visible){
//this is a configuration object for the element
this.visible(false)}
}
} })
then i have my base HTML like containing the directive call like below
<div myComm="first" config="eleConfig"></myComp>
<div myComm="second" config="newEleConfig"></myComp>
I have a separate controller for my base HTML as follows,
angular.module("baseApp",['componentModule'])
.controller('baseCtrl',function(){
$scope.eleConfig = {
visible:true,
delete:function(e){
//This is called if we call the delete method
}
}
//this is how the delete method is to be called
$scope.first.deleteElement();
})
Question
How to call the deleteElement() method in the baseCtrl as shown above (want to do it the same way KENDO UI does)
The pattern that angular uses is to expose the directive API to the scope. This is how ng-model and ng-form both expose ngModelController and ngFormController APIs.
Here is how I would do it:
angular.module("componentModule",[])
.directive("myComp",function($parse){
return{
replace:true,
scope: {
config: '&'
},
template:'<h2>This is my component</h2>',
controller: function($scope) {
//Directive API functions should be added to the directive controller here or in the link function (if they need to do DOM manipulation)
},
link:function(scope,element, attr, ctrl){
//add to directive controller
if(scope.config().visible) {
//element should be visible, etc.
}
ctrl.deleteElement = function(){
//if this function is called we want to call the config.delete method:
if(scope.config && scope.config.delete) {
//calling the scope.config() method returns the config object from the parent
scope.config().delete(element);
}
}
if(attr.myComp) {
//change to scope.$parent
scope.$parent[attr.myComp] = ctrl;
}
}
}
})
Assuming markup of:
<div my-comp="first" config="configObject"></div>
<div my-comp="second" config="configObject"></div>
In your base controller
$scope.first.deleteElement();
or
$scope.second.deleteElement();
would delete the appropriate element.
UPDATE:
I've updated the directive based on your updated question. You want to pass a config object into the directive. The best way to do that is with an & binding. If you use the & binding, you need to remember that the directive will create a new scope, and you have to attach the controller to $scope.$parent.
In your first requirement, you said you want to write the delete function in the directive, but in the case of KendoUI the actual delete(change) function implementation is done in the base controller and the delete(change) event triggered when the component value changes, which in turn calls the delete function defined in the base controller by the directive.
If you want to implement something like KendoUI does then look at this
link toplunker
Switch on the browser console to see the log. KendoUI component's change event happens automatically when the input element changes but in this case i manually triggered the delete event after 3 seconds.
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>