How to pass `controllers` status to `directive` template function? - angularjs

Accoding to the current page, i need to change the template. my question is, how to pass the current page from controller to directives template method?
here is my try:
var myApp = angular.module('myApp', []);
myApp.controller('main', function ($scope) {
$scope.template = "homePage";
});
var getTemplate = function (page) { //i need $scope.template as params
if (page == "homePage") {
return "<button>One Button</button>"
}
if (page == "servicePage") {
return "<button>One Button</button><button>Two Button</button>"
}
if (page == "homePage") {
return "<button>One Button</button><button>Two Button</button><button>Three Button</button>"
}
}
myApp.directive('galleryMenu', function () {
return {
template : getTemplate(template), //$scope.template need to pass
link : function (scope, element, attrs) {
console.log(scope.template);
}
}
})
Live Demo
UPDATE
I am trying like this, but still getting error. what is the correct way to inject the $route to directive?
var galleryMenu = function ($route, $location) {
return {
template : function () {
console.log($route.current.className); //i am not getting!
},
link : function () {
}
}
}
angular
.module("tcpApp", ['$route', '$location'])
.directive('galleryMenu', galleryMenu);

You can call $routeParams on your directive declaration, to use it inside the template function.
myApp.directive('galleryMenu', ['$routeParams', function($routeParams) {
return {
template: function () {
var page = $routeParams.page || 'homePage', // Define a fallback, if $routeParams doesn't have 'page' param
output;
switch (page) {
case "servicePage":
output = "<button>One Button</button><button>Two Button</button>";
break;
default:
case "homePage":
output = "<button>One Button</button>";
/*
NOTE: Or this other, it was confusing to tell which one to use
output = "<button>One Button</button><button>Two Button</button><button>Three Button</button>";
*/
break;
}
return output;
},
link: function(scope, element, attrs) {
/* ... */
}
}
}]);
Edit 1:
If you are using ui-router switch from $routeParams to $stateParams.

You need to get current url from $state.current and can pass into the directive with the help of templateProvider.
myApp.directive('galleryMenu', function () {
return {
templateProvider : getTemplate(template), //$scope.template need to pass
link : function (scope, element, attrs) {
console.log(scope.template);
}
}
from getTemplate you can return $state.current. hope so it'll help you.

Related

Assignment to scope isn't working in directive

I have a directive below that makes a change in the scope element sections.data which i later assign to update the data on UI. However, this approach works in the function change_subnav but not in the switch case (added the comments to code). Why is this happening? The code to this change is same. Will appreciate any help here. Please let me know if I need to add more information.
Plunker- https://embed.plnkr.co/wmoJcQ/
.directive('selectSubnav', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attr) {
var change_subnav = function (subnav) {
if (scope.active_tab == 'user_exercises') {
var sections = {};
sections[subnav] = scope[scope.active_tab][subnav];
// this works
scope.$apply(function () {
$parse('sections.data').assign(scope.$parent, sections);
});
} else {
}
};
$(element).on('click', function () {
$(element).parent().children().removeClass('active');
$(element).addClass('active');
switch (attr.selectSubnav) {
case 'All':
// this doesn't work
scope.$apply(function () {
$parse('sections.data').assign(scope.$parent, scope[scope.active_tab]);
console.log(scope.sections.data);
});
break;
default:
change_subnav(attr.selectSubnav);
break;
}
});
}
}
})
UPDATE
.directive('selectSubnav', function ($parse) {
return {
restrict: 'A',
link: function (scope, element, attr) {
var currentScope = scope;
var change_subnav = function (subnav) {
if (scope.active_tab == 'user_exercises') {
var sections = {};
sections[subnav] = scope[scope.active_tab][subnav];
} else {
}
return sections;
};
$(element).on('click', function () {
$(element).parent().children().removeClass('active');
$(element).addClass('active');
var sections;
switch (attr.selectSubnav) {
case 'All':
// this doesn't work
sections = currentScope[currentScope.active_tab];
break;
default:
sections = change_subnav(attr.selectSubnav);
break;
}
currentScope.$apply(function () {
$parse('sections.data').assign(currentScope.$parent, sections);
});
});
}
}
})
You already have access to scope in your link function, so instead of updating scope.sections.data like this:
$parse('sections.data').assign(scope.$parent, sections);
You can simply do this:
$parse('sections.data').assign(scope, sections);
Or, better yet just update it directly, like this:
scope.sections.data = sections;

Angular Delay custom directive's link function

i have simple custom directive that checks and sets href on its element.
But i need to check the href with data loaded from server (async) to make sure that user has access to that link(Kind of ACL).
So how can i delay the link function of doing its job until this data has finished loading?
Ok, i cook something up for my self, appreciate any pointers for improving it,
I made a gist
angular.module('myModule')
.factory('dataLoader', ['$http', function ($http) {
var __list = null;
var __request = null;
return function (callbak) {
if (__list)
callbak(__list);
else {
if (!__request)
__request = $http({
method: 'GET',
url: '/myurl',
params: {}
});
__request.success(function (d) {
if (d.data) {
__list = d.data;
callbak(__list);
}
});
}
}
}])
.directive('myDirective', ['dataLoader', function (dataLoader) {
return {
restrict: "A",
compile: function (scope, element, attrs) {
return function (scope, element, attrs) {
dataLoader(function (acl) {
console.log(acl.length);
var href = attrs.myDirective;
if (href) {
if (href[0] != '#')
href = '#' + href;
element.attr('href', href);
}
});
};
}
};
}]);

Access directive's controller function inside link function

How to access the directives controller functions in link? This returns error:
ReferenceError: getFormatedValue is not defined.
Saw some examples where one injects a seperate controller through require and is able to access the controller by injecting it, as seen below in my code, as the 4th param in the link function. However, i need this controller to be inside this directive.
.directive('testDirective', function () {
link: function ($scope, element, attribute, controller) {
attribute.$observe('numbers', function(value) {
controller.setNumbers(value);
});
attribute.$observe('decimals', function(decimals) {
decimals = decimals || defaultValue();
decimals = (decimals.length > 2 ? decimals.slice(0, 2 - decimals.length) : decimals);
$scope.decimals = controller.getFormatedValue(decimals, 'decimals');
});
},
controller: function ($scope, $element, $attrs) {
this.setNumbers = function (numbers) {
numbers = numbers || $scope.defaultValue();
$scope.numbers = getFormatedValue(numbers, 'numbers');
};
this.getFormatedValue = function(value, kindofvalue){
var maxDecimals = '2',
returnValue;
if (kindofvalue === 'decimals'){
returnValue = addzero(value, maxDecimals);
}
else if (kindofvalue === 'numbers'){
returnValue = value.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
return returnValue;
};
this.defaultValue = function() {
return '0';
};
this.addzero = function(decimalValue , maxDecimals) {
return decimalValue.length < maxDecimals ? $scope.addzero(decimalValue + '0', maxDecimals) : decimalValue;
};
}
};
Add require: 'testDirective' to the directive definition, i.e.:
.directive('testDirective', function () {
return {
require: 'testDirective',
link: function ($scope, element, attribute, controller) {
...
},
controller: function ($scope, $element, $attrs) {
...
}
};
});

Load Angular Directive Template Async

I want to be able to load the directive's template from a promise. e.g.
template: templateRepo.get('myTemplate')
templateRepo.get returns a promise, that when resolved has the content of the template in a string.
Any ideas?
You could load your html inside your directive apply it to your element and compile.
.directive('myDirective', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Some arbitrary promise.
fetchHtml()
.then(function(result){
element.html(result);
$compile(element.contents())(scope);
}, function(error){
});
}
}
});
This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.
Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:
Create generic service to control indicator with.
Manually load template inside link function, show indicator on request send and hide on response.
Here's very simplified example you can start with:
<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
position: absolute;
top: calc(50% - 16px);
left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
$rootScope.items = ["One", "Two"];
$rootScope.more = function () {
$rootScope.items.push(Math.random());
};
})
.factory("throbber", function () {
var visible = false;
var throbber = document.createElement("img");
throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
throbber.classList.add("throbber");
function show () {
document.body.appendChild(throbber);
}
function hide () {
document.body.removeChild(throbber);
}
return {
show: show,
hide: hide
};
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
var template = "<div>{{text}}</div>";
var templateUrl = "templateUrl";
return {
link: function (scope, el, attr) {
var tmpl = $templateCache.get(templateUrl);
if (!tmpl) {
throbber.show();
tmpl = $timeout(function () {
return template;
}, 1000);
}
$q.when(tmpl).then(function (value) {
$templateCache.put(templateUrl, value);
el.html(value);
$compile(el.contents())(scope);
throbber.hide();
});
},
scope: {
text: "=test"
}
};
});
JSBin example.
In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.
How template loading works in my example:
Check if there's our template in $templateCache.
If no, fetch it from URL and show indicator.
Manually put template inside element and [$compile][2] it.
Hide indicator.
If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.
Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.
I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.
What I would do is to add an ng-include in my directive to selectively load what I need
Check this demo from angular page. It may help:
http://docs.angularjs.org/api/ng.directive:ngInclude
````
/**
* async load template
* eg :
* <div class="ui-header">
* {{data.name}}
* <ng-transclude></ng-transclude>
* </div>
*/
Spa.Service.factory("RequireTpl", [
'$q',
'$templateCache',
'DataRequest',
'TplConfig',
function(
$q,
$templateCache,
DataRequest,
TplConfig
) {
function getTemplate(tplName) {
var name = TplConfig[tplName];
var tpl = "";
if(!name) {
return $q.reject(tpl);
} else {
tpl = $templateCache.get(name) || "";
}
if(!!tpl) {
return $q.resolve(tpl);
}
//加载还未获得的模板
return new $q(function(resolve, reject) {
DataRequest.get({
url : "/template/",
action : "components",
responseType : "text",
components : name
}).success(function(tpl) {
$templateCache.put(name, tpl);
resolve(tpl);
}).error(function() {
reject(null);
});
});
}
return getTemplate;
}]);
/**
* usage:
* <component template="table" data="info">
* <span>{{info.name}}{{name}}</span>
* </component>
*/
Spa.Directive.directive("component", [
"$compile",
"RequireTpl",
function(
$compile,
RequireTpl
) {
var directive = {
restrict : 'E',
scope : {
data : '='
},
transclude : true,
link: function ($scope, element, attrs, $controller, $transclude) {
var linkFn = $compile(element.contents());
element.empty();
var tpl = attrs.template || "";
RequireTpl(tpl)
.then(function(rs) {
var tplElem = angular.element(rs);
element.replaceWith(tplElem);
$transclude(function(clone, transcludedScope) {
if(clone.length) {
tplElem.find("ng-transclude").replaceWith(clone);
linkFn($scope);
} else {
transcludedScope.$destroy()
}
$compile(tplElem.contents())($scope);
}, null, "");
})
.catch(function() {
element.remove();
console.log("%c component tpl isn't exist : " + tpl, "color:red")
});
}
};
return directive;
}]);
````

AngularJS - bind to directive resize

How can i be notified when a directive is resized?
i have tried
element[0].onresize = function() {
console.log(element[0].offsetWidth + " " + element[0].offsetHeight);
}
but its not calling the function
(function() {
'use strict';
// Define the directive on the module.
// Inject the dependencies.
// Point to the directive definition function.
angular.module('app').directive('nvLayout', ['$window', '$compile', layoutDirective]);
function layoutDirective($window, $compile) {
// Usage:
//
// Creates:
//
var directive = {
link: link,
restrict: 'EA',
scope: {
layoutEntries: "=",
selected: "&onSelected"
},
template: "<div></div>",
controller: controller
};
return directive;
function link(scope, element, attrs) {
var elementCol = [];
var onSelectedHandler = scope.selected();
element.on("resize", function () {
console.log("resized.");
});
$(window).on("resize",scope.sizeNotifier);
scope.$on("$destroy", function () {
$(window).off("resize", $scope.sizeNotifier);
});
scope.sizeNotifier = function() {
alert("windows is being resized...");
};
scope.onselected = function(id) {
onSelectedHandler(id);
};
scope.$watch(function () {
return scope.layoutEntries.length;
},
function (value) {
//layout was changed
activateLayout(scope.layoutEntries);
});
function activateLayout(layoutEntries) {
for (var i = 0; i < layoutEntries.length; i++) {
if (elementCol[layoutEntries[i].id]) {
continue;
}
var div = "<nv-single-layout-entry id=slot" + layoutEntries[i].id + " on-selected='onselected' style=\"position:absolute;";
div = div + "top:" + layoutEntries[i].position.top + "%;";
div = div + "left:" + layoutEntries[i].position.left + "%;";
div = div + "height:" + layoutEntries[i].size.height + "%;";
div = div + "width:" + layoutEntries[i].size.width + "%;";
div = div + "\"></nv-single-layout-entry>";
var el = $compile(div)(scope);
element.append(el);
elementCol[layoutEntries[i].id] = 1;
}
};
}
function controller($scope, $element) {
}
}
})();
Use scope.$watch with a custom watch function:
scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
console.log('directive got resized:', value.split('x'));
}
)
You would typically want to watch the element's offsetWidth and offsetHeight properties. With more recent versions of AngularJS, you can use $scope.$watchGroup in your link function:
app.directive('myDirective', [function() {
function link($scope, element) {
var container = element[0];
$scope.$watchGroup([
function() { return container.offsetWidth; },
function() { return container.offsetHeight; }
], function(values) {
// Handle resize event ...
});
}
// Return directive definition ...
}]);
However, you may find that updates are quite slow when watching the element properties directly in this manner.
To make your directive more responsive, you could moderate the refresh rate by using $interval. Here's an example of a reusable service for watching element sizes at a configurable millisecond rate:
app.factory('sizeWatcher', ['$interval', function($interval) {
return function (element, rate) {
var self = this;
(self.update = function() { self.dimensions = [element.offsetWidth, element.offsetHeight]; })();
self.monitor = $interval(self.update, rate);
self.group = [function() { return self.dimensions[0]; }, function() { return self.dimensions[1]; }];
self.cancel = function() { $interval.cancel(self.monitor); };
};
}]);
A directive using such a service would look something like this:
app.directive('myDirective', ['sizeWatcher', function(sizeWatcher) {
function link($scope, element) {
var container = element[0],
watcher = new sizeWatcher(container, 200);
$scope.$watchGroup(watcher.group, function(values) {
// Handle resize event ...
});
$scope.$on('$destroy', watcher.cancel);
}
// Return directive definition ...
}]);
Note the call to watcher.cancel() in the $scope.$destroy event handler; this ensures that the $interval instance is destroyed when no longer required.
A JSFiddle example can be found here.
Here a sample code of what you need to do:
APP.directive('nvLayout', function ($window) {
return {
template: "<div></div>",
restrict: 'EA',
link: function postLink(scope, element, attrs) {
scope.onResizeFunction = function() {
scope.windowHeight = $window.innerHeight;
scope.windowWidth = $window.innerWidth;
console.log(scope.windowHeight+"-"+scope.windowWidth)
};
// Call to the function when the page is first loaded
scope.onResizeFunction();
angular.element($window).bind('resize', function() {
scope.onResizeFunction();
scope.$apply();
});
}
};
});
The only way you would be able to detect size/position changes on an element using $watch is if you constantly updated your scope using something like $interval or $timeout. While possible, it can become an expensive operation, and really slow your app down.
One way you could detect a change on an element is by calling
requestAnimationFrame.
var previousPosition = element[0].getBoundingClientRect();
onFrame();
function onFrame() {
var currentPosition = element[0].getBoundingClientRect();
if (!angular.equals(previousPosition, currentPosition)) {
resiszeNotifier();
}
previousPosition = currentPosition;
requestAnimationFrame(onFrame);
}
function resiszeNotifier() {
// Notify...
}
Here's a Plunk demonstrating this. As long as you're moving the box around, it will stay red.
http://plnkr.co/edit/qiMJaeipE9DgFsYd0sfr?p=preview
A slight variation on Eliel's answer worked for me. In the directive.js:
$scope.onResizeFunction = function() {
};
// Call to the function when the page is first loaded
$scope.onResizeFunction();
angular.element($(window)).bind('resize', function() {
$scope.onResizeFunction();
$scope.$apply();
});
I call
$(window).resize();
from within my app.js. The directive's d3 chart now resizes to fill the container.
Here is my take on this directive (using Webpack as bundler):
module.exports = (ngModule) ->
ngModule.directive 'onResize', ['Callback', (Callback) ->
restrict: 'A'
scope:
onResize: '#'
onResizeDebounce: '#'
link: (scope, element) ->
container = element[0]
eventName = scope.onResize || 'onResize'
delay = scope.onResizeDebounce || 1000
scope.$watchGroup [
-> container.offsetWidth ,
-> container.offsetHeight
], _.debounce (values) ->
Callback.event(eventName, values)
, delay
]

Resources