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;
});
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 :)
The Problem
Unable to update Model;
Using the set function I can change the ngModel value, and through the pipeline function $parsers, however the Del function uses splice to process the array, but it can not pass the pipeline function and realizes the update model value.
I tried to use $scope.$apply () to execute after ngModel.$setViewValue ($scope.images) and still can't be solved.
The version of the angular used is 1.6.6.
Online code, online code links, I hope you can help me to see where the problem is in the end.
View code
<div ng-controller="appController">
<div image-uploads ng-model="files"></div>
<p style="display: block; color: red">{{files}}</p>
</div>
Javascript code
var app = angular.module('app', []);
app.controller('appController', function ($scope) {
$scope.files = '1,2,3,4';
});
app.directive('imageUploads', function () {
return {
require: '?^ngModel',
restrict: 'EA',
template: '<div class="image-upload-box"><p class="image_upload" ng-repeat="image in images track by $index"><button ng-click="set()">setModel</button><button ng-click="del($index)">{{image}}</button></p></div>',
link: function ($scope, element, attrs, ngModel) {
ngModel.$formatters.push(function (modelValue) {
var images = new Array();
if (!ngModel.$isEmpty(modelValue)) {
var values = modelValue.split(",");
for (var j = 0; j < values.length; j++) {
images.push({
'id': values[j]
});
}
}
return images;
});
ngModel.$parsers.push(function (viewValue) {
var s = "";
for (var j = 0; j < viewValue.length; j++) {
if (viewValue[j].id != null) {
if (j > 0) {
s += ",";
}
s += viewValue[j].id;
}
}
return s;
});
ngModel.$render = function () {
$scope.images = ngModel.$viewValue;
};
$scope.del = function (i) {
$scope.images.splice(i, 1);
ngModel.$setViewValue($scope.images);
};
$scope.set = function () {
console.log('set');
$scope.images = [{id: 5}, {id: 6}, {id: 7}];
ngModel.$setViewValue($scope.images);
}
}
};
});
I have a data in my commonservice and I want to use the data in my template and I will assign a rootscope and use it but it isn't working right now and I am not sure what's wrong can anyone please suggest help.
My js:
function addGoogleAddress(id, data, scope) {
var places = new google.maps.places.Autocomplete(document.getElementById(id));
google.maps.event.addListener(places, 'place_changed', function () {
var place = places.getPlace();
if (place && place.address_components) {
for (var i = 0; i < (place.address_components.length); i++) {
if (place.address_components[i].types[0] === 'locality') {
data.city.key = '1001';
data.city.name = place.address_components[i].long_name.toString();
} else if (place.address_components[i].types[0] === 'administrative_area_level_1') {
data.state.key = '1001';
data.state.name = place.address_components[i].long_name.toString();
} else if (place.address_components[i].types[0] === 'country') {
data.country.key = '1001';
data.country.name = place.address_components[i].long_name.toString();
}
}
}
});
$timeout(function () {
$('#' + id).removeAttr('placeholder');
}, 500);
$rootScope.data = data;
}
My controller:
console.log($rootScope.data)
Here i am getting undefined.
You can not broadcast data directly, you need to broadcast event and pass parameter like follows,
Note: I am assuming here you have injected dependency
$rootScope.$broadcast('eventName',data);
Then in controller like follows -
$scope.$on('eventName',function(event,data){
console.log('data---',data);
})
app.controller('ctrl1',['$scope','$rootScope',function($scope,$rootScope) {
$scope.xyz = $rootScope.xyz;
//console.log($scope.xyz); hey
}]);
app.run(function($rootScope){
$rootScope.xyz ="hey";
})
I have created a access key Angular directive.
angular.module('tcne.common').directive("accessKey", function () {
return {
restrict: "A",
scope: {
},
link: function (scope, element, attrs) {
var $element = $(element);
$element.attr("accesskey", attrs.accessKey);
var content = $element.html();
for (var i = 0; i < content.length; i++) {
var char = content[i];
if (char.toLowerCase() === attrs.accessKey.toLowerCase()) {
content = content.substr(0, i) + "<u>" + char + "</u>" + content.substr(i + 1);
break;
}
}
$element.html(content);
},
replace: false
};
});
It underscores the access key in the button Label and adds the access key attribute to the element. Can I somehow prevent the accesskey from setting the button in focus? It kills the purpose of keyboard short cuts
edit: Rolled my own acccess key
angular.module('tcne.common').directive('accessKey', ['$compile', '$interval', function ($compile, $interval) {
var modifierPressed = false;
$("body").keyup(function (e) {
if (modifierPressed && !e.altKey) {
modifierPressed = false;
digestScopes();
}
});
$("body").keydown(function (e) {
modifierPressed = e.altKey;
if (modifierPressed && scopes.hasOwnProperty(String.fromCharCode(e.which).toLowerCase())) {
var scope = scopes[String.fromCharCode(e.which).toLowerCase()];
scope.handle();
return;
}
if (modifierPressed) {
e.preventDefault();
digestScopes();
}
});
function digestScopes() {
for (var index in scopes) {
if (scopes.hasOwnProperty(index)) {
var scope = scopes[index]
scope.$digest();
}
}
}
function isModifierPressed() {
return modifierPressed;
}
var scopes = {};
return {
restrict: 'A',
scope: {
},
link: function (scope, element, attrs) {
var key = attrs.accessKey.toLowerCase();
var content = element.html();
var char;
for (var i = 0; i < content.length; i++) {
char = content[i];
if (char.toLowerCase() === key) {
content = content.substr(0, i) + '<u><strong ng-if="highlight()">{{char}}</strong><span ng-if="!highlight()">{{char}}</span></u>' + content.substr(i + 1);
break;
}
}
element.html(content);
var underscoreScope = scope.$new();
underscoreScope.char = char;
underscoreScope.highlight = isModifierPressed;
underscoreScope.handle = element.click.bind(element);
scopes[key] = underscoreScope;
scope.$on('$destroy', function () {
delete scopes[key];
});
$compile(element.find("u"))(underscoreScope);
},
replace: false
};
}]);
It also highlights the access key button when alt key is pressed which is nice
Any pit falls with this code? Thanks
Found a pitfall, element.html and then $compile will break any directives inside the element that is already compiled. So I changed to
var captionElement = element.contents().first(":text");
var content = captionElement.text();
And then I add my custom content like
var view = $("<span>").html(content);
captionElement.replaceWith(view);
$compile(view)(vm);
Please let me know if this is considered bad practice
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;
};