Having trouble getting a directive to display properly with data that is updated via promise. The first directive updates perfectly, but the second and third do not.
Plunk
First, the data:
app.controller('MainCtrl', function($scope, $timeout) {
$scope.data = {};
$timeout(function() {
angular.copy({totalItems: 100, pageSize: 10, currentPage: 1 }, $scope.data);
}, 3000);
});
Then the directives:
var template1 = '{{(data.pageSize * (data.currentPage - 1)) + 1}} - {{data.pageSize * data.currentPage}} of {{data.totalItems}}';
var template2 = '{{lower}} - {{upper}} of {{total}}';
var dir = {
restrict: 'C',
template: template1,
scope: false
};
app.directive('pagination', function() {
return dir;
});
app.directive('pagination2', function() {
dir.template = template2;
dir.link = function(scope, element, attrs) {
scope.lower = (attrs.size * (attrs.currentPage - 1)) + 1;
scope.upper = (attrs.size * attrs.currentPage);
scope.total = attrs.total;
};
return dir;
});
app.directive('pagination3', function() {
dir.template = template2;
dir.link = function(scope, element, attrs) {
scope.lower = (scope.data.pageSize * (scope.data.currentPage - 1)) + 1;
scope.upper = (scope.data.pageSize * scope.data.currentPage);
scope.total = scope.data.totalItems;
};
return dir;
});
And lastly the markup:
<div class="pagination"></div>
<div class="pagination2" total="{{data.totalItems}}" size="{{data.pageSize}}" current-page="{{data.currentPage}}"></div>
<div class="pagination3"></div>
I understand why the first one works. The $timeout finishes, my $scope is updated, dirty check ensues, everything is recalculated, and all is well.
In the second one I think I understand why it doesn't update - because the data is not there when I pass values to the element attributes, meaning the entire directive is built using undefined values. I would like to somehow correct this and have the directive update when $scope.data updates.
I figured I'd solve the problem I was having with the second directive with the third directive but am quite confused why this one doesn't work. I thought I had direct access to the $scope because my directive is using scope: false, but apparently not.
app.directive('pagination3', function() {
dir.template = template2;
dir.link = function(scope, element, attrs) {
scope.lower = (scope.data.pageSize * (scope.data.currentPage - 1)) + 1;
scope.upper = (scope.data.pageSize * scope.data.currentPage);
scope.total = scope.data.totalItems;
};
return dir;
});
Per your code, you are assigning scope.data.totalItems at directive's linking phase but at that time scope.data is just an empty object {}, it has no property called totalItems or pageSize or currentPage. lower, upper and total will be undefined after the assignment.
If you really don't want to put the calculation in your view template, you can monitor scope.data's change as below
app.directive('pagination3', function() {
dir.template = template2;
dir.link = function(scope, element, attrs) {
scope.$watch('data', function(newVal, oldVal) {
scope.lower = (newVal.pageSize * (newVal.currentPage - 1)) + 1;
scope.upper = (newVal.pageSize * newVal.currentPage);
scope.total = newVal.totalItems;
}, true);
};
return dir;
};
Related
I am trying to create a directive that works like the ng-if directive, so I would like to grab it's functionality. I created a directive like this:
(function () {
'use strict';
angular.module('sapphire.directives').directive('ifScreensize', directive);
function directive(ngIfDirective, $window) {
var ngIf = ngIfDirective[0];
return {
controller: 'IfScreensizeController',
prority: 1,
scope: {
options: '=ifScreensize'
},
link: function (scope, element, attrs, controller) {
scope.$watch('options', function (options) {
controller.handle(element, options, ngIf);
});
var window = angular.element($window)
window.bind('resize', function () {
$timeout(function () {
controller.handle(element, scope.options, ngIf);
}, 500);
});
}
};
};
})();
And then the controller looks like this:
(function () {
'use strict';
angular.module('sapphire.directives').controller('IfScreensizeController', controller);
function controller(ifScreensizeService) {
this.handle = ifScreensizeService.handle;
};
})();
And finally, the service looks like this:
(function () {
'use strict';
angular.module('sapphire.directives').service('ifScreensizeService', service);
function service($window) {
return {
handle: handle
};
//////////////////////////////////////////////////
function handle(element, options, ngIf) {
var window = angular.element($window),
width = $window.innerWidth,
value = true;
switch (options.operator) {
case '>':
value = options.width >= width;
break;
case '>=':
value = options.width > width;
break;
case '<':
value = options.width < width;
break;
case '<=':
value = options.width <= width;
break;
default:
break;
}
ngIf.link.apply(ngIf, value);
};
};
})();
The problem is, when I try to use the directive I get an error:
TypeError: CreateListFromArrayLike called on non-object
Which is on the ngIf.link.apply(ngIf, value); line.
Can someone tell me what i need to do to get the directive to work?
.apply takes an array. Try calling it this way ngIf.link.apply(ngIf, [value]);
Ok, so I used the actual ng-if directive code to create my directive.
So, I changed the directive to this:
angular.module('sapphire.directives').directive('ifScreensize', directive);
function directive($timeout, $window) {
return {
controller: 'IfScreensizeController',
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
bindToController: {
options: '=ifScreensize'
},
link: function (scope, element, attrs, controller, transclude) {
scope.$watch('options', function (options) {
controller.handle(element, attrs, transclude);
});
var window = angular.element($window)
window.bind('resize', function () {
$timeout(function () {
controller.handle(element, attrs, transclude);
}, 500);
});
}
};
};
And I changed the controller to mimic the ng-if directive like this:
angular.module('sapphire.directives').controller('IfScreensizeController', controller);
function controller($animate, $compile, ifScreensizeService) {
var self = this;
var block, childScope, previousElements;
self.handle = function handle($element, $attr, $transclude) {
var value = ifScreensizeService.evaulate(self.options);
console.log(value);
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (previousElements) {
console.log(previousElements);
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = ifScreensizeService.getBlockNodes(block.clone);
$animate.leave(previousElements).done(function(response) {
if (response !== false) previousElements = null;
});
block = null;
}
}
};
};
The bulk of the code there, is found in the ng-if directive. I just modified it slightly to work with my directive.
One thing to notice is that in the original ng-if directive, it invokes getBlockNodes which we do not have access to, so I added this to my service:
angular.module('sapphire.directives').service('ifScreensizeService', service);
function service($window) {
var slice = [].slice;
return {
evaulate: evaulate,
getBlockNodes: getBlockNodes
};
//////////////////////////////////////////////////
function evaulate(options) {
var window = angular.element($window),
width = $window.innerWidth,
value = true;
switch (options.operator) {
case '>':
value = width >= options.width;
break;
case '>=':
value = width > options.width;
break;
case '<':
value = width < options.width;
break;
case '<=':
value = width <= options.width;
break;
default:
break;
}
console.log(options, width, value);
return value;
};
function getBlockNodes(nodes) {
// TODO(perf): update `nodes` instead of creating a new object?
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes;
for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
console.log(nodes);
blockNodes = angular.element(slice.call(nodes, 0, i));
}
blockNodes.push(node);
}
}
return blockNodes || nodes;
};
};
And the last caveat here was this line:
blockNodes = angular.element(slice.call(nodes, 0, i));
In the original ng-if, it is actually:
blockNodes = jqLite(slice.call(nodes, 0, i));
I struggled to get this to work, but basically the method jqLite is actually doing an angular.element() call. The slice method won't work unless you do var slice = [].slice; which I have done at the top of the service.
I hope this helps someone else :)
I'm building a weather app using Angular 1.5.8 and need to give users the ability to toggle back and forth between imperial and metric measurements for the temperature and wind speed.
The toggle option and all weather information (fetched from an external API) are located in separate directives, but I've thought about moving the temp and wind speed data to the same directive as the toggle option, then using either $broadcast or $emit to display the data and conversions in the weather directive. Is that the best way to go about doing this? If not, what would be?
Directive where the toggle is located:
app.directive('topBar', topBar);
function topBar() {
return {
template:
'<div class="changeTemp" ng-click="vm.changeTempUnit()">' +
'<span ng-class="vm.fahrClass">°F</span>' +
'<span>/</span>' +
'<span ng-class="vm.celsClass">°C</span>' +
'</div>',
restrict: 'E',
scope: {},
controller: TopBarController,
controllerAs: 'vm'
};
}
function TopBarController() {
var vm = this;
vm.celsClass = 'unselected';
vm.changeTempUnit = changeTempUnit;
vm.fahrClass = 'selected';
vm.temp;
vm.windSpeed;
function changeTempUnit() {
if (vm.fahrClass === "selected") {
vm.fahrClass = 'unselected'; //F unselected
vm.celsClass = 'selected'; //C selected
vm.temp = Math.round((vm.temp - 32) * 5 / 9); //Celsius
vm.windSpeed = (vm.speed * 0.44704).toFixed(0); // M/S
} else if (vm.celsClass === 'selected') {
vm.celsClass = 'unselected'; //C unselected
vm.fahrClass = 'selected'; //F selected
vm.temp = Math.round(vm.temp * 1.8 + 32); //Fahren
vm.windSpeed = (vm.speed / 0.44704).toFixed(0); //MPH
}
}
}
Directive where the weather is displayed
app.directive('weather', weather);
function weather() {
return {
template:
'<div>' +
'Temp: {{vm.temp}}°' + '<br>' +
'Wind Speed: {{vm.windSpeed}}' +
'</div>',
restrict: 'E',
scope: {},
controller: WeatherController,
controllerAs: 'vm'
};
}
WeatherController.$inject = ['weatherService'];
function WeatherController(weatherService) {
var vm = this;
vm.temp;
vm.windSpeed;
activate();
function activate() {
return weatherService.getWeather().then(function(data) {
weatherInfo(data);
});
}
function weatherInfo(data) {
vm.temp = Math.round(data.main.temp); //Fahren
vm.windSpeed = (data.wind.speed).toFixed(0); //MPH
}
}
Plunker link
Use Components
My first recommendation is to use the AngularJs 1.5+ component api. Components assume several of the directive definition object values you've already chosen.
If your answer is yes to these questions then you should be using the component api instead.
Does your directive have a template?
Does your directive have an isolated scope?
Does your directive have a controller?
Is your directive restricted to elements?
Converting your <top-bar> to a component would look like this
app.component('topBar', {
template:
'<div class="changeTemp" ng-click="$ctrl.changeTempUnit()">' +
'<span ng-class="$ctrl.fahrClass">°F</span>' +
'<span>/</span>' +
'<span ng-class="$ctrl.celsClass">°C</span>' +
'</div>',
controller: TopBarController,
bindings: {}
});
function TopBarController() {
...
}
Notice how the template uses $ctrl to refer to the controller instead of vm. With components, $ctrl is the default.
Possible Solution
emit and broadcast can be used and this is probably an ideal place to use them! but if you can avoid them, then don't rely on them.
Here is one option
Move computation to a service
Topbar
app.component('topBar', {
template:
'<div class="changeTemp" ng-click="$ctrl.changeTempUnit()">' +
'<span ng-class="$ctrl.fahrClass">°F</span>' +
'<span>/</span>' +
'<span ng-class="$ctrl.celsClass">°C</span>' +
'</div>',
controller: ['conversionService', TopBarController],
bindings: {
}
})
function TopBarController(conversionService) {
var vm = this;
vm.celsClass = 'unselected';
vm.changeTempUnit = changeTempUnit;
vm.fahrClass = 'selected';
function changeTempUnit() {
if (vm.fahrClass === "selected") {
vm.fahrClass = 'unselected'; //F unselected
vm.celsClass = 'selected'; //C selected
conversionService.selectedUnit = conversionService.tempUnits.celsius;
} else if (vm.celsClass === 'selected') {
vm.celsClass = 'unselected'; //C unselected
vm.fahrClass = 'selected'; //F selected
conversionService.selectedUnit = conversionService.tempUnits.farhenheit;
}
}
}
ConversionService
app.service('conversionService', function() {
var service = this;
service.tempUnits = {
farhenheit: 'farhenheit',
celsius: 'celsius'
};
service.selectedUnit = 'farhenheit';
service.convertTemperature = function(temp, tempUnit) {
if (service.selectedUnit === tempUnit) {
return temp;
} else if (service.selectedUnit === service.tempUnits.farhenheiht) {
return Math.round(temp * 1.8 + 32);
} else if (service.selectedUnit === service.tempUnits.celsius) {
return Math.round((temp - 32) * 5 / 9);
} else {
throw Error("Invalid unit");
}
}
});
Weather
app.component('weather', {
template:
'<div>' +
'Temp: {{ $ctrl.getTemp() }}°' +
'<br>' +
'Wind Speed: {{ $ctrl.windSpeed }}' +
'</div>',
controller: ['conversionService', 'weatherService', WeatherController],
bindings: {}
});
function WeatherController(conversionService, weatherService) {
var ctrl = this;
ctrl.temp;
ctrl.windSpeed;
ctrl.conversionService = conversionService;
activate();
function getTemp() {
return ctrl.conversionService.convertTemperature(ctrl.temp, ctrl.conversionService.tempUnits.farhenheit);
}
function activate() {
return weatherService.getWeather()
.then(weatherInfo);
}
function weatherInfo(data) {
ctrl.temp = Math.round(data.main.temp); //Fahren
ctrl.windSpeed = (data.wind.speed).toFixed(0); //MPH
}
}
Updated version of your plunk
Since Angular does dirty checking when directives like ng-click evaluate their bound expressions the template of <weather> will also be dirty checked and the expression
{{ $ctrl.conversionService.convertTemperature($ctrl.temp, $ctrl.conversionService.tempUnits.farhenheit) }}
will be evaluated.
Below is my directive, which is an utility (reusable).
var ChartNavigationDirective = { 'ChartNavigation': function (Items) {
return {
restrict: 'EA',
require: '?ngModel',
replace: false,
template: '<div class="chart-nav">' +
' </div>',
scope: {
options: '=ChartNavigation'
},
link: function (scope, element, attrs) {
scope.parameters = {
"prev_arrow": "#prev-arrow",
"next_arrow": "#next-arrow"
};
Items.getItems(scope.options.Source, scope.options.Type).then(function (d) {
scope.links = d.Items;
for (var link in scope.links) {
scope.links[link].Id = link;
}
var chartList = scope.links;
setNavigation(chartList);
});
var setNavigation = function (chartList) {
scope.totalCharts = chartList.length;
if (scope.totalCharts <= 0) {
$(scope.parameters.next_arrow).removeClass("on").addClass("off");
$(scope.parameters.prev_arrow).removeClass("on").addClass("off");
}
if (scope.totalCharts > 0) {
scope.currentItem = scope.links[0];
scope.currentIndex = Number(scope.currentItem.Id) + 1;
$(scope.parameters.prev_arrow).removeClass("off").addClass("on");
$(scope.parameters.next_arrow).removeClass("off").addClass("on");
}
updateNavigation();
};
var updateNavigation = function () {
if (scope.currentIndex <= 1) {
$(scope.parameters.prev_arrow).removeClass("on").addClass("off");
}
else {
$(scope.parameters.prev_arrow).removeClass("off").addClass("on");
}
if (scope.currentIndex >= scope.totalCharts) {
$(scope.parameters.next_arrow).removeClass("on").addClass("off");
}
else {
$(scope.parameters.next_arrow).removeClass("off").addClass("on");
}
};
scope.Previous = function () {
var currentIdx = scope.currentIndex;
var previousIdx = currentIdx - 1;
if (previousIdx >= 0) {
scope.currentItem = scope.links[previousIdx - 1];
scope.currentIndex = Number(scope.currentItem.Id) + 1;
}
updateNavigation();
};
scope.Next = function () {
var currentIdx = scope.currentIndex;
var nextIdx = currentIdx + 1;
if (nextIdx <= scope.totalCharts) {
scope.currentItem = scope.links[nextIdx - 1];
scope.currentIndex = Number(scope.currentItem.Id) + 1; ;
}
updateNavigation();
};
}
};
}
};
I would like to watch scope.currentItem from my controller. I did try using broadcast it's working fine. But I would like use watch instead. Here is my controller.
var myController = function ($scope) {
$scope.templateUrl = '/_layouts/AngularControls/myController/Views/Charts.html';
$scope.currentConfig = '';
// $rootScope.$on('curentConfig', function (event, args) {
// $scope.currentConfig = args.data;
// });
$scope.$watch("currentItem", function () {
alert(JSON.stringify($scope.currentItem));
});
}
Can anyone point out where I am doing mistake ? If there any suggestions Please let me know.
You're trying to watch a directive scope variable from a controller. That won't work because they are two different scopes (you are using an isolate scope in your directive).
You can watch from the directive controller (but I'm not sure that's what you want), or simply pass in the main controller scope variable into the directive, and have the directive manipulate it instead of its own scope variable. I guess using scope.$parent.currentItem is also possible, but definitely not recommended because of the directive reusability.
Broadcasting is also fine, not sure why you don't want to do it.
This is your structure:
controller scope
- directive scope
currentItem
And you're watching for this:
controller scope
watching for currentItem here, it does not exist
- directive scope
currentItem
I figured it my self. Using $rootScope in both directive an controller and watching for the $rootScope variable has resolved the issue.
In Direcive :
$rootScope.currentItem= myItem;
In Controller:
$scope.$watch("currentItem", function () {
$scope.currentConfig = $rootScope.currentItem;
});
I am trying to do the custom star rating with angular.js, where I will have different set of images. I need to change it dynamically on hover the image. I am having 5 images
X X X X X
if I move the mouse pointer to 4th X I should be able to dynamically change
X
I used directive to achieve it.
.directive('fundooRating', function () {
return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" ng-class="star"
ng-click="toggle($index)"><img ng-mouseenter="hoveringOver($index)"
ng-src="{{con}}" />' +
'',
scope: {
ratingValue: '=',
max: '=',
readonly: '#',
onRatingSelected: '&'
},
link: function (scope, elem, attrs) {
var updateStars = function() {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({filled: i < scope.ratingValue});
}
};
scope.con = "Images/Rating/empty.png";
scope.hoveringOver = function(index){
var countIndex = index+1;
scope.Condition = "Good.png"
console.log("Hover " + countIndex);
};
scope.toggle = function(index) {
if (scope.readonly && scope.readonly === 'true') {
return;
}
scope.ratingValue = index + 1;
scope.onRatingSelected({rating: index + 1});
};
scope.$watch('ratingValue', function(oldVal, newVal) {
if (newVal) {
updateStars();
}
});
}
} });
How can I able to find which image my mouse pointer is and how to change the rest of Images. I want to do the custom rating option.
Angular UI gives you premade directives for the same purpose, did you try it?
http://angular-ui.github.io/bootstrap/
Go down to the Rating Title in the same page, i think it might solve your purpose.
You'll need a condition for each star in your updateStars function, either as a property for each, or a separate array. Then, you can do something like this:
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.stars[i].Condition = "Good.png";
}
};
Or the separate array way (assuming the array is scope.conditions):
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.conditions[i] = "Good.png";
}
};
You also need a function opposite of hoveringOver to remove the states to default/previous versions.
I'm learning AngularJs now, and trying to write my first directives.
So i have a question: is there any way to pass complex options to directive. For example i want to write directive wrapper for slick grid. It has a lot of options, columns for example, and it's imposible to configure it using attributes. Can i do simething like this?
<s-grid>
<s-grid-columns>
<s-grid-column id="title" title="Title"/>
<s-grid-column id="duration" title="Duration"/>
</s-grid-columns>
...
</s-grid>
And get all this properties as json object in s-grid directive?
So i could do it. Is it here any mistakes?
module
.directive('sGrid', [function () {
return {
restrict: 'E',
controller: function($scope) {
$scope.columns = [];
this.setColumns = function(columns) {
$scope.columns = columns;
};
},
link: function (scope, element, attrs, controller, transclude) {
// for clearer present I initialize data right in directive
// start init data
var columns = scope.columns;
var options = {
enableCellNavigation: true,
enableColumnReorder: true
};
var data = [];
for (var i = 0; i < 50000; i++) {
var d = (data[i] = {});
d["id"] = "id_" + i;
d["num"] = i;
d["title"] = "Task " + i;
d["duration"] = "5 days";
d["percentComplete"] = Math.round(Math.random() * 100);
d["start"] = "01/01/2009";
d["finish"] = "01/05/2009";
d["effortDriven"] = (i % 5 == 0);
}
// end init data
// finally render layout
scope.grid = new Slick.Grid(element, data, columns, options);
$(window).resize(function () {
scope.grid.resizeCanvas();
})
}
}
}])
.directive("sGridColumns", function(){
return {
require: '^sGrid',
restrict: 'E',
controller: function($scope) {
var columns = $scope.columns = [];
this.addColumn = function(pane) {
columns.push(pane);
};
},
link: function (scope, element, attrs, gridCtrl){
gridCtrl.setColumns(scope.columns);
}
}
})
.directive('sGridColumn', function() {
return {
require: '^sGridColumns',
restrict: 'E',
transclude: 'element',
link: function (scope, element, attrs, gridCtrl) {
scope.field = scope.field || scope.id;
scope.title = scope.title || scope.field;
gridCtrl.addColumn({
id: attrs.id,
field: attrs.field || attrs.id,
name: attrs.name || attrs.field || attrs.id
});
}
};
});
And declaration:
<s-grid>
<s-grid-columns>
<s-grid-column id="title" name="Title"></s-grid-column>
<s-grid-column id="duration" name="Duration"></s-grid-column>
</s-grid-columns>
</s-grid>