Angular Custom Directive - How to make it Behave Like Ng-if - angularjs

I'm trying to build a custom directive to either show items on a page or completely remove them based on authorization data, and I'm clearly missing something, 'cuz its not working and I'm having a hard time figuring out why.
I've been following the guide here:
http://adamalbrecht.com/2014/09/22/authorization-with-angular-and-ui-router/
Which said to take a copy of the NgIf source code, and modify it (as I have below).
I'm really confused because not only is it not working as expected, but even when I put break-points within the function calls in the directive, it never hits those break points.
I'm not sure what I'm doing wrong. Is there something else not documented within the steps that I need to do in order to use a custom directive, or did I somehow miss a step? Regular ng-if's work on the same page just fine.
EDIT: I should note that AuthorizeService.isAuthorizedForOne returns a promise value which is either true or false. This works fine in other contexts.
'use strict';
/**
* #ngdoc directive
* #name ngIfPermission
* #restrict A
*
* #description
**/
var ngIfPermissionDirective = ['$animate', function($animate, AuthorizeService) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function($scope, $element, $attr, ctrl, $transclude) {
console.log("I am here");
var block, childScope, previousElements;
$attr.$observe("ngIfPermission", function(value){
console.log("I am there");
var permissions = JSON.parse(value.replace(/'/g, '"'));
AuthorizeService.isAuthorizedForOne(permissions).then(function(auth){
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end ngIfPermission: ' + $attr.ngIfPermission + ' ');
// 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);
});
}
},
function(err) {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
});
});
}
};
}];
How I'm referencing it:
<div ng-if-permission="['OOGY']">You can see this.</div>
<div ng-if-permission='["BOOGY"]'>or this</div>

I think you might got the declaration for the directive wrong.
app.directive( 'ngIfPermissionDirective', function($animate){
//directive here
));
DEMO http://plnkr.co/edit/BhubrfMAiW3K4ANI3pTx

Related

Implementing component require property in Angular 1.5 components

I am having no joy with implementing require: {} property as part of an angular component. Allow me to demonstrate with an example I have.
This is the component/directive that supposed to fetch a list of judgements. Nothing very fancy, just a simple factory call.
// judgements.component.js
function JudgementsController(GetJudgements) {
var ctrl = this;
ctrl.Get = function () {
GetJudgements.get().$promise.then(
function (data) {
ctrl.Judgements = data.judgements;
}, function (error) {
// show error message
});
}
ctrl.$onInit = function () {
ctrl.Get();
};
}
angular
.module('App')
//.component('cJudgements', {
// controller: JudgementsController,
//});
.directive('cJudgements', function () {
return {
scope: true,
controller: 'JudgementsController',
//bindToController: true,
};
});
I am trying to implement component require property to give me access to ctrl.Judgements from the above component/directive as follows:
// list.component.js
function ListController(GetList, GetJudgements) {
var ctrl = this;
ctrl.list = [];
ctrl.Get = function () {
GetList.get().$promise.then(
function (data) {
ctrl.list = data.list;
}, function (error) {
// show error message
});
};
//ctrl.GetJudgements = function () {
// GetJudgements.get().$promise.then(
// function (data) {
// ctrl.Judgements = data.judgements;
// }, function (error) {
// // show error message
// });
//}
ctrl.$onInit = function () {
ctrl.Get();
//ctrl.GetJudgements();
};
}
angular
.module('App')
.component('cTheList', {
bindings: {
listid: '<',
},
controller: ListController,
controllerAs: 'ctrl',
require: {
jCtrl: 'cJudgements',
},
template: `
<c-list-item ng-repeat="item in ctrl.list"
item="item"
judgements="ctrl.Judgements"></c-list-item>
<!--
obviously the reference to judgements here needs to change
or even better to be moved into require of cListItem component
-->
`,
});
Nice and simple no magic involved. A keen reader probably noticed GetJudgement service call in the ListController. This is what I am trying to remove from TheList component using require property.
The reason? Is actually simple. I want to stop database being hammered by Judgement requests as much as possible. It's a static list and there is really no need to request it more than once per instance of the app.
So far I have only been successful with receiving the following error message:
Error: $compile:ctreq
Missing Required Controller
Controller 'cJudgements', required by directive 'cTheList', can't be found!
Can anyone see what I am doing wrong?
PS: I am using angular 1.5
PSS: I do not mind which way cJudgement is implemented (directive or component).
PSSS: If someone wonders I have tried using jCtrl: '^cJudgements'.
PSSSS: And multiple ^s for that matter just in case.
Edit
#Kindzoku posted a link to the article that I have read before posting the question. I hope this also helps someone in understanding $onInit and require in Angular 1.5+.
Plunker
Due to popular demand I made a plunker example.
You should use required components in this.$onInit = function(){}
Here is a good article https://toddmotto.com/on-init-require-object-syntax-angular-component/
The $onInit in your case should be written like this:
ctrl.$onInit = function () {
ctrl.jCtrl.Get();
};
#iiminov has the right answer. No parent HTML c-judgements was defined.
Working plunker.

Dynamically added element's directive doesn't work

I'm trying to build a simple infinite scroll. It loads the data fine but after loading, new added elements' directives don't work.
This is relevant part of the scroll checking and data loading directive.
.directive("scrollCheck", function ($window, $http) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
// calculating windowBottom and docHeight here then
if (windowBottom >= (docHeight - 100)) {
// doing some work here then
$http.get('service page').then(function (result) {
if (result.data.trim() != "") {
var newDiv = angular.element(result.data);
element.append(newDiv);
}
// doing some other work
},function () {
// error handling here
});
}
scope.$apply();
});
};
})
Service page returns some repeats of this structure as result.data
<div ...>
<div ... ng-click="test($event)"></div>
<div ...>...</div>
</div>
As i said data loads just fine but those test() functions in ng-clickdirectives don't work. How to get em work?
I believe you are going to need to compile the html element returned. Something like this
$compile(newDiv)(scope); // Corrected. Thanks
You'll need to be sure and pass in $compile into your function

angularjs directive communication

I am new to angularjs, and I have been reading a ton of documentation and reading through various articles and tutorials as well as videos to help me figure this stuff out.
I am trying to get two directives to interchange information between themselves. a really simplified version of what i am trying to do is at odetocode (http://odetocode.com/blogs/scott/archive/2013/09/11/moving-data-in-an-angularjs-directive.aspx) where k scott allen has wrapped his directives with a div that has the ng-controller attribute it works beautifully.
I have a slightly more complex test I am working on, and I am trying to get it to work similarly to the code I have mentioned.
my two directives talk to each other when I list the ng-controller attribute in the actual template for each directive. it works, but I don't think it is correct. the actual controller code is run twice, once for each directive.
when I move the controller into the div that wraps the two directives, the two directives stop interacting (the change event in the location-selector template doesn't change the park in the controller). I am pretty sure it has something to do with the scope. if anyone can point me in the right direction, or where I should look for information, that would be much appreciated.
here is my fiddle showing my code http://jsfiddle.net/jgbL9/25/
<div ng-app="myApp">
<location-selector ></location-selector ><br/>
<portal-map ></portal-map >
</div>
var App = angular.module('myApp', ['ngResource']);
App.directive('locationSelector',['parkList', function(parkList) {
return {
restrict: 'E',
scope: {
parkId : '=',
parkName : '='
},
template: '<select ng-controller="portalMapCtrl"'+
' ng-model="listParks" ng-change="changePark()" '+
' park-id="parkId" park-name="parkName" ' +
' ng-options="park as park.attributes.NAME for park in Parks" >'+
'</select>',
link: function (scope,element,attrs){
parkList.getListFromGIS().success(function(data) {
scope.Parks = data.features;
});
}
};
}]);
App.directive('portalMap', function(){
return {
restrict: 'E',
scope:{
parkId: "=",
parkName: "="
},
template: '<style type="text/css" media="screen">'+
'#mapCanvas {height: 500px; width:75%; border:thin black solid; }'+
'</style>'+
'<div id="mapCanvas" park-id="parkId" park-name="parkName" ng-controller="portalMapCtrl" ></div>'
}
});
App.controller('portalMapCtrl',['$scope','parkList', function( $scope, parkList ){
var map = {};
var STREETMAPSERVICE = "https://gis.odot.state.or.us/ArcGIS/rest/services/BASEMAPS/Basemap_Streets/MapServer";
var FOTOSSERVICE = "https://maps.prd.state.or.us/arcgis/rest/services/ESRI_TEST/MapServer?f=jsapi";
var UTILSSERVICE = "http://gis.prd.state.or.us/ArcGIS/rest/services/OPRDAssets/MapServer";
var UTILSSERVICE_PARKLAYER = 0;
var UTILSSERVICE_STRUCTUREPOLY = 7;
var UTILSSERVICE_SURFACE = 11;
var UTILSSERVICE_PARCELS = 12;
var timer;
var ALL_LAYERS = [UTILSSERVICE_PARKLAYER,UTILSSERVICE_STRUCTUREPOLY,UTILSSERVICE_SURFACE,UTILSSERVICE_PARCELS];
$scope.parkId = 0;
$scope.parkName = "";
$scope.changePark = function (){
require(["esri/SpatialReference","esri/geometry/Polygon"],
function(SpatialReference,Polygon){
console.log('change park');
$scope.parkId = $scope.listParks.attributes.PARK_HUB_ID;
$scope.parkName = $scope.listParks.attributes.NAME;
parkList.getParkFromGIS($scope.parkId).then(function(data){
var x = data.data;
var y = x.features[0];
var rings = y['geometry'];
var poly = new Polygon(rings);
var xtnt = poly.getExtent();
var sr = new SpatialReference({wkid:2992});
xtnt.setSpatialReference (sr);
map.setExtent(xtnt,true);
});
});
};
function addService(srvc, srvcType, lyrId){require([
"esri/layers/ArcGISTiledMapServiceLayer",
"esri/layers/ArcGISDynamicMapServiceLayer",
"esri/layers/ImageParameters"], function(Tiled,Dynamic,Parameters){
var mapService = {};
if(srvcType == 'Tiled'){
mapService = new Tiled(srvc);
}else{
var imageParameters = new Parameters();
imageParameters.layerIds = lyrId;
imageParameters.transparent = true;
mapService = new Dynamic(srvc,{"imageParameters":imageParameters});
}
map.addLayer(mapService);
});
}
function createMap(){
require(["esri/map"],function(Map){
console.log('create map');
map = new Map("mapCanvas");
addService(STREETMAPSERVICE,'Tiled');
addService(FOTOSSERVICE,'Tiled');
addService(UTILSSERVICE,'Dynamic',ALL_LAYERS);
});
}
createMap();
}]);
App.factory('parkList',['$http', function($http) {
return {
getListFromGIS: function() {
var myUrl = 'http://maps.prd.state.or.us/arcgis/rest/services/ESRI_TEST/MapServer/0/query?where=OBJECTID+%3E+0&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&outFields=PARK_HUB_ID%2CNAME&returnGeometry=false&&returnIdsOnly=false&returnCountOnly=false&orderByFields=NAME&returnZ=false&returnM=false&returnDistinctValues=true&f=pjson&callback=JSON_CALLBACK';
return $http ({ url: myUrl, method: 'JSONP'});
},
getParkFromGIS: function (id){
var myUrl = "http://maps.prd.state.or.us/arcgis/rest/services/ESRI_TEST/MapServer/0/query?where=PARK_HUB_ID%3d"+id+"&f=pjson&callback=JSON_CALLBACK";
return $http ({ url: myUrl, method: 'JSONP'});
},
JSON_CALLBACK: function(data) {}
};
}]);
(this is the code working with the ng-controller listed in the template of each directive).
any other comments or suggestions you would like to offer about my code structure or code choices will be appreciated also, as I mentioned, I am learning, and the more I can learn the more fun I will have coding.
I believe you're having problems due to your isolate scopes on your directives (the scope: { } in your link function).
I would suggest inlining the templates into your application, instead of trying to make them directives.
In particular, the locationSelector is going to be hard to make a directive - it's usually easier to have your input elements be a part of the element their controller is on.
If you did want to have them be a directive, I'd suggest passing the listParts value into the changePark function:
<select ... ng-change="changePark(listParks)" ...>

Twitter typeahead.js: Possible to use Angular JS as template engine? If not how do I replace "{{}}" for Hogan/Mustache js?

I am working with twitter's typeahead.js and I was wondering if it was possible to modify hogan.js to use something other than {{}}?
I am looking at the minified code now and I have no idea what to change for something so simple. Doing a find and replace breaks it.
I am asking this mainly because I'm using Angular JS but twitter's typeahead requires a templating engine, causing hogan and angular's {{}} to clash. An even better solution would be simply modifying Angular JS (I know it's not a templating engine) and ditching Hogan to fit the following criteria:
Any template engine will work with typeahead.js as long as it adheres to the following API:
// engine has a compile function that returns a compiled template
var compiledTemplate = ENGINE.compile(template);
// compiled template has a render function that returns the rendered template
// render function expects the context to be first argument passed to it
var html = compiledTemplate.render(context);
Ignore the documentation on this, just look at the source code:
function compileTemplate(template, engine, valueKey) {
var renderFn, compiledTemplate;
if (utils.isFunction(template)) {
renderFn = template;
} else if (utils.isString(template)) {
compiledTemplate = engine.compile(template);
renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
} else {
renderFn = function(context) {
return "<p>" + context[valueKey] + "</p>";
};
}
return renderFn;
}
It happens you can just pass a function to template, callable with a context object which contains the data you passed in the datum objects at the time of instantiation, so:
$('#economists').typeahead({
name: 'economists',
local: [{
value: 'mises',
type: 'austrian economist',
name: 'Ludwig von Mises'
}, {
value: 'keynes',
type: 'keynesian economist',
name: 'John Maynard Keynes'
}],
template: function (context) {
return '<div>'+context.name+'<span>'+context.type.toUpperCase()+'</span></div>'
}
})
If you want to use Hogan.js with Angular, you can change the delimiters by doing something like:
var text = "my <%example%> template."
Hogan.compile(text, {delimiters: '<% %>'});
It appears that the template engine result that typeahead.js expects is an html string and not the dom element (in dropdown_view.js). So I am not sure there is a good solution for using an angular template. As a test I was able to get it binding the result to an angular template but it has to render to an element and then get the html value from the element after binding with the data. I don't like this approach but I figured someone might find it useful. I think I will go with a template function like in the previous post.
Jade template looks like
.result
p {{datum.tokens}}
p {{datum.value}}
Directive
angular.module('app').directive('typeahead', [
'$rootScope', '$compile', '$templateCache',
function ($rootScope, $compile, $templateCache) {
// get template from cache or you can load it from the server
var template = $templateCache.get('templates/app/typeahead.html');
var compileFn = $compile(template);
var templateFn = function (datum) {
var newScope = $rootScope.$new();
newScope.datum = datum;
var element = compileFn(newScope);
newScope.$apply();
var html = element.html();
newScope.$destroy();
return html;
};
return {
restrict: 'A',
link: function (scope, element, attrs, ctrl) {
element.typeahead({
name: 'server',
remote: '/api/search?q=%QUERY',
template: templateFn
});
element.on('$destroy', function () {
element.typeahead('destroy');
});
element.on('typeahead:selected', function () {
element.typeahead('setQuery', '');
});
}
};
}
]);

Access Element Style from Angular directive

I'm sure this is going to be a "dont do that!" but I am trying to display the style on an angular element.
<div ng-repeat="x in ['blue', 'green']" class="{{x}}">
<h3 insert-style>{{theStyle['background-color']}}</h3>
</div>
Result would be
<div class='blue'><h3>blue(psudeo code hex code)</h3></div>
<div class='green'><h3>green(psudeo code hex code)</h3></div>
I basically need to get the style attributes and display them.
Directive Code...
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0], null);
}
}
}];
Fiddle example: http://jsfiddle.net/ncapito/G33PE/
My final solution (using a single prop didn't work, but when I use the whole obj it works fine)...
Markup
<div insert-style class="box blue">
<h4 > {{ theStyle['color'] | toHex}} </h4>
</div>
Directive
directives.insertStyle = [ "$window", function($window){
return {
link: function(scope, element, attrs) {
var elementStyleMap = $window.getComputedStyle(element[0], null);
scope.theStyle = elementStyleMap
}
}
}];
Eureka!
http://jsfiddle.net/G33PE/5/
var leanwxApp = angular.module('LeanwxApp', [], function () {});
var controllers = {};
var directives = {};
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0].parentElement, null)
}
}
}];
leanwxApp.controller(controllers);
leanwxApp.directive(directives);
So that just took lots of persistence and guessing. Perhaps the timeout is unnecessary but while debugging it seemed I only got the style value from the parent after the timeout occurred.
Also I'm not sure why but I had to go up to the parentElement to get the style (even though it would realistically be inherited (shrug)?)
Updated fiddle again
Did one without the timeout but just looking at the parentElement for the style and it seems to still work, so scratch the suspicions about the style not being available at all, it's just not available where I would expect it.
Also holy cow there are a lot of ways to debug in Chrome:
https://developers.google.com/chrome-developer-tools/docs/javascript-debugging
I used
debugger;
statements in the code to drop in breakpoints without having to search all the fiddle files.
One more quick update
The code below comes out of Boostrap-UI from the AngularUI team and claims to provide a means to watch the appropriate events (haven't tried this but it looks like it should help).
http://angular-ui.github.io/bootstrap/
/**
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
* #param {DOMElement} element The DOMElement that will be animated.
* #param {string|object|function} trigger The thing that will cause the transition to start:
* - As a string, it represents the css class to be added to the element.
* - As an object, it represents a hash of style attributes to be applied to the element.
* - As a function, it represents a function to be called that will cause the transition to occur.
* #return {Promise} A promise that is resolved when the transition finishes.
*/
.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
var $transition = function(element, trigger, options) {
options = options || {};
var deferred = $q.defer();
var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
var transitionEndHandler = function(event) {
$rootScope.$apply(function() {
element.unbind(endEventName, transitionEndHandler);
deferred.resolve(element);
});
};
if (endEventName) {
element.bind(endEventName, transitionEndHandler);
}
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
$timeout(function() {
if ( angular.isString(trigger) ) {
element.addClass(trigger);
} else if ( angular.isFunction(trigger) ) {
trigger(element);
} else if ( angular.isObject(trigger) ) {
element.css(trigger);
}
//If browser does not support transitions, instantly resolve
if ( !endEventName ) {
deferred.resolve(element);
}
});
// Add our custom cancel function to the promise that is returned
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
// i.e. it will therefore never raise a transitionEnd event for that transition
deferred.promise.cancel = function() {
if ( endEventName ) {
element.unbind(endEventName, transitionEndHandler);
}
deferred.reject('Transition cancelled');
};
return deferred.promise;
};
// Work out the name of the transitionEnd event
var transElement = document.createElement('trans');
var transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'transition': 'transitionend'
};
var animationEndEventNames = {
'WebkitTransition': 'webkitAnimationEnd',
'MozTransition': 'animationend',
'OTransition': 'oAnimationEnd',
'transition': 'animationend'
};
function findEndEventName(endEventNames) {
for (var name in endEventNames){
if (transElement.style[name] !== undefined) {
return endEventNames[name];
}
}
}
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
return $transition;
}]);
The issue you'll face is that getComputedStyle is considered a very slow running method, so you will run into performance issues if using that, especially if you want angularjs to update the view whenever getComputedStyle changes.
Also, getComputedStyle will resolve every single style declaration possible, which i think will not be very useful. So i think a method to reduce the number of possible style is needed.
Definitely consider this an anti-pattern, but if you still insist in this foolishness:
module.directive('getStyleProperty', function($window){
return {
//Child scope so properties are not leaked to parent
scope : true,
link : function(scope, element, attr){
//A map of styles you are interested in
var styleProperties = ['text', 'border'];
scope.$watch(function(){
//A watch function to get the styles
//Since this runs every single time there is an angularjs loop, this would not be a very performant way to do this
var obj = {};
var computedStyle = $window.getComputedStyle(element[0]);
angular.forEach(styleProperties, function(value){
obj[value] = computedStyle.getPropertyValue(value);
});
return obj;
}, function(newValue){
scope.theStyle = newValue;
});
}
}
});
This solution works if you don't HAVE to have the directive on the child element. If you just place the declaration on the ng-repeat element itself, your solution works:
<div insert-style ng-repeat="x in ['blue', 'green']" class="{{x}}">
Fiddle

Resources