I'm writing the following directive:
.directive('mypagination', function () {
return {
restrict: 'E',
scope: {
pageCount: "=",
},
template: "{{pageCount}}",
link: function ($scope, $element, $attrs) {
$scope.pages = [];
for (var i = 1; i <= $scope.pageCount; i++) {
$scope.pages.push(i);
}
}
}
})
My issue is that $scope.pageCount inside the for loop is set to 0, but {{pageCount}} in the template is rendering the correct value.
In HTML the directive is being called like this:
<mypagination page-count="mjaController.pages.length"
on-page-change="mjaController.fetchStuff(page)">
</mypagination>
Why would the value of pageCount be 0 inside the link function, but render correctly on the page?
When your link function is executed at that time pageCount can be 0 because it is bound to mjaController.pages.length property which I guess is retrieved from an API and is async call. Once the mjaController.pages is filled with some data, then pageCount is set to its length and is displayed on template via $digest cycle but link function will not execute again. To make it work as expected do following
.directive('mypagination', function () {
return {
restrict: 'E',
scope: {
pageCount: "=",
},
template: "{{ pages()|json }}",
link: function ($scope, $element, $attrs) {
$scope.pages = function () {
var pages = [];
for (var i = 1; i <= $scope.pageCount; i++) {
pages.push(i);
}
return pages;
}
}
}
})
add a method in $scope and use its return value in template.
Use $watch to wait for the data to arrive from the server:
.directive('mypagination', function () {
return {
restrict: 'E',
scope: {
pageCount: "<",
onPageChange: "&"
},
template: "{{pageCount}}",
link: function (scope, elem, attrs) {
scope.$watch("pageCount", function(newValue) {
if(newValue)
scope.pages = [];
for (var i = 1; i <= newValue; i++) {
scope.pages.push(i);
}
}
});
}
}
})
In general, this type of data manupulation should be done in a directive's controller to take advantage of life-cycle hooks such as $onChanges. For more information, see AngularJS Developer Guide - Component-based Application Architecture.
Related
I have a service that's being used in different controllers, and I would like to consolidate them into a directive that can be applied to the different pages. To start, I'm just trying to get a simple example to work. Say I have two controllers, both of which have a common function:
angular.module('myApp')
.controller('CtrlOne', function() {
$scope.watchVar = 0;
$scope.changeVar = function() {
$scope.watchVar = 1;
}
});
angular.module('myApp')
.controller('CtrlTwo', function() {
$scope.watchVar = a;
$scope.changeVar = function() {
$scope.watchVar = b;
}
});
Then, in a directive I'm trying to determine the controller at runtime, so that this variable can be watched regardless of the controller the page has (as long as it uses the 'watchVar' variable):
angular.module('myApp').directive('myDirective', function () {
return {
scope: {
ctrl: '#myDirective',
watchVar: '=',
},
controller: ctrl,
link: function(scope, element, attrs) {
scope.$watch('watchVar', function(newValue, oldValue) {
if (newValue) {
console.log("watchVar changed to: "+newValue);
} else {
console.log("watchVar remained: "+oldValue);
}
}, true);
}
};
});
With the HTML being something like this:
<div p2a-filter-cache='CtrlOne'>
Is this even possible?
In the code below I have a directive that calculates a variable y every time an input field x is changed. The variable y is exposed, so it's available to the declaring controller/directive. This works fine but it's a simple abstraction, in my real scenario the computation of y is very expensive, so I cannot afford to calculate y every time x changes. Ideally, I would calculate y only when the declaring controller/directive needs it. Is there a way to achieve that?
var app = angular.module('app', []);
app.controller('ctl', function () {
});
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
link: function (scope, element, attrs) {
scope.xChanged = function() {
scope.y = scope.x * 2;
};
}
}
});
If you need this data from a child of this directive you can accomplish this by exposing a method in your directives controller and then exposing a method that the child directive can require.
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
controller: function (scope) {
scope.getY = function() {
return scope.x * 2;
};
}
}
});
And then your chid can require the parent can call that method.
app.directive("theDirectiveChild", function() {
return {
restrict: "A",
require: ["^theDirective"],
link: function(scope, element, attrs, ctrls){
var theDirective = ctrls[0];
var y = theDirective.getY();
}
}
});
EDIT: To do the opposite, where you want the parent to tell the child to update, you can utilize $scope.broadcast() This can fire a message down the scope chain, it would look something like this.
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
link: function (scope) {
scope.on('update-the-directive' , function() {
scope.y = scope.x * 2;
});
}
}
});
And then your chid can require the parent can call that method.
app.directive("theDirectiveParent", function() {
return {
restrict: "A",
link: function(scope, element){
scope.click = function() {
scope.$broadcast('update-the-directive');
}
}
}
});
I have two directives, calling 2nd directive from 1st directive.
This is my 1st directive
var initializeWidget = function ($compile, $timeout, $rootScope) {
return {
restrict: 'EA',
scope: {
maxImages: '#',
},
link: function (scope, element, attrs) {
if (!scope.cloudinaryFolder) {
throw 'folder value is missing in image uploader directive';
}
if (!scope.cloudinaryTags) {
throw 'tags value is missing in image uploader directive';
}
//1
attrs.$observe('maxImages', function (newMaxImages) {
console.log('varun==' + newMaxImages);
$timeout(function () {
angular.element(document.body).append($compile('<div class="sp-upload-widget" sp-upload-widget up-max-images="' + scope.maxImages + '"></div>')(scope));
scope.$apply();
}, 10);
});
}
};
};
I am calling my 2nd directive usixng angular.element used in above code.
Below is my 2nd directive:
var spUploadWidget = function ($q, Cloudinary, ENV) {
var templateUrl;
if ('dev' === ENV.name) {
templateUrl = '/seller/modules/uploadWidget/views/upload.html';
}
else if ('prod' === ENV.name) {
templateUrl = './views/upload.html';
}
return {
restrict: 'AE',
scope: {},
bindToController: {
maxImages: '=?upMaxImages',
},
replace: false,
controller: 'uploadWidgetController',
controllerAs: 'up',
templateUrl: templateUrl,
};
};
now in my controller when I am checking value of maxImages then it is giving the updated value but when I am using this variable to call API then it is holding the older value. Here is my controller
console.log('up===' + self.maxImages);
self.openUploader = function () {
self.closeModal();
ABC.UploaderInit( self.maxImages);
};
So when I change the value of maxImages in my directive
<div initialize-widget max-images="maxImages"></div>
It should give the updated value to my ABC.UploaderInit function
Found a solution for my problem,
I was getting this problem because I was calling 2nd directive whenever attribute of 1st directive was changing so I was creating multiple instances of my directive.
So now to handle this I am destroying the older instance of 2nd directive before I call the 2nd directive.
$rootScope.$on('destroySpUploadWidget', function (event, args) {
if (args.modalId === ctrl.modalId) {
scope.$destroy();
element.remove();
}
I have a problem updating a scope variable from a directive. my code looks something like this (simplified):
CONTROLLER
function newProjectController($scope) {
$scope.imageUploaded=false;
}
The directive should update the imageUploaded variable, which is then used to apply classes using ng-class function.
DIRECTIVE
.directive('ngThumb', ['$window', 'apiService', function($window, apiService) {
var helper = {
support: !!($window.FileReader && $window.CanvasRenderingContext2D),
isFile: function(item) {
return angular.isObject(item) && item instanceof $window.File;
},
isImage: function(file) {
var type = '|' + file.type.slice(file.type.lastIndexOf('/') + 1) + '|';
return '|jpg|png|jpeg|bmp|gif|'.indexOf(type) !== -1;
}
};
return {
restrict: 'A',
template: '<canvas/>',
bindToController: true,
link: function(scope, element, attributes) {
scope.$apply(function () {
scope.imageUploaded=true;
});
}
This is not happening and I don't know why...
Having a directive in angular that is a reusable component, what is the best practice to expose a public API that can be accessed from the controller?
So when there are multiple instances of the component you can have access from the controller
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
// this could be and exposed method
scope.changeLabel = function(newLabel) {
scope.configObj.label = newLabel;
}
}
}
});
Then when having:
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
How can I get the access the scope.changeLabel of extLabel2 in a controller?
Does it make sense?
Does this work for you?
angular.directive('extLabel', function() {
return {
restrict: 'E',
scope: {
api: '='
},
link: function(scope, iElement, iAttrs) {
scope.api = {
doSomething: function() { },
doMore: function() { }
};
}
};
});
From containing parent
<ext:label api="myCoolApi"></ext:label>
And in controller
$scope.myCoolApi.doSomething();
$scope.myCoolApi.doMore();
I like Andrej's and use this pattern regularly, but I would like to suggest some changes to it
angular.directive('extLabel', function {
return {
scope: {
api: '=?',
configObj: '='
},
// A controller, and not a link function. From my understanding,
// try to use the link function for things that require post link actions
// (for example DOM manipulation on the directive)
controller: ['$scope', function($scope) {
// Assign the api just once
$scope.api = {
changeLabel: changeLabel
};
function changeLabel = function(newLabel) {
$scope.configObj.label = newLabel;
}
}]
}
});
<ext-label name="extlabel1" config-obj="label1"></ext-label>
<ext-label api="label2api" name="extlabel2" config-obj="label2"></ext-label>
<ext-label name="extlabel3" config-obj="label3"></ext-label>
In controller of course label2api.changeLabel('label')
I faced this problem when writing a directive to instantiate a dygraph chart in my Angular applications. Although most of the work can be done by data-binding, some parts of the API require access to the dygraph object itself. I solved it by $emit()ing an event:
'use strict';
angular.module('dygraphs', []);
angular.module('dygraphs').directive('mrhDygraph', function ($parse, $q) {
return {
restrict: 'A',
replace: true,
scope: {data: '=', initialOptions: '#', options: '='},
link: function (scope, element, attrs) {
var dataArrived = $q.defer();
dataArrived.promise.then(function (graphData) {
scope.graph = new Dygraph(element[0], graphData, $parse(scope.initialOptions)(scope.$parent));
return graphData.length - 1;
}).then(function(lastPoint) {
scope.graph.setSelection(lastPoint);
scope.$emit('dygraphCreated', element[0].id, scope.graph);
});
var removeInitialDataWatch = scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
dataArrived.resolve(newValue);
removeInitialDataWatch();
scope.$watch('data', function (newValue, oldValue, scope) {
if ((newValue !== oldValue) && (newValue.length > 0)) {
var selection = scope.graph.getSelection();
if (selection > 0) {
scope.graph.clearSelection(selection);
}
scope.graph.updateOptions({'file': newValue});
if ((selection >= 0) && (selection < newValue.length)) {
scope.graph.setSelection(selection);
}
}
}, true);
scope.$watch('options', function (newValue, oldValue, scope) {
if (newValue !== undefined) {
scope.graph.updateOptions(newValue);
}
}, true);
}
}, true);
}
};
});
The parameters of the dygraphCreated event include the element id as well as the dygraph object, allowing multiple dygraphs to be used within the same scope.
In my opinion, a parent shouldn't access a children scope. How would you know which one to use and which one to not use. A controller should access his own scope or his parent scopes only. It breaks the encapsulation otherwise.
If you want to change your label, all you really need to do is change the label1/label2/label3 variable value. With the data-binding enabled, it should work. Within your directive, you can $watch it if you need some logic everytime it changes.
angular.directive('extLabel', function {
return {
scope: {
name: '#',
configObj: '='
},
link: function(scope, iElement, iAttrs) {
scope.$watch("configObj", function() {
// Do whatever you need to do when it changes
});
}
}
});
Use these directives on the element that you want to go prev and next:
<carousel>
<slide>
<button class="action" carousel-next> Next </button>
<button class="action" carousel-prev> Back </button>
</slide>
</carousel>
.directive('carouselNext', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsNext() {
if ((carousel.indexOfSlide(carousel.currentSlide) + 1) === carousel.slides.length) {
return 0;
} else {
return carousel.indexOfSlide(carousel.currentSlide) + 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsNext()]);
});
}
};
})
.directive('carouselPrev', function () {
return {
restrict: 'A',
scope: {},
require: ['^carousel'],
link: function (scope, element, attrs, controllers) {
var carousel = controllers[0];
function howIsPrev() {
if (carousel.indexOfSlide(carousel.currentSlide) === 0) {
return carousel.slides.length;
} else {
return carousel.indexOfSlide(carousel.currentSlide) - 1;
}
}
element.bind('click', function () {
carousel.select(carousel.slides[howIsPrev()]);
});
}
};
})