I'm trying to build a directive to output some HTML formatted code for a paging control (Twitter Bootstrap styled), this directive needs to take the current page and total pages from the scope of my controller and when a paging link is clicked trigger a function on my controller to change the page (builds a url and calls $location to change page).
I've watched many of the excellent YouTube angularjs videos (http://www.youtube.com/watch?v=nKJDHnXaKTY) but none seem to cover this particular complex scenario.
Any help would be great!
Here is jsfiddle that makes it clearer:
http://jsfiddle.net/philjones88/dVFDT/
What I can't get working is passing the parameter, I get:
changing page to: undefined
In your directive add the changePage call there (I know it's not where you want it). Have it call the parents scope changePage with the same parameter.
$scope.changePage = function(index){
console.log("running changePage");
$scope.$parent.changePage(index); //might want to check if the parent scope has this too
}
As another tip, in directives you shouldn't use the $ in front of the variables being sent in. In this case that would be $scope, $element, $attrs. The $ you see in front of scope in controllers (not linking functions) is there to let you know that it is being injected. It is not being injected in the linking controller. For instance, here:
app.directive("pager", function ($injector1, $injector2) {
This is where injected parameters would go, and you want to be able to distinguish the two of them. I realize this got a little off track and I hope the suggestion I have for the changePage is what you're looking for.
Edit: Updated fiddle http://jsfiddle.net/dVFDT/48/
Modified answer for future searchers: The function you were passing in via the click method like so:
..... click="changePage()".....
Needed to be changed to:
..... click="changePage".....
This means you're passing the function through and not the function call. This meant that in your directive when you wired up the changePage callback you were calling the function with the index like this:
changePage()(1)
and that's why you were getting undefined.
I dont understand completely, but at the end of your directive you want to execute a function of your controller?
Try:
<div class="pagination">
<pager current-page="currentPage" total-pages="totalPages" query="query" callback="changePage()"></pager>
</div>
I realize this question is a bit old, but there's actually another way to solve this that doesn't require recompiling or calling the parent scope. It does, however, require calling the method from within the directive in a slightly different way.
You can see the fiddle here: Fiddle
The line that's of most interest is in the template declaration. The call to onClick requires you pass it an object rather than just the value.
template:
"<div ng:repeat='i in [] | range:totalPages'> " +
"<a ng:click='onClick({page: i + 1})'>Page {{i + 1}}</a>" +
"</div>",
This also makes use of a filter from this answer from Gloopy in order to iterate n number of times in an ng:repeat. This allows the binding to all happen in the template.
Related
I'm starting with AngularJS and I have a question related to the way a method is invoked when setting a new controller.
Let's say I have a route configured like this:
$routeProvider.when('/myApp/:id', {controller: 'MyAppCtrl'});
What's the difference between these 2 controller codes, regarding the execution context and the $scope life cycle?
How many times each alternative runs after the partial is loaded?
.
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.$on('$routeChangeSuccess', function(){
$scope.data = getNewData($routeParams.id);
});
function getNewData(id){
...
}
});
And:
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.data = getNewData($routeParams.id);
function getNewData(id){
...
}
});
Thank you very much.
In my opinion, I would use resolve in route config instead of your 2 options
back to your question.
I believe controller only execute once after the partial is loaded.
and these 2 cases are pretty much doing the same thing. The first one relies on event, which is an extra step comparing with the 2nd one.
$on assigns a listener to an event. Meaning You could trigger $on manually by sending $broadcast('routeChangeSucess').
The second code is run one time, once The partial is loaded.
So using $on for a Controller load dosn't do You any good
What I want is very simple, I want the Expand All button to be auto clicked when I open this pluralsight course page. Its HTML is:
<a id="expandAll"
ng-click="expandAllModules()"
ng-hide="allModulesExpanded()">
Expand All
</a>
So it seems easy and we just need to call the function expandAllModules(). However I don't know why it give me undefined when I check its type:
typeof expandAllModules
=> "undefined"
Generally typeof a function should give me "function" like this:
function a(){}
=> undefined
typeof a
=> "function"
Since the function expandAllModules() is not available, I can't call it. Anyone can give me a hand on this issue?
Edit
Perhaps I need to elaborate on my question. I'm not the author of that page. I just want to make a simple greasemonkey or tempermonkey script and expand the modules automatically when I enter the page.
The Problem
The reason calling just expandAllModules() doesn't work is because this function belongs to one of Angular's scopes and isn't a method assigned to window. This function is defined in Plural Sight's table-of-contents-controller-v9.js like so:
"use strict";
pluralsightModule
.controller("TableOfContentsController", ['$scope', ..., function ($scope, ...) {
...
$scope.expandAllModules = function() {
_.each($scope.courseModules, function (module) { module.visible= true; });
};
...
}])
The Solution
In order for us to call this function ourselves, we have to go through this scope.
scope is an object that refers to the application model. It is an execution context for expressions. Scopes are arranged in hierarchical structure which mimic the DOM structure of the application. Scopes can watch expressions and propagate events. – AngularJS: Developer Guide
The scope is part of the element which triggers the function. We can access this particular scope by passing the element's id attribute into angular.element(), then calling scope() on that object:
angular.element('#expandAll').scope()
This will give us the following data, where we can see the expandAllModules() function:
Unfortunately AngularJS doesn't let us simply execute scope().expandAllModules(); instead we have to go through it's $apply and $eval methods:
var scope = angular.element('#expandAll').scope();
scope.$apply(function() {
scope.$eval(scope.expandAllModules())
});
We can now also collapse the modules as well by calling:
scope.$apply(function() {
scope.$eval(scope.collapseAllModules())
});
I apologize if I am off-base here. Are you trying to "link" into that page and 'force' the page to "expand all", or do you have access to the page, and want to trigger the click with some code on the page, by you inserting the code? Just doing something like this seems to work from commandline.
jQuery(function(){
jQuery('#expandAll').trigger('click');
});
Since I do not know your need, my thought is that this is a bit simplistic and not what you are looking for. From the responses of others, it appears you want to create your own directive to initiate the click?
I might have some typos -- but the idea is there.
angular.element(document.body).ready(function() {
var el = angular.element( document.getElementById('expandAll') );
var scope = el.scope();
scope.expandAllModules();
scope.$digest(); <--- might not be needed, but when i check your site, it needs to have this
});
updates
if it was just 'onclick' instead of 'ng-click', you do not need to get the scope; and just call the function directly.
updates
I have tried this on your site, you need to have scope.$digest(). When I tried it, i was using the developer console.
see the developer console below
I was playing with it on your site.
I'm creating two Angular directives, fooContainer and foo, where the fooContainer will load one or more foo elements and render them inside its container.
Now I also want to attach some JavaScript events to the foo directives compiled HTML from the fooContainer directives link function since the container is supposed to be responsible for some things like dragging the compiled foo elements around.
Since $compile is asynchronous when compiling directives with templateUrl the HTML isn't available directly after calling $compile(html)(scope), however if I use a timeout for about a second the HTML is rendered in and I can interact with it.
This isn't optimal. Does $compile expose an event or promise I can use to be notified when all HTML is rendered?
Here is a Plunker that describes my problem, http://plnkr.co/edit/coYdRqCsysV4txSFZ6DI?p=preview
Approaches in order of preference:
1) Follow pattern of simmi simmi whenever you can and use angular (ng-...) approach. This is most reliable.
1.5) UPDATE: Liquinaut 'attribute directive' approach below seems valid - I've only used it in a quick demo POC and it worked fine. Assuming this survives more complex usage I would prefer over the watch/jquery option 2 below. Please note however that the $last is specifically for ng-repeat. If you are injecting a $compile chunk of non-repeating markup as per the OP then you need to drop the $last check. And to be clear this requires you to add the attribute post-render to the element you are waiting to render i.e. (per OP plunker)
var el = $compile('<foo><div class="hide" post-render>...
with directive:
.directive('postRender', function() {
return function(scope, element, attrs) {
//your code here
element.hide();
};
});
I've forked the original plunkr and generalized the approach to allow passing a generic callback: http://plnkr.co/edit/iUdbn6zAuiX7bPMwwB81?p=preview
NOTE: This approach only works for basic activities on the element compiled in. If your compile string has angular variable interpolation e.g. {{value}} and you rely on these being resolved in the callback it won't work. They are not resolved at this time. This is also true if the postRender directive is rewritten with an explicit link function. Option 2 below works fine for these cases as it pushes resolution to at least the next digest.
2) I've found watching the DOM very reliable even in very complex apps (although performance should as always be monitored). Replace your el.find('.hide').hide(); line with this block:
scope.$watch(
function() { return element.find('.hide').length > 0;},
function(newVal, oldVal) {
if (newVal) {
//DO STUFF
el.find('.hide').hide();
}
}
);
I wouldn't be comfortable using this in a tight loop but for one off usage on directive instantiation (assuming you aren't creating a 1000 of them!) it seems reasonable - I used this approach for ng/ui-slider integration etc
3) pixelbits approach also good architectural approach if you are building something more complex (and for reusable components) but beware the extra scope that gets created if you are using transclude (e.g. nested directives) it will be $$nextSibling that gets the 'emit'. See: here
BTW: if just want a quick way to do drag and drop see: http://codef0rmer.github.io/angular-dragdrop/#/
The directive fires a postRender event:
fooContainer.directive('postRender', function() {
return function(scope, element, attrs) {
if (scope.$last){
//your code here
}
};
});
Hope that helps!
http://plnkr.co/edit/f4924y6GW7rAMItqVR0L?p=preview
.directive('fooContainer', function($compile, $timeout) {
return {
link: function(scope, element, attributes) {
console.log('link called');
var el = $compile('<foo><div class="hide" >I should always be hidden.</div><div class="hideDelay" ng-show="visiblity">I should be hidden after 1 second.</div></foo>')(scope);
element.append(el);
scope.visiblity=false;
},
restrict: 'E',
template: '<div class="fooContainer"></div>'
}
});
why Dont you try using ng-show/ng-hide
You can safely attach events to the element in the directive's link function, but only for the directive's children. The directive's parent haven't been linked yet.
So within fooContainer's link function, you know that foo has already been compiled and linked, and it's safe to attach events.
If foo needs to be notified once fooContainer is linked, you can use some form of inter-directive communication. i.e. $scope.$emit. If you want to attach events to foo, you can also use a jquery selector inside fooContainer's link function.
According to Angular documentation:
templateUrl
Same as template but the template is loaded from the specified URL.
Because the template loading is asynchronous the compilation/linking
is suspended until the template is loaded
This means your template is already loaded by the time your link function executes. A promise object is not needed.
I have an issue with my angular.js directive.
It should be a kind of autocomplete, in directive's controller property I'm loading an array of values and inside link function compiling template to show the results.
But when I update scope inside link it doesn't reflect on controller and template, please take look at the example here - http://plnkr.co/edit/Lz3QGwklghPo3as2QTqU
Should I apply scope changes or smth similar?
Your code has two problems
Attach click event to document instead of body
Use $apply() inside bind
Below code will resolve your problem
$document.bind('click', function (e) {
scope.results = [];
scope.$apply();
});
I update your $body.bind('click',...) method to
$body.bind('change', function (e) {
scope.results = [];
});
and it seemed to work (I mean that after 0.5 sec I typed a letter, the list of name is re-displayed).
I have a click event that happens outside the scope of my custom directive, so instead of using the "ng-click" attribute, I am using a jQuery.click() listener and calling a function inside my scope like so:
$('html').click(function(e) {
scope.close();
);
close() is a simple function that looks like this:
scope.close = function() {
scope.isOpen = false;
}
In my view, I have an element with "ng-show" bound to isOpen like this:
<div ng-show="isOpen">My Div</div>
When debugging, I am finding that close() is being called, isOpen is being updated to false, but the AngularJS view is not updating. Is there a way I can manually tell Angular to update the view? Or is there a more "Angular" approach to solving this problem that I am not seeing?
The solution was to call...
$scope.$apply();
...in my jQuery event callback.
Why $apply should be called?
TL;DR:
$apply should be called whenever you want to apply changes made outside of Angular world.
Just to update #Dustin's answer, here is an explanation of what $apply exactly does and why it works.
$apply() is used to execute an expression in AngularJS from outside of
the AngularJS framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the AngularJS framework we need to perform proper scope life cycle of
exception handling, executing watches.
Angular allows any value to be used as a binding target. Then at the end of any JavaScript code turn, it checks to see if the value has changed.
That step that checks to see if any binding values have changed actually has a method, $scope.$digest()1. We almost never call it directly, as we use $scope.$apply() instead (which will call $scope.$digest).
Angular only monitors variables used in expressions and anything inside of a $watch living inside the scope. So if you are changing the model outside of the Angular context, you will need to call $scope.$apply() for those changes to be propagated, otherwise Angular will not know that they have been changed thus the binding will not be updated2.
Use
$route.reload();
remember to inject $route to your controller.
While the following did work for me:
$scope.$apply();
it required a lot more setup and the use of both .$on and .$broadcast to work or potentially $.watch.
However, the following required much less code and worked like a charm.
$timeout(function() {});
Adding a timeout right after the update to the scope variable allowed AngularJS to realize there was an update and apply it by itself.