Basically, what I need is an alternative to ngView that allows me to load a custom route inside an html element.
For example, this route:
$routeProvider.when('/some/path', {
templateUrl: 'some/template.html',
controller: someController,
controllerAs: 'ctrlAlias'
});
With this directive (with myRoute='/some/path'):
<div routeView="myRoute" />
Whould result in:
<div ng-controller="someController as ctrlAlias" ng-include src="some/template.html" />
Because of compatibility and legacy restrictions I can't use ui-router.
How do I implement this using a directive (or otherwise) ?
So, I adapted the code from ngView to do what i needed.
It is not very elegant since there is a lot of code duplication from angular-router module but it works perfectly.
Note that this only works with routes with templateUrl's and with routes without parameters.
Usage:
<div route-view="'/my/route'"></div>
<div route-view="myRoute"></div>
Code:
angular.module('app', [])
.directive('routeView', routeViewDirective)
.directive('routeView', routeViewFillContentDirective)
function routeViewDirective($animate, $parse, $q, $route, $sce, $templateRequest) {
return {
restrict: 'EA',
terminal: true,
priority: 400,
transclude: 'element',
link: routeViewDirectiveLink
};
function routeViewDirectiveLink(scope, $element, attributes, ctrl, $transclude) {
var model = $parse(attributes.routeView);
var currentScope;
var currentElement;
scope.$watch(model, update);
function cleanupLastView() {
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if (currentElement) {
$animate.leave(currentElement);
currentElement = null;
}
}
function update(path) {
var route = $route.routes[path];
if (route && route.templateUrl) {
var newScope = scope.$new();
var clone = $transclude(newScope, function(clone) {
$animate.enter(clone, null, currentElement || $element);
cleanupLastView();
});
currentElement = clone;
currentScope = newScope;
} else {
cleanupLastView();
}
}
}
}
function routeViewFillContentDirective($compile, $controller, $parse, $q, $route, $sce, $templateRequest) {
return {
restrict: 'EA',
priority: -400,
link: routeViewFillContentDirectiveLink
};
function routeViewFillContentDirectiveLink(scope, $element, attributes) {
var path = $parse(attributes.routeView)(scope);
var route = $route.routes[path];
var templateUrl = route && $sce.getTrustedResourceUrl(route.templateUrl);
if (angular.isDefined(templateUrl)) {
$templateRequest(templateUrl).then(function(template) {
$element.html(template);
var link = $compile($element.contents());
if (route.controller) {
var controller = $controller(route.controller, { $scope: scope });
if (route.controllerAs) {
scope[route.controllerAs] = controller;
}
$element.data('$ngControllerController', controller);
$element.children().data('$ngControllerController', controller);
}
link(scope);
});
}
}
}
Related
My ajax fires after the complete directive executes. Is there any work around for this so that I can have my grid configuration loads before coming to the grid directive
gridApp.directive('grid', function () {
return {
restrict: "EA",
scope: {
gridName: "#"
},
template: '<h1>kendoDirective</h1><br/><div kendo-grid={{gridName}} options="gridOptions"></div>',
controller: function ($scope, $element, $attrs, widgetUtils) {
var gridConfig = widgetUtils.GetGridOption().then(onLoad);
var onLoad = function (data) {
$scope.gridOptions = data;
}
console.log('DirectiveScope: ' + $scope.gridOptions);
},
link: function ($scope, $element, $attrs) {
}
};
});
gridApp.service('widgetUtils', function ($http) {
var getGridOption = function () {
return $http.get('/Base/LoadGridConfiguration').then(function (response) {
return response.data;
});
}
return {
GetGridOption: getGridOption
};
});
You can handle it with ng-if in template. I created $scope.isReady and change it state after options loaded.
gridApp.directive('grid', function () {
return {
restrict: "EA",
scope: {
gridName: "#"
},
template: '<h1>kendoDirective</h1><br/><div data-ng-if="isReady" kendo-grid={{gridName}} options="gridOptions"></div>',
controller: function ($scope, $element, $attrs, widgetUtils) {
var gridConfig = widgetUtils.GetGridOption().then(onLoad);
$scope.isReady = false;
var onLoad = function (data) {
$scope.gridOptions = data;
$scope.isReady = true; // here we ready to init kendo component
$scope.$apply();
}
console.log('DirectiveScope: ' + $scope.gridOptions);
},
link: function ($scope, $element, $attrs) {
}
};
});
I am trying to use a directive, click-anywhere-but-here, in my header HTML, using controller navCtrl. Angular is throwing error:
Unknown provider: clickAnywhereButHereProvider <-
I'm thinking this has to do with how I'm using gulp to concatenate the JS files. I checked the concatenated main.js files with all JS, and see that navCtrl is defined above the clickAnywhereButHere directive. Not sure if this matters at all since the controller isn't using the directive at all, only the header.html file.
<header ng-controller="navCtrl">
<a click-anywhere-but-here="clickedSomewhereElse()" ng-click="clickedHere()">
<li>study</li>
</a>
</header>
How can I force the header to wait until clickAnywhereButHere directive is loaded before complaining?
Edit: Code:
navCtrl.js: I've gutted out a lot of the unrelated code
angular
.module('DDE')
.controller('navCtrl', ['$rootScope', '$location', '$scope', 'Modal', 'Auth', '$window', '$state', 'deviceDetector',
function($rootScope, $location, $scope, Modal, Auth, $window, $state, deviceDetector) {
$scope.clicked = '';
$scope.clickedHere = function(){
$scope.clicked = 'stop that';
console.log('clicked on element');
};
$scope.clickedSomewhereElse = function(){
console.log('clicked elsewhere');
$scope.clicked = 'thanks';
};
$scope.headings = [
{page: 'contact', route: '#/contact'}
];
}
]);
clickAnywhereButHere.js directive:
angular.module('DDE')
.directive('clickAnywhereButHere', function($document, clickAnywhereButHereService){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
var handler = function(e) {
e.stopPropagation();
};
elem.on('click', handler);
scope.$on('$destroy', function(){
elem.off('click', handler);
});
clickAnywhereButHereService(scope, attr.clickAnywhereButHere);
}
};
});
clickAnywhereButHereService.js Service:
angular.module('DDE')
.factory('clickAnywhereButHereService', function($document){
var tracker = [];
return function($scope, expr) {
var i, t, len;
for(i = 0, len = tracker.length; i < len; i++) {
t = tracker[i];
if(t.expr === expr && t.scope === $scope) {
return t;
}
}
var handler = function() {
$scope.$apply(expr);
};
$document.on('click', handler);
// IMPORTANT! Tear down this event handler when the scope is destroyed.
$scope.$on('$destroy', function(){
$document.off('click', handler);
});
t = { scope: $scope, expr: expr };
tracker.push(t);
return t;
};
});
Both the directive and service are present in my min file:
You need to take into account the fact that your JS is minified.
So change this
.directive('clickAnywhereButHere', function($document, clickAnywhereButHereService){
to this
.directive('clickAnywhereButHere',
['$document', 'clickAnywhereButHereService',
function($document, clickAnywhereButHereService){
//...
}])
I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.
First let me share the directives I have:
PARENT
angular
.module('app.components')
.directive('myTable', myTable);
function myTable() {
var myTable = {
restrict: 'E',
transclude: {
actions: 'actionsContainer',
table: 'tableContainer'
},
scope: {
selected: '='
},
templateUrl: 'app/components/table/myTable.html',
controller: controller,
controllerAs: 'vm',
bindToController: true
};
return myTable;
function controller($attrs, $scope, $element) {
var vm = this;
vm.enableMultiSelect = $attrs.multiple === '';
}
}
CHILD
angular
.module('app.components')
.directive('myTableRow', myTableRow);
myTableRow.$inject = ['$compile'];
function myTableRow($compile) {
var myTableRow = {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
return myTableRow;
function link(scope, element, attrs, ctrls) {
var self = ctrls.shift(),
tableCtrl = ctrls.shift();
if(tableCtrl.enableMultiSelect){
element.prepend(createCheckbox());
}
self.isSelected = function () {
if(!tableCtrl.enableMultiSelect) {
return false;
}
return tableCtrl.selected.indexOf(self.model) !== -1;
};
self.select = function () {
tableCtrl.selected.push(self.model);
};
self.deselect = function () {
tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
};
self.toggle = function (event) {
if(event && event.stopPropagation) {
event.stopPropagation();
}
return self.isSelected() ? self.deselect() : self.select();
};
function createCheckbox() {
var checkbox = angular.element('<md-checkbox>').attr({
'aria-label': 'Select Row',
'ng-click': 'vm.toggle($event)',
'ng-checked': 'vm.isSelected()'
});
return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
}
}
function controller() {
}
}
So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.
When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.
I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:
describe('myTableRow Directive', function() {
var $compile,
scope,
compiledElement,
tableCtrl = {
enableMultiSelect: true,
selected: []
},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
});
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
scope.data = {foo: 'bar'};
compiledElement = $compile(element)(scope);
scope.$digest();
controller = compiledElement.controller('myTableRow');
});
describe('select', function(){
it('should work', function(){
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
But I'm getting an error:
undefined is not an object (evaluating 'controller.toggle')
If I console log out the value of controller in my test it shows as undefined.
I am no doubt doing something wrong here in my approach, can someone please enlighten me?
Thanks
UPDATE
I have come across these posts already:
Unit testing a directive that defines a controller in AngularJS
How to access controllerAs namespace in unit test with compiled element?
I have tried the following, given I'm using controllerAs syntax:
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
$compile(element)($scope);
$scope.$digest();
console.log(element.controller('vm'));
But the controller is still coming up as undefined in the console log.
UPDATE 2
I have come across this post - isolateScope() returning undefined when testing angular directive
Thought it could help me, so I tried the following instead
console.log(compiledElement.children().scope().vm);
But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach
UPDATE 3
I have come across this article which covers exactly the kind of thing I'm trying to achieve.
When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:
beforeEach(function(){
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
compiledElement = $compile(element)($scope);
$scope.$digest();
element = element.find('act-table-row');
console.log(element);
console.log(element.scope()); //returns undefined
});
I just wonder if this is down to me using both a link function and controllerAs syntax?
You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.
See below, specifically:
controller = compiledElement.find('tr').controller('myTableRow');
/* Angular App */
(function() {
"use strict";
angular
.module('app.components', [])
.directive('myTableRow', myTableRow);
function myTableRow() {
return {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
function link($scope, $element, $attrs, $ctrls) {
var self = $ctrls.shift(),
tableCtrl = $ctrls.shift();
self.toggle = function() {
// keeping it simple for the unit test...
tableCtrl.selected[0] = self.model;
};
}
function controller() {}
}
})();
/* Unit Test */
(function() {
"use strict";
describe('myTableRow Directive', function() {
var $compile,
$scope,
compiledElement,
tableCtrl = {},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
tableCtrl.enableMultiSelect = true;
tableCtrl.selected = [];
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
$scope.data = {
foo: 'bar'
};
compiledElement = $compile(element)($scope);
$scope.$digest();
controller = compiledElement.find('tr').controller('myTableRow');
//console.log(controller); // without the above .find('tr'), this is undefined
});
describe('select', function() {
it('should work', function() {
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
Here is an example to quote the use of angular directives using the parent child relationship.
The definition of annotated-image looks like this:(which is the parent)
angular.module('annotatedimage').directive('annotatedImage', function() {
function AnnotatedImageController(scope) {}
return {
{
restrict: 'E',
template: [
'<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
'<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
'<annotated-image-current></annotated-image-current>'
].join('\n'),
controller: ['$scope', AnnotatedImageController],
scope: {
configuration: '='
}
}
};
});
Now for the annotatedImageController , annotatedImageViewer and the annotatedImageCurrent which are the children.
angular.module('annotated-image').directive('annotatedImageControls', function() {
function link(scope, el, attrs, controller) {
scope.showAnnotations = function() {
controller.showAnnotations();
};
controller.onShowAnnotations(function() {
scope.viewing = true;
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: [
'<div>',
'<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
'<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
'<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
'</div>'
].join('\n'),
link: link,
scope: {
annotations: '='
}
};
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
function link(scope, el, attrs, controller) {
var canvas = el.find('canvas');
var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);
controller.onShowAnnotations(function() {
viewManager.showAnnotations(scope.annotations);
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: '<canvas></canvas>',
link: link,
scope: {
src: '=',
annotations: '='
}
};
});
The same can be done for the annotatedImageCurrent
Summary
<parent-component>
<child-component></child-component>
<another-child-component></another-child-component>
</parent-component>
Parent Component
module.directive('parentComponent', function() {
function ParentComponentController(scope) {
// initialize scope
}
ParentComponentController.prototype.doSomething = function() {
// does nothing here
}
return {
restrict: 'E',
controller: ['$scope', ParentComponentController],
scope: {}
};
});
Child Component
module.directive('childComponent', function() {
function link(scope, element, attrs, controller) {
controller.doSomething();
}
return {
restrict: 'E',
require: '^parentComponent',
link: link,
scope: {}
}
});
I am following the joe eames tutorials on pluralsight. It seems to be fairly straightforward. I am setting up one directive inside of another, and setting up * require: on a child controller*
Here is the code that I have from the demo. I am using angular 1.5 I haven't changed the $scope to controllerAs as I am focused on figuring out communication between directive controllers.
(function() {
'use strict';
angular
.module('app', [])
.controller('mainCtrl', function($scope) {
})
.directive('swTabstrip', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
controller: function($scope) {
$scope.panes = [];
$scope.select = function(pane) {
pane.selected = true;
$scope.panes.forEach(function(curPane) {
if(curPane !== pane) {
curPane.selected = false;
}
})
}
this.addPane = function(pane) {
$scope.panes.push(pane);
if($scope.panes.length ===1) {
pane.selected = true;
}
}
},
templateUrl: 'swTabstrip.html'
}
})
.directive('swPane', function() {
return {
restrict: 'E',
transclude: true,
scope: {
title: '#'
},
require: '^swTabstrip',
link: function(scope, el, attrs, tabstripCtrl) {
tabstripCtrl.addPane(scope);
},
templateUrl: 'swPane.html'
}
})
})();
The tutorial calls for me to set up directive swPane to require 'swTabstrip'. However, I am getting an error in the console
3angular.js:13156 Error: [$compile:ctreq]
Controller 'swTabstrip', required by directive 'swPane', can't be found!
You have to actually create your tabstripCtrl that your directive uses and at the same time then you can pass it in:
(function () {
'use strict';
angular
.module('app', [])
.controller('mainCtrl', function ($scope) {})
.controller('tabstripCtrl', function($scope) {
$scope.panes = [];
$scope.select = function (pane) {
pane.selected = true;
$scope.panes.forEach(function (curPane) {
if (curPane !== pane) {
curPane.selected = false;
}
})
}
this.addPane = function (pane) {
$scope.panes.push(pane);
if ($scope.panes.length === 1) {
pane.selected = true;
}
}
})
.directive('swTabstrip', function () {
return {
restrict : 'E',
transclude : true,
scope : {},
controller : 'tabstripCtrl' ,
templateUrl : 'swTabstrip.html'
}
})
.directive('swPane', function () {
return {
restrict : 'E',
transclude : true,
scope : {
title : '#'
},
require : '^tabstripCtrl',
link : function (scope, el, attrs, tabstripCtrl) {
tabstripCtrl.addPane(scope);
},
templateUrl : 'swPane.html'
}
})
})();
If you are trying to share data between your directives, look into services.
I am trying to create a page that dynamically loads a template based on the option that the user chooses from a select box. I currently have it loading the template on page load but after that it does not change based on user action.
.directive('ngUsersearch', ['$compile', '$http', '$templateCache', function($compile, $http, $templateCache) {
var getTemplate = function(contentType) {
var templateLoader,
baseUrl = 'view2/components/',
templateMap = {
beer: 'beerList.html',
brewery: 'breweryList.html',
event: 'eventList.html',
guild: 'guildList.html'
};
var templateUrl = baseUrl + templateMap[contentType];
templateLoader = $http.get(templateUrl, {cache: $templateCache});
return templateLoader;
}
var linker = function(scope, element, attrs) {
var loader = getTemplate(scope.ngModel);
var promise = loader.success(function(html) {
element.html(html);
}).then(function (response) {
element.replaceWith($compile(element.html())(scope));
});
}
return {
restrict:"E",
scope: {
ngModel: '='
},
link: linker
}
}]);
Here is my HTML:
<select ng-model="userFilter">
<option value="beer">Beer</option>
<option value="brewery">Brewery</option>
<option value="event">Event</option>
<option value="guild">Guild</option>
</select>
<ng-usersearch ng-model="userFilter"></ng-usersearch>
you forgot listen the change event of the model;
var linker = function(scope, element, attrs) {
scope.$watch('ngModel', function(newValue, oldValue) {
var loader = getTemplate(newValue);
var promise = loader.success(function(html) {
element.html(html);
}).then(function (response) {
element.replaceWith($compile(element.html())(scope)); // you compile and you have isolated scope?
});
});
}
on your compile the only scope available would be ngModel
This solution worked for me. I switched the way that the directive was loading the template. This can be done at the link function, but after the directive is set up and a part of the DOM, I was trying to remove the directive itself from the DOM by replacing it, which does not play well with how Angular's selectors work. So, now I am just replacing its contents. Also, in order to get the ng-repeat to work within the custom directive I had to add the search-results='searchResults' and then define that in the directives scope as well.
HTML:
<ng-usersearch ng-model="userFilter" search-results='searchResults'></ng-usersearch>
Controller:
.controller('View2Ctrl', [ '$scope', 'Restangular', function($scope, Restangular) {
$scope.userSearch = "";
$scope.userFilter = "beer";
$scope.search = function(userSearch, userFilter) {
$scope.searchResults = ("No " + userFilter + " Information Available");
Restangular.all('search?q=' + userSearch + '&type=' + userFilter + '&withBreweries=Y').customGET().then(function(data) {
$scope.searchResults = data;
});
};
}])
Directive:
.directive('ngUsersearch', ['$http', '$templateCache', '$compile', function($http, $templateCache, $compile) {
var getTemplate = function(contentType) {
var templateLoader,
baseUrl = 'view2/components/',
templateMap = {
all: 'all.html',
beer: 'beerList.html',
brewery: 'breweryList.html',
event: 'eventList.html',
guild: 'guildList.html'
};
var templateUrl = baseUrl + templateMap[contentType];
templateLoader = $http.get(templateUrl, {cache: $templateCache.get()});
return templateLoader;
}
var link = function(scope, element) {
scope.$watch('ngModel', function(newValue, oldValue) {
var loader = getTemplate(newValue);
var promise = loader.success(function(html) {
var rendered = $compile(html)(scope);
element.empty();
element.append(rendered); });
});
}
return {
restrict:"E",
scope: {
ngModel: '=',
searchResults: '='
},
link: link
}
}]);
I hope this helps other coders because I struggled with this for a day.