I'm running into a bit of an issue solving a problem with some Angularjs functionality I'm working on.
The basic idea is that I have a system where certain criteria must be met before the user is allowed to advance to the next section of the app. One example of this is that a user must both add a comment, and click a link (in the real app, this is a file download) in order for them to advance.
You can take a look at the full example here: https://jsfiddle.net/d81xxweu/10/
I'll assume that the HTML is pretty self explanatory and move on to what I'm doing with my Angular module. My app declaration and initialization are as follows:
var myApp = angular.module('myApp', ['ngRoute']);
myApp.run(function ($rootScope) {
// Both of these must be met in order for the user to proceed with 'special-button'
$rootScope.criteria = {
criteria1: false,
criteria2: false
};
});
This is pretty simple. I'm attaching an object called criteria to the root scope of the application in order to make it accessible to my directives and controllers. I have a directive that renders the link which allows the user to advance once the criteria are met. In this example the text of the link changes from "Waiting..." to "Click to proceed" to indicate we may advance.
myApp.directive('specialButton', function ($rootScope) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
scope.linkText = 'Waiting...';
var setLinkState = function(currentCriteria) {
var criteriaMet = true;
for(var k in $rootScope.criteria) {
if($rootScope.criteria[k] == false) {
criteriaMet = false;
}
}
if(criteriaMet) {
scope.linkText = 'Click to proceed';
}
};
// Watch for changes to this object at the root scope level
$rootScope.$watchCollection('criteria', function(newValues) {
setLinkState(newValues);
});
}
};
});
So in order to trigger the watch statement we've set on this directive I can add a comment as allowed by this controller:
myApp.controller('comments', function ($scope, $rootScope) {
$scope.commentText = '';
$scope.comments = [];
$scope.addComment = function () {
$scope.comments.push({ commentText: $scope.commentText });
$scope.commentText = ''
// When the user adds a comment they have met the first criteria
$rootScope.criteria.criteria1 = true;
};
});
The previous is my controller for displaying/adding comments. I set criteria1 to true here to indicate the user has added a comment. This actually works fine, and the $watchCollection in the specialButton directive is called as expected.
The problem arises when I try to perform the same action from the link that must be clicked in order to advance. This is rendered with a directive as it is my understanding that in a case such as this a directive makes more sense than a controller, unlike the comment list/form.
myApp.directive('requiredLink', function($rootScope) {
return {
scope: true,
template: "<a href='#'>Click me!</a>",
replace: true,
link: function(scope, el, attrs) {
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
});
}
};
});
As you can see here I pass in $rootScope just as in the controller. However when I set criteria2 to true the $watchCollection is not triggered.
So what ends up happening is if I add a comment first, then click the other button, I do not see specialButton update its text because the second change never triggers the watch. If, however, I click the link first, then add a comment, specialButton updates as expected. The click of requiredLink IS updating the data, but not triggering the watch. So when I then add a comment and the $watch is triggered it sees that BOTH have been set to true.
Thanks in advance for any help you can offer in resolving this issue; I appreciate your time.
Your actual problem is you are update $rootScope from the event which is outside the angular context, so its obivious that angular binding will not update because digest cycle doesn't get fired in that case. You need to fire it manually by using $apply() method of $rootScope
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
$rootScope.$apply(); //this will run digest cycle & will fire `watchCollection` `$watcher`
});
Demo Plunkr
Though this solution work but I'll suggest you to use service instead
of using $rootScope
For implementation using service you need to follow below things that would help you.
Your service should be using criteria variable in object form, should follow the dot rule so that the respective reference will update using JavaScript prototypal
Sevice
app.service('dataService', function(){
this.criteria = {
criteria1: false,
criteria2: false
};
//...here would be other sharable data.
})
Whenever you want to use it any where you need to inject it in function of controller, directive, filter wherever you want.
And while putting watch on service variable from directive you need to do something like below.
Directive
myApp.directive('specialButton', function (dataService) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
//.. other code
// deep watch will watch on whole object making last param true
scope.$watch(function(){
return dataService.criteria //this will get get evaluated on criteria change
}, function(newValues) {
setLinkState(newValues);
}, true);
}
};
});
Related
So I am working on a application that has many many sections that are loaded via jquery ajax, some when it starts and some in response to other sections, and some are even nested. What I wan't to be able to do is say "Oh, the content in mainContainer changed?" Well, go compile the code in that section.
The reason is that I would like to be able to put ui-sref's into parts of the code we don't otherwise want to mess with yet. I came up with a solution but it isn't working the way I thought, I added a $watch to a custom directive but it fires a bazillion times. Here is the code.
Hopefully some one can explain how to get it to run once only when the content in X <div> is changed via one of the many ajax calls that happen, that way I don't need to immediately turn all of the old code into angular and can link to new code that is using angular.
app.directive('ngMainContainer', function ($compile, $timeout) {
console.log("main container directive");
return function (scope, elem, attrs) {
scope.$watch(function () {
return elem;
}, function (val) {
console.log("Something changed. " + val);
$timeout(function () {
$compile(val)(scope);
}, 0);
});
};
});
again, I thought putting a watch on the element passed into the directive was the answer however this runs even when I move the mouse and click on the page somewhere outside of the container that was loaded via jquery ajax.
Here is a solution that may work for you. I place directive ngMainContainer on our element we want to observe the html contents of.
In our controller I append <p>{{ content }}</p> which I first $compile, then update the scope var afterwards like $scope.html = angular.element(myDiv).html(). This is where the our directive $watch fires.
Here is the full working example. The <button> is appending html, but this should emulate the resolution of an ajax call request complete.
JSFiddle Link
app.directive('ngMainContainer', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
scope.$watch('html', function(n, o) {
if(n !== o) {
console.log(elem.contents())
// html has changed, do something!
}
});
}
}
}]);
app.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.content = "scope content";
var myDiv = document.getElementById('main');
$scope.appendContent = function() { // Replicate Ajax Callback
var dynamic = $compile('<p ng-cloak>{{ content }}</p>')($scope); // Compile content
angular.element(myDiv).append(dynamic);
$scope.html = angular.element(myDiv).html()
}
}]);
Edit
Here is a way to do this while appending content to the <div> with no notion of angular on the code that is performing the appending
Updated JSFiddle
<button id="noAngular" onclick="noAngularAppend()">Append Content - No Angular</button>
function noAngularAppend() {
angular.element(document.getElementById('main')).scope().externallyAppend('<p ng-cloak>{{ content }}</p>');
}
Directive Function (inject $compile)
scope.externallyAppend = function(element) {
var node = $compile(element)(scope);
elem.append(node);
scope.$apply(scope.html = elem.contents());
}
Sal, I up voted your answer because it ultimately did help me figure this out but in the end it wasn't quite what I was going for.
This is what I ended up with. In the controller for the main application there is function with only two lines in it.
$scope.doReshing = function (element) {
$compile(angular.element(element))($scope);
$scope.$apply();
}
Then in which ever partial that has an ng directive in it right at the top of the document.ready() I put the following.
var removeableContentContainer = $('#removableContentContainer');
angular.element(removeableContentContainer).scope().doReshing(removeableContentContainer);
This does exactly what I want. It lets me take an existing application change virtually nothing about how it works and lets me add ui-srefs in the "old" code and not have to worry about how or when it loads, when it does load, the document.ready() fires and it will tell angular that it needs to compile. This was my goal, you can pull bits of code in at any point in time and have them essentially act as though they were parts of that page with it's controller.
I have a directive to serve as a credit card form. This form can have many different submit buttons. During the purchase, which is async, I need to make sure that the buttons are disabled. So I'm using a simple observer pattern to accomplish this. The issue I'm having is that when the user clicks a button, the observer pattern is working fine the isolated scope attribute controlling the ng-disable is being set correctly, however the disabled isn't being applied to the buttons. I'm thinking it might be a priority thing?
So heres an observer. The subject is rather mundane. Just validates a form, and has a list of it's observers. Here's where I'm having issues.
.directive('submitCardButton', ['$q', function ($q) {
return {
restrict: 'E',
require: '^createCard',
scope: {
successBack: '&',
buttonVal: '#'
},
template: "<button class='button button-pink full-width small-top' ng-class=\"{disabled: submitting}\" ng-click='getCC()' ng-disabled=\"submitting\">{+ submitting +} {+ buttonVal +}</button>",
link: function (scope, elem, attr, card) {
card.registerButton(scope);
scope.submitting = false;
function getCC () {
card.disableButtons();
card.getCC().then(function (response) {
$q.when(scope.successBack({response:response}))
.finally(card.enableButtons);
}, function () {
card.enableButtons();
});
}
scope.disable = function () {
scope.submitting = true;
console.log(scope.submitting);
};
scope.enable = function () {
scope.submitting = false;
console.log(scope.submitting);
};
scope.getCC = getCC;
} // end link
};// end return
}])// end directive
When I debug, inside the getCC, after I call the disableButtons the submitting is set to true. Howerver the submitting inside the template is still false and therefore not disabled. Any help would be soooo much appreciated.
I created a plunkr that demonstrates the issue I'm having. I'm using a simple user first name last name to show the issue. It works fine if you submit properly. However, if you just submit with out putting any data in, you can see that the submitting flag in the button directive is set to True, but the disabled is not being set properly.
http://plnkr.co/edit/8KTUCNMPBRAFVl1N4nXp?p=preview
In your createCard.getCC() the positive case returns an unresolved promise (it is resolved later with a $timeout), so while the promise is unresolved, the submitting property of submitCardButton's scope is "true" and button is disabled.
In the negative case, the promise is rejected right away (synchronously), and so there is no time for the button to be disabled - the promise rejection handler sets submitting immediately to false.
If you want to see the effect, change the negative use case to this:
if (!(user.firstname && user.lastname)) {
$timeout(function() {
defer.reject('bad user! bad!');
}, 5000);
}
So i've looked around for someone who has tried to attempt what I am and have had no luck... So here we go. In response to the Kendo Lab's post *Disclaimer*: While Angular Kendo UI is not supported under the formal Kendo UI support agreement, this is an active community project, and we would LOVE for you to open an issue, or fork the repo and submit a pull request. StackOverflow is also a great place to get assistance. I have no choice but to come to StackOverflow. Here's my situation.
I have angular kendo setup in my web app and it runs GREAT! (Bit of a learning curve but that's how things go). I want to know test that directives i've written work properly and would LOVE to be able to test the with the kendo directives i'm looking for.
Now for the code
resize-directive.js
app.directive("ibsResizeGraphDirective", ['$window', function ($window) {
return function (scope, element) {
//Bind to window resize so that we can have the elements respond.
//There is no element.resize event and watching for width change doesn't work
angular.element($window).bind('resize', function () {
scope.$apply();
});
//Watch for element.width() change
scope.$watch(function () {
return $(element).width();
}, function (newValue, oldValue) {
if (newValue != oldValue) {
scope.graph.options.chartArea.width = $(element).width();
// Turn off transitions so that the graphs don't redraw everytime the window changes
if (oldValue != 0 && scope.graph.options.transitions) {
scope.graph.options.transitions = false;
}
scope.graph.refresh();
}
})
//...
};
}]);
as you can see i'm trying to basically check the size of the chart's element and set the chartArea.width accordingly.
The biggest problem i'm having is getting the chart to even show up. To help make things easier on our end we decided to wrap up our chart declaration into a directive!
chart.js
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
return {
// Restrict E for element
restrict: 'E',
// Here we setup the template for the code we want to replace our directive
template:"<div ibs-resize-graph-directive \n\
ibs-owner-warehouse-listener-directive \n\
ibs-graph-culture-caption \n\
kendo-chart='graph' \n\
k-options='chartOptions' \n\
k-data-source='dataSource' \n\
class='col-lg-4'/>",
replace: true,
scope: {
//...
},
controller: function($scope){
//...
},
link: function (scope, element, attrs) {
//...
};
}]);
and finally my tests...which I can't even get my chart to render properly...so how am I supposed to even check that the width changed?!
resize-test.js
//Setup items before each test
beforeEach(module('dynamanMain', 'kendo.directives'));
//Initialization Tests
it('should render the chart', angular.mock.inject(function ($compile, $rootScope, $timeout) {
var scope = $rootScope.$new();
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
scope.$apply();
//$timeout.flush();
//Test that the kendo grid was created over the div element
console.log(ele.find('div')); //This basically outputs nothing
}));
And here is a screenie of the result
There is not chart rendered (which I know it's because i'm not appending it to the body)
There is no element in the body after my scripts (which when doing the angular-kendo tests they appear)
I get the prototype returned from my element.find('div')
This is definitely a longer post but I wanted to be as thorough as I could to get a good answer. Anyone have any thoughts?
I'm a bonehead. I was not targeting the directive correctly.
ele = $compile('<ibs-chart-directive></ibs-chart-directive>')(scope);
should be
ele = $compile('<ibs-chart></ibs-chart>')(scope);
because my directive is defined as
app.directive('ibsChart', [ "ibsMainService", function (ibsMainService) {
as a result of making these changes I was also able to complete the test and test the width of the chart and whether or not the resize function was being called....I LOVE finding small things like that after searching for days.....
I am getting into AngularJS, and I've been trying to understand directives because they are pretty much mandatory if you want to work with the DOM (when using AngularJS, correct me if I'm wrong). So here is the scenario, I am trying to create a simple login system (I am actually using the MEAN stack - MongoDB, ExpressJS, AngularJS, NodeJS). I'm not too worried about security (or otherwise less than perfect code) because I am just trying to learn how to use the frameworks. Here is the relevant code:
MemberModule.js:
var MemberModule = angular.module('MemberModule', ['ui.bootstrap']);
MemberModule.controller('MemberListController', function ($scope, $html)) {
$scope.members = [];
$scope.newMember = {
done : false
};
$scope.doneFilter = { done : true };
$scope.notDoneFilter = { done : false };
//various methods...
});
MemberModule.directive('usernameDir', ['$interval', function($interval) {
function link(scope, element, attrs) {
var newMember,
timeoutId;
function updateUsername() {
element.text(scope.newMember.username);
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
UpdateTime(); // update DOM
}, 1000);
}
return {
link: link
};
});
MemberModule.directive('passwordDir', function () {
// The above name 'myDirective' will be parsed out as 'my-directive'
// for in-markup uses.
return {
restrict: 'E',
transclude: true,
scope: {
'sub' : '&ngSubmit'
},
template: 'home'
}
});
As you can see above, I created the main angular.module and called it MemberModule - which gets referenced in my HTML (I am using jade templates - so by HTML I mean layout.jade). After that I created the controller with its various methods that I need. Finally, I created the directives which is what I need help with. I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
The relevant form HTML ('index.jade'):
extends layout
block content
div.container(ng-controller="MemberListController", ng-init="setMembers( #{JSON.stringify(members)} )")
h1 Welcome
h2 Sign Up
form(novalidate, ng-submit="addNewMember()")
input( type="text", username-dir info="userdir")
br
input( type="password", password-dir info="passdir")
br
input( type="password" )
br
button.btn.btn-primary(class="sub", type="submit") Submit
h2 Adding...
span(username dir)
span(password dir)
I am just pasting what I have so far so you can see where I am at in terms of progress. I am fully aware that my code is not functional as is - I am just looking for some help in pointing out what needs to go where to accomplish my goal. I realize that the two directives (while trying to attain the same goal) are not using the same style of directive code - this is just because of where I am at in terms of trying things. Again, my goal is (specifically for the username and password):
I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
Thanks.
Big ups to Julian Hollmann (check comments):
"You don't need both directives at all. Just use ng-model (docs.angularjs.org/api/ng/directive/ngModel) to bind your scope data to the input elements. Then use ng-submit to call a function in the controller."
Bingo - thanks!
In a nutshell:
I try to do something like this inside my directive - namely change the value of model that is liked to 'trigger' attribute:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
and it does not work. Why? Should I do it other way around?
And here is full story:
I have a dialog that is triggered when showRemoveDialog flag is set. This flag is set when user clicks Remove button.
Here is a dialog's opening tag:
<div remove-dialog trigger="{{showRemoveDialog}}" class="modal fade" id="myModal">
Then I have a directive removeDialog:
myApp.directive("removeDialog", function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
attrs.$observe('trigger', function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
});
},
controller: 'DeleteController'
};
});
As you can see, I observe trigger attribute and when it changes to true (user clicks Remove button), I show the dialog:
$scope.remove = function () {
$scope.showRemoveDialog = true;
};
And it works.
But if the value of trigger changes to false/null I want to close it - for instance Cancel button was clicked, or X icon was clicked. And if one of these two actions occur, I need to set back trigger value to false/null, so that the next time when user click on Remove button value would change from false -> true, and my dialog appears once again.
The problem is that this piece of code does not work:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
I mean it does not set the value of {{showRemoveDialog}} in scope to null. I already tried $apply function, but still in wain.
I guess I'm doing something really wrong in angular. Please help.
Yes, the idea you have come up with is kind of confusing, changing the attribute will not actually change the scope variable, so to fix this you would have to change the scope variable, in this case you know what the variables name is so it would work, but for other elements you might not know what the variable is. To fix this specific issue you would have to do.
scope.showRemoveDialog = null;
scope.$apply();
This is not very dynamic though. Here is what I would do (not tested).
Pass the variable name in as a string
trigger="showRemoveDialog"
Then in your directive get some help from $parse
myApp.directive("removeDialog", function ( $parse ) { ....
The link function...
link: function (scope, element, attrs, controller) {
var variableName = attrs.trigger;
angular.element(element).on('hidden.bs.modal', function () {
$parse(variableName + ' = null')(scope);
scope.$apply(); // Might not be needed.
});
scope.$watch(variableName, function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
}, true); // true might not be needed.
},
Also you don't need to do angular.element(element) as the element passed to the link function should already be wrapped.
The first argument to the jQuery on() method is the event you're listening for. I've never seen it used with custom events before, only standard Javascript ones like "keydown". So my first question would be have you tested that the event hook is ever called? If not put a console.log("event called"); before you try to set your element's trigger attribute.
Another thing I would mention is that setting an attribute to null like that wont work. Have a look at the AngularJS source code . Instead I would set the attribute to false.
Lastly I would recommend just using the Angular UI Bootstrap library that includes a nice modal feature - or something else, I don't mind but reinventing the wheel here seems unnecessary.