I'm using Polymer and AngularJS together, and I want the page to show a toast after a successful form submitting.
Here's my HTML:
<div ng-view></div>
<paper-toast id="toast" text="{{$rootScope.message}}" show-toast></paper-toast>
My $routeProvider has indicated different templates and controllers for different routes, so I put the toast element outside. Since the submitting causes a page jump, I need to put the message where the toast element can access, i.e. $rootScope.
And JS:
app.directive('showToast', ['$rootScope', function($rootScope) {
restrict: 'A',
link: function($scope, element, attrs) {
var toast = document.querySelector('#toast');
if ($rootScope.message != '') {
toast.show();
toast.addEventListener('core-overlay-close-completed', function() {
$rootScope.message = '';
})
}
}
}])
But an error occurs saying that undefined is not a function at link, pointing to toast.show(). But if I put the content of link function into that page controller, it can work.
What's wrong with directive? And as you can see, the link function has a parameter element, but since the show() method is not a jQuery method, I don't know how to call it through element.
Remove this line
var toast = document.querySelector('#toast');
Because the toast element should be a global variable on the window object (you could access it by using window.toast if there is some other scope variable interfering with it)
My apologies. I made a mistake. the element parameter is an array of DOM nodes, so I can just call show() method by element[0].show(). Sorry for confusing if any.
Related
I have limited knowledge of Angular so please bear with me. I am in a situation where I can only modify one js file which is included BEFORE all of the Angular stuff. There is a directive that is causing a problem, yet I can not modify it directly. So I've tried to override it by adding the snippet below in a document ready block:
app.directive('selectionChange', function($rootScope){
return {
priority: 1,
terminal: true,
link: function(scope, el, attr) {
console.log('works');
};
};
});
I can see this directive added to the end of the invokeQueue, but it is never executed. How do I get this attached? Thanks!
UPDATE:
Sorry, let me try to clarify. Problem is, the original directive continues to fire, but the newly attached one does not (tested by using console.log and alert). The markup is something like this:
<html>
<head>
...
<script src="[the file I can modify].js"></script>
...
</head>
<body>
...
<script src="angular.js"></script>
<script src="directives.js"></script> // here is where the existing selectionChange directive is defined
...
</body>
</html>
Here's a plunker
$(function () {
var app = angular.module('app');
app.config(function ($provide) {
$provide.decorator('badDirective', function ($delegate) {
var badDirective = $delegate[0];
var link = function (scope, element) {
element.text('good');
}
var originalCompile = badDirective.compile || function () {};
badDirective.compile = function () {
originalCompile.apply(badDirective, arguments);
// compile returns link fn, directive 'link' property will be ignored anyway
return link;
}
return $delegate;
});
});
})
Doing it on 'ready' state (e.g. jQuery ready implementation) is the right thing. This way the code will be launched before the bootstrapping process (it will be queued on 'ready' via ng-app as soon as angular.js is loaded).
bad directive is just badDirective service internally which contains an array of DDO (because there can be several directives with the same name). And it can be decorated, as any other service.
Link function can be defined with either link or compile (it can return link) DDO properties. The second overrides the first, so always stick to compile when decorating directives.
In the file you can modify create a script tag with a (reference/definition) to your new directive and place that tag at the bottom of the body right after the troubled directive definition. By being the last one defined you'll ensure that is your directive the one rendered.
I have this code:
<body ng-controller="testController">
<div test-directive transform="transform()">
</div>
<script type="text/ng-template" id="testDirective.html">
<div>
<p>
{{transform()}}
</p>
</div>
</script>
<script>
angular.module("Test", [])
.directive("testDirective", function() {
return {
templateUrl: "testDirective.html",
scope: {
transform: "&"
},
link: function(scope) {
}
};
})
.controller("testController", function($scope) {
$scope.transform = function() {
return "<a ng-click='somethingInController()'>Do Something</a>";
};
$scope.somethingInController = function() {
alert("Good!");
};
});
</script>
</body>
So basically what I want to accomplish is to create a directive with a method that will be called from the controller. And that method will do something with the values passed (in this example it does not receives nothing, but in the real code it does).
Up to that point is working. However, the next thing I want to do is create an element that will call a method in the controller. The directive does not knows what kind of element will be (can be anything) nor what method will be. Is there any way to do it?
Fiddle Example:
http://jsfiddle.net/abrahamsustaita/C57Ft/0/ - Version 0
http://jsfiddle.net/abrahamsustaita/C57Ft/1/ - Version 1
FIDDLE EXAMPLE WORKING
http://jsfiddle.net/abrahamsustaita/C57Ft/2/ - Version 2
The version 2 is now working (I'm not sure if this is the way to go, but it works...). However, I cannot execute the method in the parent controller.
Yes. However there is a few problems with your code. I will start by answering your question.
<test-directive transform='mycustommethod'></test-directive>
// transform in the directive scope will point to mycustommethod
angular.module('app').directive('testDirective', function() {
return {
restrict: 'E',
scope: {
transform: '&'
}
}
});
The problem is that printing the html will be escaped and you will get < instead of < (etc.). You can use ng-bind-html instead but the returned html will not be bound. You will need to inject the html manually (you can use jquery for this) in your link method and use var compiled = $compile(html)(scope) to bind the result. Then call ele.after(compiled) or ele.replace(compiled) to add it to your page.
I finally get to get it working.
The solution is combined. First of all, I needed to add another directive to parse the element I wanted:
.directive("tableAppendElement", function ($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs) {
var el = angular.element("<span />");
el.append(attrs.element);
$compile(el)(scope);
element.append(el);
}
}
})
This will receive the element/text that will be appended and then will registered it to the scope.
However, the problem still exists. How to access the scope of the controller? Since my directive will be used by a lot of controllers, and will depend on the model of the controller, then I just set scope: false. And with that, every method in the controller is now accessible from the directive :D
See the fiddle working here. This also helped me because now, there is no need to pass the transform method, so the controller can be the one handling that as well.
I'm looking to compile the contents of transcluded stuff before the link function executes. Currently, if I transclude an ng-bind-safe, the contents will not be added until after my link function.
I can do a scope.$apply() in the link function (and it works) but I get console errors since the digest cycle is already in progress.
Thoughts? Thank you!
During the compilation and linking phases ($compile and $link functions), you only have access to your template in your $compile function, and access to your scope and template in the $link function. You do not have access to the rendered template yet because it hasn't happened yet. For that, you need to setup a watch expression, which you will provide call back functions for. Angular will let you know when the value that you're watching has changed, and within this call back you have access to the rendered template.
This watch expression can only be done within the $link function because that is only place in the directive that you can properly access the scope.
Here is an example:
app.directive('tmTime', function() {
return {
restrict: 'A',
template: '<div>{{time}}</div><div ng-transclude></div>',
transclude: true,
link: function (scope, element, attr) {
scope.time = 'Its hammer time!';
scope.$watch('time', function(newVal, oldVal) {
// this is your call back function
// within here, you have access to the rendered template
alert(element[0].outerHTML); // (it's hammer time! in first div, transcluded contents in second div)
});
}
};
});
I need to scroll to a specific anchor tag on page reload. I tried using $anchorScroll but it evaluates $location.hash(), which is not what I needed.
I wrote a custom provider based on the source code of $anchorScrollProvider. In it, it adds a value to the rootScope's $watch list, and calls an $evalAsync on change.
Provider:
zlc.provider('scroll', function() {
this.$get = ['$window', '$rootScope', function($window, $rootScope) {
var document = $window.document;
var elm;
function scroll() {
elm = document.getElementById($rootScope.trendHistory.id);
if (elm) elm.scrollIntoView();
}
$rootScope.$watch(function scrollWatch() {return $rootScope.trendHistory.id;},
function scrollWatchAction() {
if ($rootScope.trendHistory.id) $rootScope.$eval(scroll);
});
return scroll;
}];
});
Now, when I try to call the scroll provider in my controller, I must force a digest with $scope.$apply() before the call to scroll():
Controller:
//inside function called on reload
$scope.apply();
scroll();
Why must I call $scope.$apply()? Why isn't the scroll function evaluating in the Angular context when called inside the current scope? Thank you for your help!
I'm not sure what your thinking is behind using $rootScope.$eval(scroll) - since the scroll() function is already executing in a context where it has direct access to the $rootScope.
If I understand correctly, you want to be able to scroll to a particular element as denoted by an id which is stored in $rootScope.trendHistory.id.
When that id is changed, you want to scroll to that element (if it exists on the page).
Assuming this is a correct interpretation of what you are trying to achieve, here is how I might go about implementing it:
app.service('scrollService', function($rootScope) {
$rootScope.trendHistory = {};
$rootScope.$watch('trendHistory.id', function(val) {
if (val) {
elm = document.getElementById($rootScope.trendHistory.id);
if (elm) elm.scrollIntoView();
}
});
this.scrollTo = function(linkId) {
$rootScope.trendHistory.id = linkId;
}
});
This is a service (like your provider, but using the simpler "service" approach) which will set up a $watch on the $rootScope, looking for changes to $rootScope.trendHistory.id. When a change is detected, it scrolls to the element indicated if it exists - that bit is taken directly from your code.
So to use this in a controller, you'd inject the scrollService and then call its scrollTo() method with the ID as an argument. Example:
app.controller('AppController', function($scope, scrollService) {
scrollService.scrollTo('some_id');
});
In your question, you mention this needing to occur on reload, so you'd just put the call into your reload handler. You could also just directly modify the value of $rootScope.trendHistory.id from anywhere in the app and it would also attempt to scroll.
Here is a demo illustrating the basic approach: http://plnkr.co/edit/cJpHoSemj2Z9muCQVKmj?p=preview
Hope that helps, and apologies if I misunderstood your requirements.
Is there a way to call an Angular function from a JavaScript function?
function AngularCtrl($scope) {
$scope.setUserName = function(student){
$scope.user_name = 'John';
}
}
I need the following functionality in my HTML:
jQuery(document).ready(function(){
AngularCtrl.setUserName();
}
The problem here is my HTML code is present when page is loaded and hence the ng directives in the html are not compiled. So I would like to $compile(jQuery("PopupID")); when the DOM is loaded.
Is there a way to call a Angular function on document ready?
Angular has its own function to test on document ready. You could do a manual bootstrap and then set the username:
angular.element(document).ready(function () {
var $injector = angular.bootstrap(document, ['myApp']);
var $controller = $injector.get('$controller');
var AngularCtrl = $controller('AngularCtrl');
AngularCtrl.setUserName();
});
For this to work you need to remove the ng-app directive from the html.
The answer above although correct, is an anti-pattern. In most cases when you want to modify the DOM or wait for the DOM to load and then do stuff (document ready) you don't do it in the controller but in he link function.
angular.module('myModule').directive('someDirective', function() {
return {
restrict: 'E',
scope: {
something: '='
},
templateUrl: 'stuff.html',
controller: function($scope, MyService, OtherStuff) {
// stuff to be done before the DOM loads as in data computation, model initialisation...
},
link: function (scope, element, attributes)
// stuff that needs to be done when the DOM loads
// the parameter element of the link function is the directive's jqlite wraped element
// you can do stuff like element.addClass('myClass');
// WARNING: link function arguments are not dependency injections, they are just arguments and thus need to be given in a specific order: first scope, then element etc.
}
};
});
In all honesty, valid use of $document or angular.element is extremely rare (unable to use a directive instead of just a controller) and in most cases you're better of reviewing your design.
PS: I know this question is old but still had to point out some best practices. :)