Why are mouse events from my directive not reflected in the view? - angularjs

Here is an example fiddle. Please open the console first.
It's a little long, so to quickly explain: You'll see two divs. One is "regular", the other is created via a directive. They both have click event handlers (added via ng-click).
Clicking the regular (non-directive) div does what I expect - the appropriate variables in the scope are set and you see that in the view (top two lines in the output show the id and top of the div that was clicked).
The problem is when clicking the directive div. These variables are not updated. I know I'm misunderstanding something about the scope of the directive, but what is confusing me is the log messages that are printed (open the console when you do this). The clicks are registered, and the controller's scope does set the variable values, as you can see in the console.
Yet the html does not update - try clicking one div, then the other, and go back and forth, and you'll see.
What am I missing here?
Code (please don't be put off by its length!):
<div ng-app="MyApp">
<div ng-controller="MyController">
Top of the div you just clicked = {{top}}.<br/>
Id of the div you just clicked = {{lastId}}.<br/>
<div id="notADirective" class="myClass" ng-click="update($event)">
Not a directive. Click me.
</div>
<my-div element-id="isADirective" cls="myClass">
I'm a directive. Click me.
</my-div>
</div>
angular.module('components', [])
.controller('MyController', function ($scope) {
$scope.lastId = '';
$scope.top = '';
$scope.update = function (event) {
var myDiv = getFirstMyClassAncestor(event.target);
var style = window.getComputedStyle(myDiv);
$scope.top = style.top;
$scope.lastId = myDiv.id;
console.log("You just clicked " + $scope.lastId + " and its top is " + $scope.top);
}
function getFirstMyClassAncestor(element) {
while (element.className.indexOf('myClass') < 0) {
element = element.parentNode;
}
return element;
}
}).directive('myDiv', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
controller: 'MyController',
scope: {
cls: '#',
elementId: '#'
},
template: '<div id="{{elementId}}" class="{{cls}}" ng-click="update($event)"><div ng-transclude></div></div>'
}
});
angular.module('MyApp', ['components'])
.myClass {
background-color: #DA0000;
position: absolute;
}
#isADirective {
top: 300px;
}
#notADirective {
top: 100px;
}

In case of your directive the assigned controller MyController gets the scope of this directive and not the scope of div as you probably expect. I added a log statement for the different scope ids:
http://jsfiddle.net/6zbKP/4/
If you want to update the outer or parent scope you have to use a binding like this:
scope: {
cls: '#',
elementId: '#',
callback: '='
},
Then bind your update function in the directive:
<my-div element-id="isADirective" cls="myClass" callback="update">
I'm a directive. Click me.
</my-div>
And call the callback in your directive:
template: '<div id="{{elementId}}" class="{{cls}}" ng-click="callback($event)"><div ng-transclude></div></div>'
See http://jsfiddle.net/6zbKP/5/

The angular expression bindings are outside of your directive's isolated scope. And you haven't imported lastId or top from your parent controller scope into your directives isolated scope, so there is no reason to expect that updating one variable in the inner directive scope will update the variable in the outer controller scope. There is no two way binding set up.
You can setup two way binding however using scope: {lastId: '=', top: '=' }.
Also you shouldn't be sharing controllers like you're doing. I suggest using anonymous controller functions.

Related

How to get modified values from controller to custom directive?

I have defined one variable in controller and i have assigned this value to one attribute of custom directive. So on the basis of this value i am showing the modal box template. Now if i click on the cancel button from modal box template then it calls one function from controller which is modifying the variable value to false but it is not hiding the popup box. Please help me to fix it.
(function () {
'use strict';
angular.module('module1').directive('myDirective', function () {
function linkFunction(scope, elem, attrs) {
//scope.openvalue = attrs.openvalue;
scope.closevalue = false;
scope.close = function () {
console.log("Inside Close");
scope.openvalue = false;
scope.closevalue = false;
};
};
return {
templateUrl: 'confirmTemplate.html',
restrict: 'E',
link: linkFunction,
scope: {
confirmtext: '#',
openvalue: '=',
closeconfirm: '&',
submitconfirm: '&'
},
controller: ['$scope', function ($scope) {
$scope.$watch('openvalue', function () {
console.log("OpenValue : " + $scope.openvalue);
});
}]
};
});
})();
Following is the html for opening this modal.
<div class="col-xs-12 options" ng-click="cntrl.flag1 = true">
<div class="row">
<myDirective openvalue="cntrl.flag1" confirmtext="This is the text from directive"
closeconfirm="cntrl.closeconfirm()" submitconfirm="cntrl.submitconfirm()"></myDirective>
<div class="col-xs-9 no-left-right-padding">My text</div>
</div>
</div>
And i want the updated value of openvalue inside html template but it is not working.
It would be more clear to have you codes here, but I think the problem is when you call the function from controller, it doesn't actually modify the variable of controller scope but modal's scope.
In AngularJS scope, any change of inherited variable in child scope will create a local version.
Based on your words, when you open a modal window it will create a new child scope and when you call the function from controller to modify that scope variable, you actually modifying that child scope variable not controller's.
You can simply add console.log($scope.$id); in controller and the function then you should be able to see the scope id is different.
This Fiddle will give you the idea, press Esc key to close the modal window. However, as I said it would be better to have your code to address exact issue.
Based on your code, a quick fix is assign the cntrl object into directive which will make sure your directive refer to the same object.
Change your modal to
<myDirective cntrl="cntrl" confirmtext="This is the text from directive"></myDirective>
in your directive
scope: {
confirmtext : '#',
cntrl : '='
},
in your linkFunction
function linkFunction(scope, elem, attrs){
scope.close = function(){
scope.cntrl.flag1 = false;
}
you still can access closeconfirm and submitconfirm by $scope.cntrl.closeconfirm and $scope.cntrl.submitconfirm respectively.

Angular directive injecting DOM element with click event

Content edited
The goal is to create a directive that can be attached to a textbox that, when the textbox has focus, an image/button will appear after the textbox and the image/button click event will fire a function contained within the directive. The goal is for this functionality to be entirely self-contained in the directive so it can be easily deployed in many pages or apps.
The image/button appears after the textbox with no problem but the click event of the button does not fire the function. I have created a plunkr with the example code.
In the plunk, line 15 defines a function called 'search,' which does nothing more than fire an alert. When the textbox has focus, the button appears as expected and line 34 calls the search function successfully, which means the function itself is working. However, the button's click event doesn't fire the search function.
Original post
I'm trying to recreate some functionality in our apps that is currently being accomplished with jQuery. The functionality involves attaching a pseudo-class to a textbox which is then picked up by jQuery and an image of a magnifying glass is injected into the DOM immediately after the textbox. Clicking on the image causes a dialog box to pop open.
What I've accomplished so far is a simple html page, a simple controller, and a simple directive. When the textbox has focus, the image appears as expected. However, the ng-click directive does not fire.
Here's the html:
<input
id="txtAlias"
type="text"
ng-model="pc.results"
user-search />
</div>
Here is the controller:
angular
.module('app')
.controller('PeopleController', PeopleController);
PeopleController.$inject = ['$http'];
function PeopleController() {
var pc = this;
pc.results = '';
pc.search = function () {
alert('test');
};
}
And this is the directive:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile) {
return {
restrict: 'EAC',
require: 'ngModel',
//transclude: true,
scope: {
//search : function(callerid){
// alert(callerid);
//}
},
template: "The user's alias is: <b><span ng-bind='pc.results'></span>.",
//controller: UserSearchController,
link: function (scope, element, attrs) {
element.bind('focus', function () {
//alert(attrs.id + ' || ' + attrs.userSearch);
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="pc.search("' + attrs.id + '")" ' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
});
}
};
};
For the time being, I'd be happy to get an alert to fire by either hitting pc.search in the controller or by search in the isolated scope. So far, neither has worked. I'm sure it's something simple that's missing but I can't figure out what.
Solution
Thanks to a user over at the Google forum for showing me the controllerAs property for directives. This version now works perfectly:
angular
.module('app')
.directive('userSearch', userSearch);
function userSearch($compile){
return {
controller: function ()
{
this.search = function () {
alert('Test');
};
},
link: function (scope, element, attrs) {
element.bind('focus', function () {
var nextElement = element.parent().find('.openuserdialog').length;
if (nextElement === 0) {
var btn = '<img src="' + homePath + 'Images/zoomHS.png" ' +
'ng-click="userSearch.search()" ' +
'class="openuserdialog" />';
element.after($compile(btn)(scope));
}
});
},
controllerAs: 'userSearch'
};
};
You are using isolated scope in your directive which means it don't have access to its parent scope. So in this case you need to pass your method reference explicitly to directive. Passed method reference to your directive scope by new variable inside a isolated scope of directive.
Markup
<input id="txtAlias"
type="text" ng-model="pc.results"
user-search search="search(id)" />
scope: {
search: '&'
}
As you don't have access to parent scope, you can't use controller alias over there like you are using pc.. Simply do use following without alias. So directive will bind those variables from directive scope directly.
template: "The user's alias is: <b><span ng-bind='results'></span>.",
Also change compile template to
if (nextElement == 0) {
var magnifyingglass = $compile('<img src="' + homePath + 'Images/zoomHS.png" ' +
'alt="User Search" ' +
'ng-click="search({id: ' + attrs.id + '})' +
'class="openuserdialog">')(scope);
element.after(magnifyingglass);
}
Rather I'd love to have the compiled template as part of template of directive function only. And I'll show and hide it based on ng-if="expression" directive.
Relative answer
Rather than trying to inject into the DOM, and then trying to hook up to that thing you just injected, wrap both the input and the search button/icon in a directive. You can use an isolated scope and two-way binding to hook up both the input and the button:
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<search-box data="name" search-function="search"></search-box>
</body>
Here's both a controller and a directive that demonstrate this. Note the "=" in the isolated scope, creating a two-way binding to the corresponding attributes when the directive is used in a template.
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.search= function() { alert('search clicked'); }
});
app.directive('searchBox', function() {
return {
restrict: 'E',
scope: {
searchFunction: '=',
data: '=',
},
template: '<input ng-model="data" /><button ng-click="searchFunction()">Search</button>'
}
})
You should be able to easily replace the button element with an img or whatever else your heart desires.
Here's a plunk with an alert() for the search box, and where typing in the text box in the directive affects the corresponding property of the controller scope:
http://plnkr.co/edit/0lj4AmjOgwNZ2DJMSHDj

AngularJS directive: Produce different HTML depending on $scope variable

I just started using AngularJS and immediately ran into a problem:
I have a sidebar which contains "action-buttons" - depending on the currently active view, different buttons should be visible.
My view-controller defines an object which looks as follows:
$scope.sidebar.actionButtons = [
{ icon: "plus", label: "Add", enabled: true, url: "customer.new" },
{ icon: "minus", label: "Delete", enabled: false, action: function(){ alert("Not implemented yet"); }}
];
As you can see, there are two different kinds of action-buttons: Either the button changes to another view (url is set to customer.new), or the button triggers an arbitrary function (action is set to alert()).
Each button type has to generate some slightly different html, and I'm not able to get this working.
After playing around for several hours, here is my current (not-working) approach:
My sidebar uses the following template-code to generate the buttons:
<ul class="nav" id="sidebar-action-buttons">
<action-button ng-repeat="button in actionButtons" button="button"/>
</ul>
Now, the actionButton directive has everything it needs and should produce the html depending on the button type:
angular.module('myApp')
.directive('actionButton', function($compile) {
function linker($scope, $element, $attrs){
var innerHtml = '';
$element.attr('ng-class', '{disabled: !button.enabled}');
if($scope.button.url) {
$element.attr('ui-sref-active', 'active')
innerHtml = '<a ui-sref="{{button.url}}">';
} else {
innerHtml = '<a ng-click="button.action()">';
}
innerHtml += '{{button.label}}</a>';
$element.html(innerHtml).show();
$compile($element.contents())($scope);
}
return {
restrict: 'E',
replace: true,
scope: { button: "=" },
link: linker,
template: "<li></li>"
}
});
This generates the correct content. The problem here is, that the attributes which are placed on the actionButton element (in this case ng-class='{disabled: !button.enabled}') are not compiled.
How can a directive produce different html depending on scope variables? What is the correct approach for doing this? How can I also compile the newly added attributes?
By the time the ng-class is added to the action-button element, the digest is over with for that element. You could call $scope.$apply(), but I would add the ng-class to each anchor element instead, then there would be no need to call $scope.$apply() again.
Because you are compiling content() of the li but ng-class has been added with li itself. Simple solution is to add ng-class directly with the action-button directive i.e.
<action-button ng-repeat="button in actionButtons" button="button" ng-class="{disabled: !button.enabled}" />

Controller must wait until Directive is loaded

I'm currently writing a small set of AngularJS controls, and I'm encountering an issue here.
The control which I'm creating is a button (let's start simple).
The directive looks like this:
officeButton.directive('officeButton', function() {
return {
restrict: 'E',
replace: false,
scope: {
isDefault: '#',
isDisabled: '#',
control: '=',
label: '#'
},
template: '<div class="button-wrapper" data-ng-click="onClick()">' +
'<a href="#" class="button normal-button">' +
'<span>{{label}}</span>' +
'</a>' +
'</div>',
controller: ['$scope', function($scope) {
// Controller code removed for clarity.
}],
link: function(scope, element, attributes, controller) {
// Link code removed for clarity.
}
}
});
I've removed some code because it will make my question very hard to understand and I don't believe it's needed here.
Inside my controller of my directive, I'm writing an API and that being done by executing the following code:
controller: ['$scope', function($scope) {
// Allows an API on the directive.
$scope.api = $scope.control || {};
$scope.api.changeLabel = function(label)
$scope.label = label;
}
}]
So, on my directive, I do have an isolated scope to which I can pass a control property. Through this property, I'll have access to the method changeLabel.
My control can be rendered by using the following directive in the HTML website:
<office-button control="buttonController"></office-button>
The controller on my application will looks like:
officeWebControlsApplication.controller('OfficeWebControlsController', ['$scope', function($scope) {
$scope.buttonController = {};
}]);
I can execute my changeLabel method right now by calling the following method in my scope
$scope.buttonController.changeLabel('Demo');
However, this doesn't work since at this point changeLabel is not defined. The button must first be completely rendered.
Any idea on how to resolve this particular issue?
Edit: Plunker Added
I've added a plunker to provide more information.
When the changeLabel method is called within the onClick event, then everything is working, but if I call it outside, it isn't working anymore. This is because the $scope.api.changeLabel is not defined at the moment of execution (button is not rendered yet). So I basically have to wait in my application's controller until the button is fully rendered.
Kind regards

Why does my nested directive disconnect from its ngModel as soon as $setViewValue() is called?

Plunker here.
I have a directive ("child") nested inside another directive ("parent"). It requires ngModel, and ngModelCtrl.$modelValue is shown and kept up-to-date just fine in its template. That is, until I call ngModelCtrl.$setViewValue().
So here is the HTML initialising the directives:
<div parent>
<div child ng-model="content">Some</div>
</div>
And here are the directives:
angular.module('form-example2', [])
.controller('MainCtrl', function($scope){
$scope.content = 'Hi';
})
.directive('parent', function() {
return {
transclude: true,
template: '<div ng-transclude></div>',
controller: function(){
},
scope: {}
};
})
.directive('child', function() {
return {
require: ['ngModel', '^parent'],
transclude: true,
template: '<div>Model: {{model.$modelValue}} (<a style="text-decoration: underline; cursor: pointer;" ng-click="alter()">Alter</a>)<br />Contents: <div style="background: grey" ng-transclude></div></div>',
scope: {},
link: function(scope, elm, attrs, ctrl) {
var ngModelCtrl = ctrl[0];
var parentCtrl = ctrl[1];
scope.model = ngModelCtrl;
// view -> model
scope.alter = function(){
ngModelCtrl.$setViewValue('Hi2');
}
// model -> view
// load init value from DOM
}
};
});
When the model (i.e. content) changes, this change can be seen inside the child directive. When you click the "Alter" link (which triggers a call of $setViewValue()), the model's value should become "Hi2". This is correctly displayed inside the child directive, but not in the model outside the directive. Furthermore, when I now update the model outside the directive, it is no longer updated inside the directive.
How come?
The directives ended up being just fine; the only problem was that the passed model should be an object property. Hence, the directives work if the following modifications are made to the calling code (Plunker):
In the controller, instead of $scope.content = 'Hi';:
$scope.content = {
value: 'Hi'
};
In the template, replace all references to content with content.value:
<input ng-model="content.value" type="text" />
<div parent>
<div child ng-model="content.value">Some</div>
</div>
<pre>model = {{content.value}}</pre>
The reason this works, roughly, is that when Angular passes the reference to the model to the transcluded scope of the parent directive (i.e. the one the child is in), this is only a reference when it refers to an object property - otherwise it is a copy, which Angular cannot watch for changes.
#Josep's answer helped greatly so, even though it did not provide the actual solution, if you're reading this and it's useful, give it a vote :)

Resources