Angular JS directives with isolate scopes and internal controllers - angularjs

I haven't touched angular js in a while and back when I did write, we used a flavour with typescript which was pretty straightforward with me. Now I want to write vanilla angular js and I feel I am a bit confused.
Problem:
I have a directive with a few variables in its isolate scope and I basically want to bind to this directive that is spawned inside a for each <ul> to a click event. I tried with directly binding a function on ng-click and with link element e.t.c. bind on click, but it seems I am doing something wrong since with the first way nothing happens, with the second way the two-way bound variable is undefined.
Here it goes:
https://plnkr.co/edit/OOBMs8pYONLjUE9lQXla?p=preview
activity-header.html
<div>
<h4>
Activity Name: {{activity.activity_name}}
</h4>
<h6>
Activity Start Date: {{activity.activity_start_date}}
</h6>
<h6>
Activity End Date: {{activity.activity_end_date}}
</h6>
<h6>
Participants: {{activity.participants}}
</h6>
</div>
activity-header.js
var app = angular.module('mainApp');
/*
app.controller('activityHeaderCtrl', ['$scope', function($scope) {
$scope.activity='';
$scope.msg='';
$scope.check = function() {
alert($scope.msg);
};
}]);
*/
app.directive('activityHeader', function() {
return {
restrict: 'AE',
templateUrl: 'activity-header.html',
controller: ['$scope', Controller],
scope: {
activity:'=',
msg:'='
},
link: function($scope, $element, attrs) {
$element.bind('click', function($scope) {
alert($scope.msg);
})}
};
function Controller($scope) {
$scope.check = function() {
alert($scope.msg);
};
}
});
index.html
<html ng-app="mainApp">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<script src="script-main.js"></script>
<script src="activity-header.js"></script>
<body>
<div ng-controller="ctrl">
<h1>
Major Bla bla System
</h1>
<ul>
<li ng-repeat="x in events">
<div activity-header activity="x" msg="greetingsfriend" ng-click="check()"></div>
</li>
</ul>
<h6>
Beta v.0.2
</h6>
</div>
</body>
</html>
script-main.js
var app = angular.module('mainApp', []);
app.controller('ctrl', function ($scope) {
//$scope.events = ["Elections", "Protest", "Martial Law", "X-mas Celebration"];
$scope.events = [
{"activity_name": "Elections", "activity_start_date": "31/12/2014", "activity_end_date": "31/12/2015", "participants": "1453"},
{"activity_name": "Martial Law", "activity_start_date": "31/12/2014", "activity_end_date": "31/12/2015", "participants": "1821"},
{"activity_name": "Protest", "activity_start_date": "31/12/2014", "activity_end_date": "31/12/2015", "participants": "1940"},
{"activity_name": "X-mas Celebration", "activity_start_date": "31/12/2014", "activity_end_date": "31/12/2015", "participants": "2009"}
];
$scope.salute = function () {
alert('hello there');
};
});
(By the way, I'm using Mozilla Firefox, otherwise I'd have to host it e.g. on node.js for the same origin policy, don't know how to turn it off in chrome/ internet explorer).

depends on what you want to obtain.
For example, if you want to bind a function of your directive on click you don't need the link function. You can simply bind a click on your outer div with ng-click. See this example: http://jsbin.com/sanova/edit?html,js,output
But if you want to call a function on your parent controller you need to pass a reference to that function with a property on your directive. See this example: http://jsbin.com/peqasu/edit?html,js,output
As you can see i've put in both example a ng-click directive on the outer div in your directive template. On click the check function on the directive controller is invoked. In the first example simply alert the message, in the second one calls the greetFunction passed as a property of your directive.

Any idea on how to handle the click?
Your problem is that you are using $scope as the name for the first argument in the click handler function. That name is overriding the $scope argument from the linking function.
//BAD
link: function($scope, $element, attrs) {
$element.bind('click', function($scope) {
alert($scope.msg);
})}
Fix your code like this:
//GOOD
link: function(scope, element, attrs) {
element.on('click', function clickHandler(event) {
console.log(scope.msg);
});
}
AngularJS jqLite invokes the click handler with a JQuery Event Object as the first argument not $scope.
Is this the correct way to handle events on directives?
Why doesn't the ng-click event ever work to call the function I have in my directive's controller?
The ng-click directive binds to functions in the parent scope.
To bind click events to functions on the directive's scope, use element.on(). It's how ng-click gets the event from the browser. Look at the source code.

First of all you need to know the difference between smart and dumb components (directives).
Here you can read a very good article explain the difference from smart and dumb components written by Dan Abramov, the creator of Redux: https://medium.com/#dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
When you understand this difference you can try to write more dumb components and less smart one. This means that you need to keep all the logic on your parents components and pass it down to your dumb components.
In previous examples we do that only in the second one. In fact, in that example we keep the logic (our check function) in our parent component and only pass a reference to it. In this way the activity-header component have no idea of what to do when a click is done. It only know that it must call a function, what this function does is not its problem.
This is a good approach to have in a complex application, so you can reuse your components in different ways simply changing the reference function.

Related

How to bind ng-click to a custom directive and call a parent function?

We are using Angular 1.4.2 and I am trying to take a count value from a directive using ng-click, pass it to a function, then pass it up to the parent controller. After some effort it is working in a plunker, but unfortunately when I tried to move this functionality back into the main code, I'm not able to get a controller to bind to the isolated scope.
Should be simple, but I've tried injecting the current controller into the directive and trying to create a new controller, but nothing happens when I press click on the button.
Here is the code:
TEMPLATE:
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#1.4.2" data-semver="1.4.2" src="https://code.angularjs.org/1.4.2/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<div id="app" ng-app="app">
<div ng-controller="mainCtrl">
<my-directive ctrl-fn="ctrlFn(count = count + 10)"></my-directive>
</div>
</div>
</body>
</html>
SCRIPT:
var app = angular.module('app', []);
app.controller('mainCtrl', function($scope){
$scope.count = 0;
$scope.ctrlFn = function() {
console.log('In mainCtrl ctrlFn!');
//$scope.count += 10; Old hardcoded value.
console.log("count is: " + JSON.stringify($scope.count));
//Call service here
};
});
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
'ctrlFn' : '&'
},
template: "<div><button ng-click='ctrlFn()'>Click Here</button></div>",
link: function(scope, element, attributes) {
scope.ctrlFn(count);
}
};
});
Here is the template code I'm trying to modify in the main code base:
<div>
<div layout="row">
<results-loader ctrl-fn="ctrlFn(count = count + 10)"></results-loader>
<md-button class="md-raised md-primary md-button" ng-click="ctrlFn()" flex>Click Me</md-button>
</div>
</div>
and here is where I use an existing controller in my directive as a parent controller. It's defined in a route, rather than ng-controller and is already used for this view.
myresultsCtrl.$inject = ['$scope', ' myService'];
/* #ngInject */
function myresultsCtrl($scope, myService) {
$scope.count = 0;
etc...
however, it apparently isn't bound properly as I never hit the directive or this function with ng-click.
Like I said I tried adding a new controller to the template, then I tried injecting an existing controller into the directive, but neither worked. I took out the template from the directive and tried to put the ng-click directly into the template with ctrl-fn, but I wasn't sure how to wire up the click with the call to the ctrl-fn attribute of the directive with both in the template? The idea here is to move the template into it's own html file and reference it from the directive, as in: template: "myFile.html. I'm trying to enscapsulate as much as possible to make this into a reusable component.
I haven't worked much with custom directives.
Here is the direct link to the plunker.
I don't know your exact assignment, but rethink your architecture. Why do you want to count the value in the directive? In your simple case it would be better to count the value in a service or in the controller, not a directive.
Injecting controller in the directive is anti-angular pattern. You need to rethink your intentions. It is better to pass the number to the directive, make your calculations there and send your number back to the controller. It would work without extra code, because of the two-way data binding. Here a fork of your plunker:
http://plnkr.co/edit/KpB6bv5tHvXSvhErLcEp?p=preview
Main part is the definiton of the directive:
<div><span>{{count}}</span><br /><button ng-click='myFunction()'>Calculate</button></div>
I prefer not to answer my own questions as that can have a negative appearance, but in this case, I don't know if I could have explained the question well enough to get the right answer. What was throwing me off was integrating my working plunker code into our existing code base.
Where I got stuck was how to setup the controller properly for this use case. Though I declared my controller, in the module and injected the controller as it was already bound to that view, I needed to define the controller in the directive itself. Once I did all three together, everything started working.
Here are some code snippets:
angular.module('directiveName', [])
.directive('directiveName', directiveName)
.controller('injectedCtrl', injectedCtrl)
etc...
var directive = {
restrict: 'E',
scope: {
'ctrlFn' : '&'
},
template: "<div><button ng-click='ctrlFn()'>Click Here</button></div>",
controller: "injectedCtrl",
link: function(scope, element, attributes) {
scope.ctrlFn(); //This will pass to the parent controller: injectedCtrl, where $scope resides.
}
}
return directive;
}
injectedCtrl.$inject = ['$scope', 'myService'];
/* #ngInject */
function injectedCtrl($scope, myService) {
$scope.ctrlFn = function() {
//do something
}
etc...
HTML CODE:
When the button is clicked from the directive, ctrlFn here is a reference that is under my directives isolated scope in the DDO. This takes the external function that was passed in, namely: ctrlFn() which can then be invoked from the directive to call the parent controller.
<div layout="row">
<results-loader ctrlFn="ctrlFn()"></results-loader>
</div>
I'm posting this to help someone else that might have this use case as it was not easy to figure this out.
Dah Wahlins post goes into this subject in greater detail: Creating Custom AngularJS Directives Part 3 - Isolate Scope and Function Parameters and what helped to get my thinking straightened out.

Angular script won't run when first loaded

On page load the console log prints but the toggleClass/click won't work I even use angular.element but it has the same result.I need to change state in order for the toggleClass to work.I dunno what's wrong in my code.
.run(['$rootScope', function ($rootScope) {
console.log('test');//this prints test and it's ok
//this part won't load at the first loading of page.
$('.toggle-mobile').click(function(){
$('.menu-mobile').toggle();
$(this).toggleClass('toggle-click');
});
//....
}])
even doing it this way doesn't work.
$rootScope.$on('$viewContentLoaded', function () {
angular.element('.toggle-mobile').on('click', function (event) {
angular.element(this).toggleClass('toggle-click');
angular.element('.menu-mobile').toggle();
event.preventDefault();
});
});
The Angular way to render items is different from "On DOM Ready" that is why we need to treat these as 2 separate things.
Angular could render items later on even after DOM is ready, this could happen for example if there is an AJAX call($http.get) and that is why a directive may be the recommended approach.
Try something like this:
<body ng-controller="MainCtrl">
<div toggle-Me="" class="toggle-mobile"> Sample <div class="menu-mobile">Sample 2</div>
</div>
<script>
var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {}]);
myApp.directive("toggleMe", function() {
return {
restrict: "A", //A - means attribute
link: function(scope, element, attrs, ngModelCtrl) {
$(element).click(function(){
$('.menu-mobile').toggle();
$(this).toggleClass('toggle-click');
});
}
};
});
...
By declaring the directive myApp.directive("toggleMe",... as an attribute toggle-Me="" every time angular generates the input element it will execute the link function in the directive.
Disclaimer: Since the post lacks from a sample html I made up something to give an idea how to implement the solution but of course the suggested html is not part of the solution.

Angular ng-init pass element to scope

Is there a way to get the current element where my ng-init is currently binded on?
For example:
<div ng-init="doSomething($element)"></div>
I believe that there is a way for ng-click but I can't do this using ng-init like this:
<div ng-click="doSomething($event)"></div>
Controller:
$scope.doSomething = function(e){
var element = angular.element(e.srcElement);
}
How do I do this with ng-init?
Your HTML:
<div ng-app='app' ng-controller="Ctrl">
<div my-dir></div>
</div>
Your Javascript:
var app = angular.module('app', [], function () {});
app.controller('Ctrl', function ($scope) {
$scope.doSomething = function (e) {
alert(e);
};
});
app.directive('myDir', function () {
return function (scope, element, attrs) {
scope.doSomething(element);
};
});
From above, element will be your DOM object.
Don't do it.
As #CodeHater said, handling DOM manipulation in a directive is a better solution than engaging controller with the element.
I was also looking for similar thing but finally I created one more directive added to the child div element. In the directive code block I get the element object and placed all my event related function and other instructions over there. Also, I add the element to $scope object, this help me to use this object else where as well and no need to find it every time I need it.

ng-click stops working after the first use of $compile when using nested directives

I have an Angular modal directive that uses a helper/wrapper directive. This way I can always use the same wrapper and just load a different template where needed for different modal content.
PROBLEM: This snippet works, but only for the first life cycle of the modal. So I can fire the modal, close the modal and fire it again. But once the modal is open the second time none of the ng-click directives work. Any tips would be just super.
Usage
<button my-modal="views/login.html">Launch Login-specific Modal</button>
Directive Module (app.js)
angular.module('myModal',[])
.directive('modalWrapper', function(){
return {
replace: true,
templateUrl: 'views/modal.html',
controller: function($scope, $element){
$scope.close = function(){
$element.remove();
};
// NOTE: I use this array to showcase that ng-repeat still works the second time although ng-click stops functioning properly.
$scope.others = ["One", "Two", "Three"];
}
}
})
.directive('myModal', function( $compile){
function link(scope, element, attr){
scope.partial = attr.myModal; // NOTE: Loads sub template via ng-include
var ngModal = $compile('<div modal-wrapper></div>')(scope);
element.on('click', function(){
angular.element('body').append(ngModal);
});
scope.yo = function(){
alert("Yo from inside template.");
};
}
return {
link: link,
scope: {}
}
});
Templates
modal.html
<div class="my-modal">
<p>Modal Wrapper</p>
<div ng-include="partial"></div>
<button ng-click="close()">Close</button>
<p>This just proves that other directives still work (ng-repeat), but ng-click does not.</p>
<div ng-repeat="stuff in others">
<p>{{stuff}}</p>
</div>
</div>
login.html
<h1>Well hey there, I'm the login template.</h1>
<button ng-click="yo()">Say Yo</button>
I think the problem is that you are destroying the scope on which the ng-click is compiled.
When scope.close() is called, an $element.remove() occurs. This both removes the element from the DOM, and destroys the scope to which it is attached. This will result in your ng-click being de-registered.
Unfortunately (as of last time I checked), element.detach() also destroys scope, so your best bet is to compile and append the element to body only once. After this you can use element.show() and element.hide() to show and hide the modal. Alternatively you can recompile the modal each time you want to show it.

Bind and parse HTML content

I am using AngularJS v1.2.1.
The improved ng-bind-html directive allows me to trust unsafe Html into my view.
Example
HTML:
<div ng-repeat="example in examples" ng-bind-html="example.content()"></div>
JS:
function controller($scope, $sce)
{
function ex()
{
this.click = function ()
{
alert("clicked");
}
this.content() = function ()
{
//if
return $sce.trustAsHtml('<button ng-click="click()">some text</button>');
// no problem, but click is not called
//when
return $sce.parseAsHtml('<button ng-click="click()">some text</button>');
//throw an error
}
}
$scope.examples = [new ex(), new ex()];
}
My question is, how to bind HTML content that may contain Angular expressions or directives ??
If you need dynamic templates per element, as your question suggests, one solution would be to use $compile within a directive to parse the HTML within the context of the local scope. A simple version of this is shown in this Plunk.
An example directive:
app.directive('customContent', function($compile) {
return function(scope, el, attrs) {
el.replaceWith($compile(scope.example.content)(scope));
}
});
The corresponding HTML:
<div ng-repeat="example in examples">
<div custom-content></div>
</div>
Notice that, in the Plunk controller, I've pulled out the click function into the scope for simplicity, since in the template HTML you are calling click() in the context of the scope, not on the example object. There are a couple ways you could use a different click function for each example, if that's what you'd like to do. This egghead.io screencast has a good example of passing an expression into a directive explicitly; in your case, it could be a click function or the whole example object, depending on what you need.

Resources