AngularJS directive won't detect controller scope that binds variables using "this" - javascript

I have the following angularJS controller and directive:
angular.module('twitterApp', [])
.controller('AppCtrl', AppCtrl)
.directive('enter', EnterFunc);
function AppCtrl($scope) {
$scope.loadMoreTweets = function() {
alert('loading more tweets!');
}
}
function EnterFunc() {
return function(scope, element, attrs) {
element.bind('click', function() {
scope.loadMoreTweets();
});
}
}
And the following HTML
<body ng-app="twitterApp">
<div class="container" ng-controller="AppCtrl">
<div enter>
Roll over to load more tweets
</div>
</div>
</body>
Now, this works perfectly, since all I'm doing is accessing the controller's scope from a directive. However, I'm trying to adapt my controller to bind variables to the "this" scope so I can use the "controller as" syntax on the html to make it easier to read, however, when I change my controller function to the following:
function AppCtrl() {
var vm = this;
vm.loadMoreTweets = function() {
alert('loading more tweets!');
}
}
My example stops working and upon clicking on the directive I get the following error:
Uncaught TypeError: scope.loadMoreTweets is not a function
Can someone explain how to get this directive to work without going back to binding to $scope? Here is the Plunkr for the not/working "Controller As" version:
http://plnkr.co/edit/PyIA4HVOMLn0KNJ5V2bc?p=info

I fixed it for now, but posting my solution in case other people stumble upon this. To fix this, instead of using "scope.loadMoreTweets()" on my directive, I used "scope.ctrl.loadMoreTweets()". Even tough this works, I'm not very happy that there isn't a way to access the parent scope without the ".ctrl" since ".$parent" didn't work either. If someone has a better solution please let me know. There should be more documentation on using directives with Controller's Controller As syntax.

I have modified your code to pass a reference to which controller's scope you want to pass to the directive. Then you can call the method on the reference. See if this works for you
Directive:
function EnterFunc() {
return {
restrict: 'A',
scope: {
controller: '='
},
link: link
}
function link(scope, element, attrs) {
element.bind('click', function() {
//scope.$apply(attrs.action);
scope.controller.loadMoreTweets();
});
}
}
HTML:
<body ng-app="twitterApp">
<div class="container" ng-controller="AppCtrl as ctrl">
<div enter controller="ctrl">
Roll over to load more tweets
</div>
</div>

Related

AngularJS custom directive bidirectional binding broken after material upgrade

I wrote a small directive working under Angular 1.6.1 and material 1.1.1.
It is a simple lock/unlock button icon.
I had to update material to 1.1.3 (for the date picker), but since then the directive doesn't work anymore.
I do not understand why a material update would do this....
The plunker below works, but if you change the material version to 1.1.2, it stops working.
http://plnkr.co/edit/ZamxN3WTXaOl5cTv4aWI?p=info
index.html:
<html lang="en" >
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/angular-material/1.1.3/angular-material.css">
<link rel="stylesheet" href="//fonts.googleapis.com/icon?family=Material+Icons">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-animate.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-aria.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-material/1.1.1/angular-material.js"></script>
<!-- <script src="//cdnjs.cloudflare.com/ajax/libs/angular-material/1.1.1/angular-material.js"></script> -->
<script src="script.js"></script>
</head>
<body ng-app="app" ng-controller="controller as ctrl">
<ju-lock ng-model="ctrl.lock"></ju-lock>|{{ctrl.lock}}|
</body>
</html>
script.js:
angular
.module('app', ['ngMaterial'])
.directive('juLock', function(){
return {
restrict: 'E',
scope: {
bindModel: '=ngModel'
},
template:
'<md-button class="md-icon-button">'+
'<md-icon class="material-icons">lock_open</md-icon>'+
'</md-button>|{{bindModel}}',
link: function(scope, element, attributes){
element.on('click', function (ev) {
scope.bindModel = !scope.bindModel;
});
scope.$watch('bindModel', function(){
angular.element(element[0].querySelector('.material-icons')).text(scope.bindModel ? 'lock' : 'lock_open');
});
}
};
})
.controller('controller', function(){
var vm = this;
vm.lock=true;
});
I have investigated as much as I could before asking the stack community, does anyone have some insight into this?
Avoid using ng-model as an attribute in custom directives. If you do so, don't use isolate scope two-way binding with it. Use the ngModelController API which that attribute instantiates.
The main problem is the jqLite click handler needs to notify the AngularJS framework of changes to scope with scope().$apply():
element.on('click', function (ev) {
scope.bindModel = !scope.bindModel;
//USE $apply
scope.$apply();
});
Angular modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and Angular execution context. Only operations which are applied in Angular execution context will benefit from Angular data-binding, exception handling, property watching, etc... You use $apply() to enter Angular execution context from JavaScript.
Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
— AngularJS Developer Guide - Integration with the browser event loop
The DEMO on PLNKR
Also instead of manipulating DOM to change lock and unlock icons. It can done with the ng-show and ng-hide directives:
app.directive('juLock', function(){
return {
restrict: 'E',
scope: {
bindModel: '=myModel'
},
template:
'<md-button class="md-icon-button">'+
'<md-icon ng-show="bindModel" class="material-icons">lock</md-icon>'+
'<md-icon ng-hide="bindModel" class="material-icons">lock_open</md-icon>'+
'</md-button>|{{bindModel}}',
link: function(scope, element, attributes){
element.on('click', function (ev) {
scope.bindModel = !scope.bindModel;
//USE $apply
scope.$apply();
});
/*
scope.$watch('bindModel', function(){
angular.element(element[0].querySelector('.material-icons')).text(scope.bindModel ? 'lock' : 'lock_open');
});
*/
}
};
})
I am not sure about your exact problem among versions, but check this fiddle, this code will work with both versions.
The key point is using ng-click, which is from angularJs, rather than depending on element.on(), which may vary if you inject jQuery or if you not. Plus this way is more declarative
The only change I did was in your directive
.directive('juLock', function(){
return {
restrict: 'E',
scope: {
bindModel: '=ngModel'
},
template:
'<md-button ng-click="bindModel = !bindModel" class="md-icon-button">'+
'<md-icon class="material-icons">{{lock_text}}</md-icon>'+
'</md-button>|{{bindModel}}',
link: function(scope, element, attributes){
scope.lock_text = '';
scope.$watch('bindModel', function(){
scope.lock_text = scope.bindModel ? 'lock' : 'lock_open';
});
}
};
})
I also removed dom inspection in your watch, and I did it more in an Angular-Style

Is there a way to load data into script

So in Angular i'm trying to do
$scope.data = "<script> alert('hi'); </script>";
But unfortunately that doesn't work. I also tried to add ng-bind-html but without any results.
{{data}}
also I tried to load data in a script tag but that also seems not to work. Is there a way to avoid this all? For example
$scope.data = "bob";
-
<script>
var name = {{data}};
</script>
You could create a directive that will load the script into DOM dynamically.
Markup
<load-script ng-if="data" data="data"></load-script>
Directive
app.directive('loadScript', function($compile){
return {
restrict: 'E',
scope: {
'data': '='
},
link: function(scope, element, attrs){
element.append($compile(scope.data)(scope))
}
}
})
Working Plunkr
Your $scope will be already either within a <script> block or in a Javascript file.
Now, when/how do you want the alert to be called? If I understand correctly what you're trying to do, here's how to do it:
<div ng-click="doAlert()">
Click here to see an alert
</div>
and in your controller:
$scope.doAlert = function() {
alert('hi);
};

AngularJS - Run custom directive after ng-bind-html

I've a scenario which I want to run a custom directive on the DOM that ng-bind-htmlcreate.
Basicly I've to customize the behavior of the html tag <a> because the html that is loading is unsafe and when the user click the link I've to execute some functions before anything happens, aka, the click event of jqLite.
So my original ideia was create a directive that modifies the behavior of any <a> inside the container that I put the attribute of my directive.
This works fine, until I combine with ng-bind-html, my directive runs before the ng-bind-html compile the string into html and attached to the DOM.
So, is any way to run my custom directive after the ng-bind-html run?
DEMO
HTML:
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-bind="sometext" my-directive>before</div>
</div>
Controller:
angular.module('myApp', []);
angular.module('myApp').controller('myCtrl', function($scope) {
$scope.sometext="stuff here";
});
Directive:
angular.module('myApp').directive('myDirective', function() {
return {
priority: 10, // adjust this value ;)
link: function(scope,element,attrs) {
scope.$watch(attrs.ngBind, function(newvalue) {
console.log("element ",element.text());
});
}
};
});
Use priority property inside directive to run your code after mg-bind

Call Javascript After Directive Renders DOM for ShareThis

In an Angular (1.3) app, I am displaying list of records using ng-repeat. There is a directive with a template inside the ng-repeat. Within the template I'm using ShareThis controls which are activated after the DOM is loaded.
On initial load of the app, the ShareThis Javascript works correctly and activates the buttons. On route change it does not activate. I've found to references to activate the controls manually via stButtons.makeButtons() or stButtons.locateElements();, but I'm unsure where to call this function in the directive or page cycle. I've tried within:
the directive link function - using $timeout or scope.$watch
the template <script>stButtons.locateElements();</script> - activates before model binding
the controller after binding - activates before DOM rendered
My understanding is the function to activate needs to be called after binding and after DOM rendering, but Angular does not know when the DOM is ready. There is a method to dynamically render the ShareThis controls using only Javascript, but I want the HTML defined in the template not Javascript for this case.
I've seen several questions out there related, but none of the answers seem to work 100% for my scenario (and many are broken as of Angular 1.3).
item-list.html (view)
<div ng-repeat="item in vm.itemList">
<item-directive item="item"></item-directive>
</div>
item-list.cs (controller)
{ ... vm.itemList = getItems(...) ... }
item-directive.js (directive)
(function () {
angular.module('app');
function itemDirective() {
var directive = { templateUrl: 'item.html', link: linkFunc, controller: ItemDirective };
return directive;
function linkFunc(scope, element, attr, ctrl) { var item = scope.item }
}
ItemDirective.$inject = ['$scope'];
function ItemDirective($scope) { ... }
}
item.html (directive template)
...
<div class="item-share-section">
<span class='st_sharethis_large' st_url="{{vm.item.url}}" st_title="{{vm.item.name}}"></span>
</div>
...
if I understood well, you want to call the function after the dom is completely render, right? Try this inside the postLink of your directive:
$scope.$watch('viewContentLoaded', stButtons.locateElements())
While my solution is a little hackish, I still prefer it over using $watch, since that is inefficient. Instead, I initialize the function which loads the buttons when the particular view you want to load the buttons with is rendered. The technique is as follows:
Here is the function which you should put in your controller:
$scope.loadShareThis = function() {
stButtons.makeButtons();
}
You'd then add to your item-list.html as such:
<div ng-repeat="item in vm.itemList" ng-init="loadShareThis()">
<item-directive item="item"></item-directive>
</div>
The dynamic URL's might give you additional problems, but that's another issue all together.

Angular directive breaks scope?

First off, I'm new to Angular, and realize that I may be missing a core concept...
Consider the jsfiddle: http://jsfiddle.net/D4dFv/
I'd like to be able to click on each link, and see the {{driveState.currentView}} update in the DOM.
Everything works fine until I add in a directive that helps me detect when all images on the page have loaded successfully. With that directive in place, the binding appears to break, and you can no longer click on each link and see driveState.currentView update.
Why is this?
To test this in the jsfiddle, note that the following works fine:
<img width='10' height='10' src='http://www.w3schools.com/images/pulpit.jpg'>
...and this breaks the data binding somehow:
<img imageonload width='10' height='10' src='http://www.w3schools.com/images/pulpit.jpg'>
Thanks in advance.
The reason is that the Directive is defining its own controller. This makes a new instance of the controller class and is somehow messing up the scope.
To fix, take out the controller: 'Ctrl', in the Directive definition.
Here is the new Directive code:
myApp.directive('imageonload', function () {
return {
restrict: 'A',
link: function ($scope, element) {
element.bind('load', function () {
_viewsLoaded++;
if (_viewsLoaded === $scope.appViews.length) {
alert('init layout here');
}
});
}
};
});
And an updated fiddle for you.

Categories