Controller must wait until Directive is loaded - angularjs

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

Related

Angular event or command that signals the end of processing child directives

I'm currently coding a tab navigation example to gain practical experience with Angular. This example uses custom directives and controller inheritance.
Plunker can be found here.
The issue: once the directives have finished processing I'd like to select a default tab to display. But at the point of calling the selectTab method of the myTabs controller (line 41 in script.js $ctrl.selectTab(0)), Angular hasn't yet finished processing the myTab directive (which generates the tab links), so the tabs array is empty and the selection fails.
I tried using $timeout without a delay, but that fails. $timeout only works with a delay of say 500ms set, which is hacky.
Is there an event or command available that signals the end of Angular processing certain directives, particularly directives that inherit from one another?
What I suspect is happening:
The myTabs directive finishes processing then fires its link function, but it's fired before the myTab directive is done processing. I can't place the selectTab method call within the myTab link function because would get called multiple times (based on the number of tabs processed.) Hope I'm explaining this clearly...I need a massage
app.directive('myTabs', ['$timeout', function($timeout) {
return {
restrict: 'E',
transclude: true,
controllerAs: 'myTabsCtrl',
templateUrl: 'my-tabs.htm',
scope: {},
controller: function ($scope) {
vm = this;
this.tabs = [];
this.addTab = function (tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab (tabIndex) {
for (var i = 0; i < this.tabs.length; i++) {
this.tabs[i].selected = (i === tabIndex ? true : false);
}
};
},
link: function ($scope, $element, $attrs, $ctrl) {
$timeout(function () {
$ctrl.selectTab(0);
});
}
};
}]);
app.directive('myTab', function() {
return {
restrict: 'E',
require: '^^myTabs',
transclude: true,
templateUrl: 'my-tab.htm',
scope: {
title: '#'
},
link: function (scope, element, attrs, ctrl){
scope.tab = {
title: scope.title,
selected: false
};
ctrl.addTab(scope.tab);
}
};
});
Each individual tab is registering itself with the parent directive controller. Simply set the selected flag on the first tab when it registers itself:
this.tabs = [];
this.addTab = function (tab) {
console.log("add tab ", tab);
if (!this.tabs.length) {
//Set flag for first tab to register
tab.selected = true;
};
this.tabs.push(tab);
};
The DEMO on PLNKR
AngularJS 1.5.3 introduced the $postLink life-cycle hook:
Life-cycle hooks
Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the directive:
$postLink() - Called after this controller's element and its children have been linked. Similar to the post-link function this hook can be used to set up DOM event handlers and do direct DOM manipulation. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs.
--AngularJS $compile Service API Reference -- Life-cycle hooks
The $postLink life-cycle hook won't work in this case because the directives use templateUrl. It also won't work with directives that build DOM after postlink; ng-repeat, ng-if, ng-include, etc.

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

Passing a parent directive attribute to a child directive attribute

I'm creating directives for a library that customers can use. I need to let the customers create their own templates for a directive and pass the absolute url value of that template into my directives. One of my directives will have another custom directive inside of it, and it's template will be figured out based upon the value of one of the parent directive's attributes. Here's an example:
<parent-dir menu-template="this.html" item-template="that.html"></parent-dir>
I have a template for this directive that looks like this:
<ul style="list: none" ng-repeat="item in menu">
<child-dir template="{{itemTemplate}}"></child-dir>
</ul>
My directives look like this:
angular.module('myApp')
.directive('parentDir', function () {
return {
restrict: 'E',
scope: {
menuTemplate: '#',
itemTemplate: '#',
menuType: '#',
menuName: '#',
menuId: '#',
},
templateUrl: function (element, attrs) {
alert('Scope: ' + attrs.menuTemplate);
return attrs.menuTemplate;
},
controller: function ($scope, $element, $attrs) {
$scope.defaultSubmit = false;
alert('Menu: '+$attrs.menuTemplate);
alert('Item: ' + $attrs.itemTemplate);
$scope.itemTemplate = $attrs.itemTemplate;
if ($attrs.$attr.hasOwnProperty('defaultSubmit')) {
alert('It does');
$scope.defaultSubmit = true;
}
}
};
})
.directive('childDir', function () {
return {
restrict: 'E',
require: '^parentDir',
templateUrl: function (element, attrs) {
alert('Item Template: ' + attrs.template);
return attrs.template;
},
controller: function ($scope, $element, $attrs) {
$scope.job;
alert('Under job: ' + $scope.itemTemplate);
}
};
});
I'm not showing all of the code but this is the main piece of my problem. When I run this, I keep getting undefined for the template on the childDir.
What is the best practice in perpetuating the value of itemTemplate from the parentDir so that the childDir can use it as it's template?
The reason you're running into problems is because the function that generates the templateUrl is running before a scope has been assigned to your directive - something that has to be done before interpolated data can be replaced.
In other words: at the point that the templateUrl function runs, the value of the template attribute is still "{{itemTemplate}}". This will remain the case until the directive's link (preLink to be precise) function runs.
I created a plunker to demonstrate the point here. Be sure to open the console. You'll see that templateUrl runs before both the parent and child linking functions.
So what do you do instead?
Fortunately, angular provides a $templateRequest service which allows you to request the template in the same way it would using templateUrl (it also uses the $templateCache which is handy).
put this code in your link function:
$templateRequest(attrs.template)
.then(function (tplString){
// compile the template then link the result with the scope.
contents = $compile(tplString)(scope);
// Insert the compiled, linked element into the DOM
elem.append(contents);
})
You can then remove any reference to the template in the directive definition object, and this will safely run once the attribute has been interpolated.

Angularjs - Pass argument to directive

Im wondering if there is a way to pass an argument to a directive?
What I want to do is append a directive from the controller like this:
$scope.title = "title";
$scope.title2 = "title2";
angular.element(document.getElementById('wrapper')).append('<directive_name></directive_name>');
Is it possible to pass an argument at the same time so the content of my directive template could be linked to one scope or another?
here is the directive:
app.directive("directive_name", function(){
return {
restrict:'E',
transclude:true,
template:'<div class="title"><h2>{{title}}</h3></div>',
replace:true
};
})
What if I want to use the same directive but with $scope.title2?
You can pass arguments to your custom directive as you do with the builtin Angular-directives - by specifying an attribute on the directive-element:
angular.element(document.getElementById('wrapper'))
.append('<directive-name title="title2"></directive-name>');
What you need to do is define the scope (including the argument(s)/parameter(s)) in the factory function of your directive. In below example the directive takes a title-parameter. You can then use it, for example in the template, using the regular Angular-way: {{title}}
app.directive('directiveName', function(){
return {
restrict:'E',
scope: {
title: '#'
},
template:'<div class="title"><h2>{{title}}</h2></div>'
};
});
Depending on how/what you want to bind, you have different options:
= is two-way binding
# simply reads the value (one-way binding)
& is used to bind functions
In some cases you may want use an "external" name which differs from the "internal" name. With external I mean the attribute name on the directive-element and with internal I mean the name of the variable which is used within the directive's scope.
For example if we look at above directive, you might not want to specify another, additional attribute for the title, even though you internally want to work with a title-property. Instead you want to use your directive as follows:
<directive-name="title2"></directive-name>
This can be achieved by specifying a name behind the above mentioned option in the scope definition:
scope: {
title: '#directiveName'
}
Please also note following things:
The HTML5-specification says that custom attributes (this is basically what is all over the place in Angular applications) should be prefixed with data-. Angular supports this by stripping the data--prefix from any attributes. So in above example you could specify the attribute on the element (data-title="title2") and internally everything would be the same.
Attributes on elements are always in the form of <div data-my-attribute="..." /> while in code (e.g. properties on scope object) they are in the form of myAttribute. I lost lots of time before I realized this.
For another approach to exchanging/sharing data between different Angular components (controllers, directives), you might want to have a look at services or directive controllers.
You can find more information on the Angular homepage (directives)
Here is how I solved my problem:
Directive
app.directive("directive_name", function(){
return {
restrict: 'E',
transclude: true,
template: function(elem, attr){
return '<div><h2>{{'+attr.scope+'}}</h2></div>';
},
replace: true
};
})
Controller
$scope.building = function(data){
var chart = angular.element(document.createElement('directive_name'));
chart.attr('scope', data);
$compile(chart)($scope);
angular.element(document.getElementById('wrapper')).append(chart);
}
I now can use different scopes through the same directive and append them dynamically.
You can try like below:
app.directive("directive_name", function(){
return {
restrict:'E',
transclude:true,
template:'<div class="title"><h2>{{title}}</h3></div>',
scope:{
accept:"="
},
replace:true
};
})
it sets up a two-way binding between the value of the 'accept' attribute and the parent scope.
And also you can set two way data binding with property: '='
For example, if you want both key and value bound to the local scope you would do:
scope:{
key:'=',
value:'='
},
For more info,
https://docs.angularjs.org/guide/directive
So, if you want to pass an argument from controller to directive, then refer this below fiddle
http://jsfiddle.net/jaimem/y85Ft/7/
Hope it helps..
Controller code
myApp.controller('mainController', ['$scope', '$log', function($scope, $log) {
$scope.person = {
name:"sangeetha PH",
address:"first Block"
}
}]);
Directive Code
myApp.directive('searchResult',function(){
return{
restrict:'AECM',
templateUrl:'directives/search.html',
replace: true,
scope:{
personName:"#",
personAddress:"#"
}
}
});
USAGE
File :directives/search.html
content:
<h1>{{personName}} </h1>
<h2>{{personAddress}}</h2>
the File where we use directive
<search-result person-name="{{person.name}}" person-address="{{person.address}}"></search-result>
<button my-directive="push">Push to Go</button>
app.directive("myDirective", function() {
return {
restrict : "A",
link: function(scope, elm, attrs) {
elm.bind('click', function(event) {
alert("You pressed button: " + event.target.getAttribute('my-directive'));
});
}
};
});
here is what I did
I'm using directive as html attribute and I passed parameter as following in my HTML file. my-directive="push" And from the directive I retrieved it from the Mouse-click event object. event.target.getAttribute('my-directive').
Insert the var msg in the click event with scope.$apply to make the changes to the confirm, based on your controller changes to the variables shown in ng-confirm-click therein.
<button type="button" class="btn" ng-confirm-click="You are about to send {{quantity}} of {{thing}} selected? Confirm with OK" confirmed-click="youraction(id)" aria-describedby="passwordHelpBlock">Send</button>
app.directive('ngConfirmClick', [
function() {
return {
link: function(scope, element, attr) {
var clickAction = attr.confirmedClick;
element.on('click', function(event) {
var msg = attr.ngConfirmClick || "Are you sure? Click OK to confirm.";
if (window.confirm(msg)) {
scope.$apply(clickAction)
}
});
}
};
}
])

Angularjs: redraw directive with different template when user logs in

in our general layout we have a sidebar which contains a few user details like name and thumbnail. but also a few different things.
we had the idea to load the sidebar with a directive, and inside of that, have 2 templates, 1 when logged in and 1 when logged out.
to display and not display the user details.
however, we have a directive called sidebar working with either of the 2,
App.directive('sidebar', ['UserService', '$compile', function(User, $compile) {
var getTemplate = function() {
console.warn(User.isLogged);
var templateUrl = "#sidebar" + ((User.isLogged) ? '_loggedin' : '') + "_template";
console.log('requesting template: [%s]', templateUrl);
return $(templateUrl).html();
};
return {
restrict: 'A',
link: function link(scope, element, attrs) {
var tmpl = getTemplate();
element.html(tmpl);
element.replaceWith($compile(element.html())(scope));
},
template: getTemplate()
};
}]);
and we load our user details in a service
App.factory('UserService', [function userService() {
var User = {
isLogged: false,
username: ''
};
return User;
}]);
the login form accepts this UserService as dependency, and sets it's isLogged to true
but how can I let the directive redraw the sidebar, when the isLogged is changed?
are we doing this the right way?
There is a stage known as the compile phase where angular
walks the DOM to identify all the registered directives in the
template. For each directive, it then transforms the DOM based on the directive’s
rules (template, replace, transclude, and so on), and calls the compilefunction
if it exists. The result is a compiled templatefunction, which will invoke the link
functions collected from all of the directives.
Basically, you can't conditionally load templates - it will compile the first one you give it. If you want to dynamically render your view, you might use two divs with the ng-show directive inside the template:
<div ng-show="user.isLogged">// logged in content</div>
<div ng-show="!user.isLogged">// logged out content</div>
You might think you need to inject a factory into your directive - I have just tried this and it does not work! I believe this is because directives can only set up one and two-way binding's with those on the scope chain. With that in mind, I brought the user object into application wide scope:
App.controller("AppCtrl", function($rootScope, UserService) {
$rootScope.user = UserService;
})
And then used an open scope from the directive:
app.directive("sidebar", function() {
return {
restrict: "A",
template: '<div>' +
'<div ng-show="user.isLogged">Logged In</div>' +
'<div ng-show="!user.isLogged">Logged Out</div></div>',
}
})
The user object here is being found through scope chaining. Because the directive does not have an isolated scope (I haven't declared a scope property), it is finding it through the scope chain - right up to the root scope. This is of course a "global" in a sense, and could easily be hidden:
$scope.user = somethingElse
Not the prettiest solution, but it does the job.
You could also conditionally manipulate the DOM in the link function, or better yet - off load this kind of logic to the router making use of nested templates and resolve.

Resources