AngularJS - Compile with ng-include bypasses internal controller in directive - angularjs

I'd like to have a directive with multiple templates as in this SO
When using compile in a directive as in this jsfiddle
the the ng-include uses the external controller and the
and the internal controller is not available to the scope of the template
example
function someDirective(){
return {
scope:{
...
},
compile: function(element, attrs) {
var type = "extended"; //default
if(typeof attrs.type !== 'undefined')
type = attrs.type;
element.append('<div ng-include="\'myproj/views/templates/group/groups-' + type + '.html\'"></div>');
},
//templateUrl: 'myproj/views/templates/group/groups-sideMenu.html',
controller:function($scope, $attrs, $rootScope, UtilsSrvc){
// ... the template won't use this controller
}
}
}
how to fix this problem?
EDIT
After some headbang something got clearer
In this fiddle (by Alessandro Cifani) the script works either for Angular 1.0, Angular 1.1 and Angular 1.2
The problems start when trying to isolate the scope:
this fiddle only works with Angular <= 1.1, with Angular >= 1.2 is not working
Things change when an empty 'templateUrl' is added as shown in this fiddle: it starts to be compliant to all versions
???????????????

I would use a different approach:
1) yoy may use the $compile service
2) you should avoid to use ng-include in favor of custom directives.
Following an example of what I'm saying:
(function () {
'use strict';
angular.module('myApp', [])
.directive('user',
function user($compile, $window) {
return {
scope: {
role: '#',
name: '#'
},
restrict: 'EA',
link: function link(scope, elem) {
var roles = {
SUPERADMIN: '<button ng-click="doSomething()" class="btn">Do something</button>',
STUDENT: '<div class="alert alert-success">You are a student</div>',
OTHER: '<div>Guest users have no options</div>'
};
// Create HTML elements in according with the role (SUPERADMIN, STUDENT, OTHER)
var role = roles[scope.role] || roles.OTHER;
var html = '<div><h2>' + scope.name + '</h2>' + role + '</div>';
// Step 1: parse HTML into DOM element
var template = angular.element(html);
//console.log (html);
// Step 2: compile the template
var linkFn = $compile(template);
//console.log (linkFn);
// Step 3: link the compiled template with the scope.
var element = linkFn(scope);
console.log (element[0]);
// Step 4: Append to DOM (optional)
elem.append(element);
scope.doSomething = function doSomething() {
$window.alert('doSomething');
};
}
};
}
);
})();
USAGE:
<user name="fabio" role="SUPERADMIN"></user>
<user name="paolo" role="STUDENT"></user>
<user name="marco"></user>
NOTE: in the previous example you could replace my "SUPERADMIN", "STUDENT" and "OTHER" templates with your own custom directives, ie: , ,
and here a JSBin: https://jsbin.com/mumahobuwu/edit?html,js,output

Related

How to $compile angular template to make it work in multiple controllers with aliases?

I have a custom directive that simply $compiles a template into another.
.directive('staticInclude', function($http, $templateCache, $compile) {
return function(scope, element, attrs) {
var templatePath = attrs.staticInclude;
//
$http.get(templatePath, {
cache: $templateCache
}).success(function(response) {
var contents = element.html(response).contents();
$compile(contents)(scope);
});
};
});
I use it like:
<div static-include="components/campaign/details.html"></div>
Because I'm using aliases for the controller (using angular UI router), all model in any of the templates are like:
<p>Delivery Time: <span class="text-medium">{{CtrlAlias.campaign.newsletter.sentDate | date:CtrlAlias.currentUser.params.settings}}</span></p>
How do I make this directive work in multiple templates where CtrlAlias changes?
I tried changing $compile(contents)(scope); into $compile(contents)(scope.newCtrlAlias);
Any ideas?
When you $compile and then link, you are free to provide your own scope against which the compiled content is linked. That means that you can have the template content refer to some arbitrary ViewModel name, say vm:
<p>Delivery Time: <span>{{vm.campaign.newsletter.sentDate}}</span></p>
And link against a scope that has vm property:
var scope = { vm: {...} }
It actually might be even useful to use an isolate scope for your compiled content, to make sure that you aren't assuming an existence of scope variables that may or may not be there when the content is linked:
.directive('staticInclude', function($templateRequest, $compile) {
return {
link: function(scope, element, attrs){
var alias = attrs.alias || 'vm';
var templatePath = attrs.staticInclude;
var newScope = scope.$new(true); // isolate scope
newScope.vm = scope[alias];
// $templateRequest is essentially $http with $templateCache
$templateRequest(templatePath)
.then(function(html){
$compile(html)(newScope, function cloneAttachFn(clone){
element.empty();
element.append(clone);
});
});
}
};
});
Then usage is like so:
<div ng-controller="MainCtrl as main">
<div static-include="components/campaign/details.html" alias="main">
</div>
</div>
Really not sure I understand why you would need to use this so it's not easy to answer. However, one possible solution could be to wrap the template in a <div> to which you can append the desired controller information. It's a bit gross but it might work for you. You would have to pass in the controller name and it's alias but you could perhaps add that to your $state's data properties and access them from that but again it all seems a bit hacky.
DEMO
app.directive('staticInclude', function($http, $templateCache, $compile) {
return {
scope: {
ctrlName : '#',
alias : '#'
},
link: link
};
function link(scope, element, attrs) {
var templatePath = attrs.staticInclude;
$http
.get(templatePath, {
cache: $templateCache
})
.success(function(response) {
var ctrlStr = scope.ctrlName + ' as ' + scope.alias,
template = '<div ng-controller="' + ctrlStr + '" >' + response + '</div>',
contents = element.html(template).contents();
$compile(contents)(scope);
});
};
});

angularjs directive unit testing with jasmine with template and link

I have a directive as below which i want to cover as part of my jasmine unit test but not sure how to get the template value and the values inside the link in my test case. This is the first time i am trying to unit test a directive.
angular.module('newFrame', ['ngResource'])
.directive('newFrame', [
function () {
function onAdd() {
$log.info('Clicked onAdd()');
}
return {
restrict: 'E',
replace: 'true',
transclude: true,
scope: {
filter: '=',
expand: '='
},
template:
'<div class="voice ">' +
'<section class="module">' +
'<h3>All Frames (00:11) - Summary View</h3>' +
'<button class="btn" ng-disabled="isDisabled" ng-hide="isReadOnly" ng-click="onAdd()">Add a frame</button>' +
'</section>' +
'</div>',
link: function (scope) {
scope.isDisabled = false;
scope.isReadOnly = false;
scope.onAdd = onAdd();
}
};
}
]);
Here is an example with explanation:
describe('newFrame', function() {
var $compile,
$rootScope,
$scope,
$log,
getElement;
beforeEach(function() {
// Load module and wire up $log correctly
module('newFrame', function($provide) {
$provide.value('$log', console);
});
// Retrieve needed services
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$log = _$log_;
});
// Function to retrieve a compiled element linked to passed scope
getCompiledElement = function(scope) {
var element = $compile('<new-frame></new-frame>')(scope);
$rootScope.$digest();
return element;
}
// Set up spies
spyOn($log, 'info').and.callThrough();
});
it('test', function() {
// Prepare scope for the specific test
$scope.filter = 'Filter';
$scope.expand = false;
// This will be the compiled element wrapped in jqLite
// To get reference to the DOM element do: element[0]
var element = getCompiledElement($scope);
// Get a reference to the button element wrapped in jqLite
var button = element.find('button');
// Verify button is not hidden by ng-hide
expect(button.hasClass('ng-hide')).toBe(false);
// Verify button is not disabled
expect(button.attr('disabled')).toBeFalsy();
// Click the button and verify that it generated a call to $log.info
button.triggerHandler('click');
expect($log.info).toHaveBeenCalled();
});
});
Demo: http://plnkr.co/edit/tOJ0puOd6awgVvRLmfAD?p=preview
Note that I changed the code for the directive:
Injected the $log service
Changed scope.onAdd = onAdd(); to scope.onAdd = onAdd;
After reading the angular documentation for directives,i was able to solve this. Since the restrict is marked as E, the directive can only be injected through a element name. Earlier i was trying through div like below.
angular.element('<div new-frame></div>')
This will work if restrict is marked as A (attributes). Now i changed my injection in he spec file to match the directive with element name.
angular.element('<new-frame></new-frame>')
Now i was able to get the template and scope attributes in my spec. Just to be sure to accept everything, the combination of A (aatributes), E (elements) and C (class name) can be used in the restrict or any 2 as needed.

How to set the dynamic controller for directives?

Talk is cheap, show my codes first:
HTML:
<div add-icons="IconsCtrl">
</div>
directive:
angular.module('attrDirective',[]).directive('addIcons', function($compile){
return {
restrict : 'A',
controller : "IconsCtrl"
},
link : function (scope, elem , attrs, ctrl) {
var parentElem = $(elem);
var icons = $compile("<i class='icon-plus' ng-click='add()'></i>)(scope);
parentElem.find(".accordion-heading").append(icons);
},
}
});
controller:
function IconsCtrl($scope){
$scope.add = function(){
console.log("add");
};
}
now it works, when i click the plus icon, browser console output "add".
but i want to set the controller into the directive dynamically,like this:
HTML:
<div add-icons="IconsOneCtrl">
</div>
<div add-icons="IconsTwoCtrl">
</div>
Controller:
function IconsOneCtrl($scope){
$scope.add = function(){
console.log("IconsOne add");
};
}
function IconsTwoCtrl($scope){
$scope.add = function(){
console.log("IconsTwo add");
}
}
directive likes :
angular.module('attrDirective',[]).directive('addIcons', function($compile){
return {
restrict : 'A',
controller : dynamic set,depends on attrs.addIcons
},
link : function (scope, elem , attrs, ctrl) {
var parentElem = $(elem);
var icons = $compile("<i class='icon-plus' ng-click='add()'></i>)(scope);
parentElem.find(".accordion-heading").append(icons);
},
}
});
how to achieve my goal? thanks for your answer!
Now it is possible with AngularJS. In directive you just add two new property called
controller , name property and also isolate scope is exactly needed here.
Important to note in directive
scope:{}, //isolate scope
controller : "#", // # symbol
name:"controllerName", // controller names property points to controller.
Working Demo for Setting Dynamic controller for Directives
HTML Markup :
<communicator controller-name="PhoneCtrl" ></communicator>
<communicator controller-name="LandlineCtrl" ></communicator>
Angular Controller and Directive :
var app = angular.module('myApp',[]).
directive('communicator', function(){
return {
restrict : 'E',
scope:{},
controller : "#",
name:"controllerName",
template:"<input type='text' ng-model='message'/><input type='button' value='Send Message' ng-click='sendMsg()'><br/>"
}
}).
controller("PhoneCtrl",function($scope){
$scope.sendMsg = function(){
alert( $scope.message + " : sending message via Phone Ctrl");
}
}).
controller("LandlineCtrl",function($scope){
$scope.sendMsg = function(){
alert( $scope.message + " : sending message via Land Line Ctrl ");
}
})
Your case you can try this below code snippets.
Working Demo
HTML Markup :
<div add-icons controller-name="IconsOneCtrl">
</div>
<div add-icons controller-name="IconsTwoCtrl">
</div>
Angular Code :
angular.module('myApp',[]).
directive('addIcons', function(){
return {
restrict : 'A',
scope:{},
controller : "#",
name:"controllerName",
template:'<input type="button" value="(+) plus" ng-click="add()">'
}
}).
controller("IconsOneCtrl",function($scope){
$scope.add = function(){
alert("IconsOne add ");
}
}).
controller("IconsTwoCtrl",function($scope){
$scope.add = function(){
alert("IconsTwo add ");
}
});
This is how it is done:
Inside your directive element all you need is an attribute which gives you access to the name of the controller: in my case my card attribute holds a card object which has a name property. In the directive you set the isolate scope to:
scope: { card: '=' }
This isolates and interpolates the card object to the directive scope. You then set the directive template to:
template: '',
this looks to the directive's controller for a function named getTemplateUrl and allows you to set the templateUrl dynamically as well. In the directive controller the getTemplateUrl function looks like this:
controller: ['$scope', '$attrs', function ($scope, $attrs) {
$scope.getTemplateUrl = function () { return '/View/Card?cardName=' +
$scope.card.name; }; }],
I have an mvc controller which links up the proper .cshtml file and handles security when this route is hit, but this would work with a regular angular route as well. In the .cshtml/html file you set up your dynamic controller by simply putting as the root element. The controller will differ for each template. This creates a hierarchy of controllers which allows you to apply additional logic to all cards in general, and then specific logic to each individual card. I still have to figure out how I'm going to handle my services but this approach allows you to create a dynamic templateUrl and dynamic controller for a directive using an ng-repeat based on the controller name alone. It is a very clean way of accomplishing this functionality and it is all self-contained.
1- you don't need to use: var parentElem = $(elem); as elem is a jquery element. This is similar to: $($('#myid'))
2- you can not dynamically assign a controller, because directive controller is instantiated before the prelinking phase.
The directive controller has access to attrs, so you can dynamically choose which internal function (functions inside your controller) according to the value of your attrs['addIcons']
ps. note attrs['addIcons'] is camel naming.

Can I require an adjacent directive?

I have two element-level directives, a search box and a search results. My markup is something like this (simplified):
<catalogue-search-box query="{{query}}">
<catalogue-search-results></catalogue-search-results>
I'm trying to access the search box controller from the search results directive, but the documentation suggests that in the directive's require property I can only find controllers on the same element or on the parent element. Is there a way to find controllers on adjacent elements?
After you comments here is how I would do it: use an object to hold all your state and pass it to both directives. Demo plunker
HTML
<body ng-controller="MySearchController">
<search-box search="mySearch"></search-box>
<search-results search="mySearch"></search-results>
</body>
JS
var search = angular.module('search', []);
//simulated service
search.service('Search', ['$timeout', '$q', function($timeout, $q) {
return {
findByQuery : function(query) {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve([query + ' result1', query + ' result2']);
console.log('resolved query ' + query);
}, 2000);
return deferred.promise;
}
};
}]);
search.controller('MySearchController', ['$scope', function($scope) {
$scope.mySearch = {
query : ''
}
}]);
search.controller('SearchBoxCtrl', ['$scope', 'Search', function($scope, Search) {
$scope.execute = function(search) {
console.log(search);
if(search.query && search.query.length > 3 && !search.running) {
search.running = true;
search.promise = Search.findByQuery(search.query).then(function(val) {
search.results = val;
});
}
};
}]);
search.directive('searchBox', function(){
return {
restrict: 'E',
scope : {
search : '='
},
controller: 'SearchBoxCtrl',
template : '<div ng-hide="search.results">Query: <input type="text" ng-model="search.query" ng-disabled="search.running"></input> <button ng-click="execute(search)" ng-disabled="search.running">Search</button></div>',
replace: 'true'
};
});
search.controller('SearchResultsCtrl', function(){
});
search.directive('searchResults', function(){
return {
restrict: 'E',
scope : {
search : '='
},
controller: 'SearchResultsCtrl',
template : '<div ng-show="search.results"><div ng-repeat="result in search.results">{{result}}</div></div>',
replace: true,
link : function(scope, element, attrs, ctrl){
}
};
});
PS:
Don't use p tags in directive templates as the root node. The html parser reports 2 nodes if you have p child nodes and angular has a requirement for a single root node.
You can further use the promise in the controller to register other functions to execute when the results come in.
One way I've been experimenting with since the question is having some kind of controller directive i.e.
<catalogue-search>
<catalogue-search-box query="{{query}}">
<catalogue-search-results></catalogue-search-results>
</catalogue-search>
I can then access the "controller directive" this using the parent (^) modifier in my require statement. Each directive can then talk to each other via the controller directive.
Does this seem sensible or is it overcomplicating things?

Evaluating Custom Directive after jQuery replaceWith

I have the following code:
<div ng-app="myApp" ng-controller="AngularCtrl">
Click here
</div>
<script>
jQuery("#click_btn").click(function(){
jQuery(this).replaceWith('<p>Hello {{student.name}}</p><div my-repeater></div>');
});
</script>​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​
Here is my angular code:
var myApp = angular.module('myApp',[]);
myApp.directive('myRepeater', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var myTemplate = "<div>{{rating}}</div>";
for(var i=0;i<scope.items.length;i++)
{
var myItem = scope.items[i];
var text = myTemplate.replace("{{rating}}",myItem.rating);
element.append(text);
}
}
};
});
function AngularCtrl($scope) {
$scope.student = {id: 1, name: 'John'};
$scope.items = [{id: 1, ratings: 10}, {id: 2, ratings: 20}];
}
Here, whenever i click on the button, the element is just getting replaced and not evaluated. I tried with "angular.bootstrap(document);" after document is ready.
But that just evaluates the angular objects. But still the custom directive "my-repeater" is not getting evaluated. Any help on how can i get this done?
First of all, I suppose this is test code, since angular has ng-repeat, which fits your needs.
There are several issues with your code:
1) You shouldn't use myTemplate.replace, but use the $compile service. Inject the $compile service into your directive (add as function param) and use it:
var text = $compile(myTemplate)(scope);
2) Items on the controller will not by accessible in your directive. Add it as a value to your my-repeater attribute:
<div my-repeater='{{items}}'>
In your directive you need to evaluate my-repeater:
var items = scope.$eval(attrs.myRepeater);
3) jQuery(this).replaceWith will not kickoff angular, since it it out of its scope. You need to do it manually by using scope.$apply. But is better to add the click event in the directive link function:
link: function(scope, element, attrs) {
element.on('click', function() {
...
scope.$apply();
});
Edit: Here is a working example.

Resources