custom ng-if directive angularjs - javascript

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 :)

Related

Config is undefined for $scope.model property

I am implementing a controller for a content picker in Umbraco 7, where I need to change the start node to match a specific content node. However wen I load up the page with the content picker I receive an error saying:
"Cannot read property 'config' of undefined"
In relation to this piece of code:
$scope.model.config.StartNodeId = 1083;
if ($scope.model.config.StartNodeId) {
options.startNodeId = $scope.model.config.StartNodeId;
}
My entire controller:
angular.module("umbraco").controller("UIOMatic.FieldEditors.Pickers.ContentController",
function ($scope, $routeParams, $http, dialogService, entityResource, iconHelper) {
function init() {
if (!$scope.setting) {
$scope.setting = {};
}
var val = parseInt($scope.property.value);
if (!isNaN(val) && angular.isNumber(val) && val > 0) {
$scope.showQuery = false;
entityResource.getById(val, "Document").then(function (item) {
item.icon = iconHelper.convertFromLegacyIcon(item.icon);
$scope.node = item;
});
}
$scope.openContentPicker = function () {
var d = dialogService.treePicker({
section: "content",
treeAlias: "content",
multiPicker: false,
callback: populate
});
};
$scope.model.config.StartNodeId = 1083;
if ($scope.model.config.StartNodeId) {
options.startNodeId = $scope.model.config.StartNodeId;
}
$scope.clear = function () {
$scope.id = undefined;
$scope.node = undefined;
$scope.property.value = undefined;
};
function populate(item) {
$scope.clear();
item.icon = iconHelper.convertFromLegacyIcon(item.icon);
$scope.node = item;
$scope.id = item.id;
$scope.property.value = item.id;
}
};
if ($scope.valuesLoaded) {
init();
} else {
var unsubscribe = $scope.$on('valuesLoaded', function () {
init();
unsubscribe();
});
}
});
I tried changing the start node ID to 1083, which is what I want, and I can open the content picker just fine, but it won't allow me to save my changes. It also allows for multi-picking, which I have set to false in my config object.
This is the documentation of the content picker from the author:
http://uiomatic.readthedocs.io/en/stable/02.DefaultEditorViews/#content-picker
I think You should initialize $scope.model before assigning value to its object.
use
$scope.model = {}

Disable that acceess key sets focus to button

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

Directive needs data updated after element is rendered

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;
};

Moving code into a factory in an Angular app

I am trying to clean up a controller that has too many lines of code in it. In the controller below where you find a function called getProductDetails, I would like to move the filter to a factory or a service, but I am not sure how to do it.
'use strict';
(function () {
var userQuoteBuild = angular.module('priceApp');
userQuoteBuild.controller('quoteBuilderController', function ($scope, $http) {
// loads of controller logic here...
$scope.getProductDetails = function (item) {
$scope.listOfProductVariants = item.default_variant_attributes;
// TODO: put this in its own factory?
$scope.selectedProductAttributes = $scope.listOfAttributes.filter(function (item) {
var validated = false, i, length = $scope.listOfProductVariants.length;
for (i = 0; i < length; i++) {
if (item.name === $scope.listOfProductVariants[i]){
validated = true;
}
}
return validated;
});
};
});
(function () {
'use strict';
angular
.module('priceApp')
.factory('filterService', filterService);
function filterService() {
var service = {
getValidated: getValidated
}
return service;
function getValidated(list, variants) {
return list.filter(function (item) {
var validated = false, i, length = variants.length;
for (i = 0; i < length; i++) {
if (item.name === variants[i]) {
validated = true;
}
}
return validated;
});
}
}
})();
Simply inject this filterService to your controller and then use it as in example here:
$scope.selectedProductAttributes = filterService
.getValidated($scope.listOfAttributes,
$scope.listOfProductVariants)
I followed John Papa's AngularJS Style Guide. Make sure to choose a better name than filterService. : )
Check this:
userQuoteBuild.factory('myService', function() {
var service = {
getProductDetails: function(item) {
// your logic
return value;
}
}
return service;
});

Watch Directive Scope Variable From Controller

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;
});

Categories