Is there an easy way to extend an attribute directive in angularjs? - angularjs

I want to make a custom ng-if but can't find any good examples of how that should be done.
What I'm aiming for is:
<div my-if="someText">....</div>
I want that to expand to
<div ng-if="true|false">....</div>
where true|false depends on someText. Is there an easy way to do this?

To replace a directive with an ng-if a recompile is needed. The easiest way I get it working was this:
(function () {
angular.module('enheter').directive('toggleIf',['$compile', function($compile) {
return {
restrict: 'A',
compile: function(el,attr) {
var toggleName = attr.toggleIf;
var toggleOn = toggleName === "sometext";
el.attr('ng-if', toggleOn);
el.removeAttr('toggle-if');
var fn = $compile(el);
return function(scope) {
fn(scope);
};
}
};
}]);
})();
The directive above first get the value of the toggleIf attribute. The part that define the value of toggleOn will be more complex, but this shows what I was aiming for. Then I just add the ng-if to the element and remove the toggle-if. If the toggle-if was not removed there would be an infinite loop since the call to $compile would execute this function again and again.

<div ng-if="showText ? true : false">....</div>
but then again, if your showText is a boolean, it will do the same thing.

Related

Move function from controller to directive

I have a function in my controller that manipulates the DOM. I understand this to be a bad practice and DOM manipulations should be moved into a directive. I'm having trouble pulling it out of the controller and into its own directive.
I have the following example code in my controller:
$scope.sidebarToggle = function() {
if ($scope.request = null) {
$(#container).switchClass('bottom', 'top', 400, 'linear');
$scope.editing = true;
}
else {
$(#container).switchClass('top', 'bottom', 400 'linear');
$scope.editing = false;
{
};
The above code's if conditions are very simplified, in the live code there are multiple conditions. Otherwise an ng-show/hide directive might have been possible.
The purpose of the code is to recognize the state the user is in, reveal/hide an off-screen sidebar (the class assignments), and set the 'editing' state of the controller.
How can this be refactored into a directive to accomplish the same goal?
Take a look at the documentation for angular directives to get started.
https://docs.angularjs.org/guide/directive
For 'angularising' the class of the container, use ng-class.
example # Adding multiple class using ng-class
You probably don't need to create your own directive for that.
Angular have already created some directives that could help you out.
In your case, you should use angular directive : ng-show and ng-class or ng-style
Exemple :
HTML
<div ng-show="request == null"> Edit </div>
<div ng-class="{'class-top': request == null,'class-bottom' : request != null}"> <div>
CSS :
.class-top{
...
}
.class-bottom{
...
}
Let me know if it works for you,
Nico
Try this:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
},
link: sidebarToggle
};
});
I think it's creating a directive and link to your function sidebarToggle

angularjs directive on bind output

I have something like this:
.controller('contr',['$scope', '$http',function($scope, $http){
$http.post(...).success(function(){
$scope.myTextVar = "some text here";
$scope.completed == true;
});
}]);
an HTML snippet like so:
<div class="myClass" ng-if="completed == true" manipulate-header>
<p>{{myTextVar}}</p>
</div>
and the directive, for simplicity sake let's say it looks like this:
.directive('manipulateHeader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
The manipulate-header directive is supposed to do some manipulation of the text inside the <p></p> tag, however, it runs before {{myTextVar}} gets replaced and hence it outputs {{myTextVar}} instead of some text here.
How may i get around this problem? (i can pass the variable inside the directive scope, but i'm thinking there must be another way).
EDIT: the controller is there and working as intended. Issue is not related to it. I didn't include it to shorten the post.
If it MUST be a directive
If you're trying to do string manipulation in your link function, you're going to have a bad time. The link function is executed before the directive is compiled (that's the idea of the link function), so any bindings (ng-bind or otherwise) will not have been compiled inside of link functions.
To execute code after the compilation stage, you should use a controller. However, you cannot access the DOM in controllers (or rather, you shouldn't). So the logical solution is to instead modify the scope argument instead. I propose something like this:
angular.directive('manipulateHeader', function() {
return {
scope: {
myTextVar: '='
},
controller: function($scope, myFilter) {
// you can't use bindToController here because bindToController executes *after*
// this function
this.modifiedText = myFilter($scope.myTextVar);
},
controllerAs: 'ctrl',
// display the modified text in a template
template: '<span ng-bind="ctrl.modifiedText"></span>'
};
})
.filter('myFilter', function() {
return function(inputText) {
// do some text manipulation here
};
});
Usage:
<manipulate-header myTextVar='myTextVar'></manipulate-header>
Or:
<p>{{ myTextVar | myFilter }}</p>
You could, of course, make this an attribute instead, but best practice indicates that directives that have a template should be an element instead.
The above is only if you need this to be a directive. Otherwise, it should almost definitely be a filter.
If you need to change the $scope variable from your controller you need to isolate scope ,
scope:{
myattr='#', // this will provide one way communication , you can define in your template as <p myattr="hello"><p>
message:'&', //This allows you to invoke or evaluate an expression on the parent scope of whatever the directive is inside
message:'=' // sets up a two-way binding expression between the directive's isolate scope and the parent scope.
}
refer https://docs.angularjs.org/guide/directive
As suggested by #DanPantry - you most likely want a filter not a directive
Read this guide about using filters
https://docs.angularjs.org/guide/filter
Here is an example of such a filter (from documentation)
angular.module('myStatefulFilterApp', [])
.filter('decorate', ['decoration', function(decoration) {
function decorateFilter(input) {
//This is the actual modification of text
//That's what you are looking for
return decoration.symbol + input + decoration.symbol;
}
decorateFilter.$stateful = true;
return decorateFilter;
}])
.controller('MyController', ['$scope', 'decoration', function($scope, decoration) {
$scope.greeting = 'hello';
$scope.decoration = decoration;
}])
.value('decoration', {symbol: '*'});
I am not sure whether you defined $scope.myTextVar
in correct scope. Like, if you defined it in any controller, then directive should be under the controller scope.
Here is the updated HTML
<div ng-controller ="MainController">
<div class="myClass" manipulate-header>
<p>{{myTextVar}}</p>
</div>
</div>
JS :
app.controller('MainController', ['$scope', function($scope) {
$scope.myTextVar = "some text here";
}]);
app.directive('manipulateHerader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
Here is the plunker

How to use a dynamic value with ngClass

I'm trying to apply a class name that's the same as a scope variable.
For example:
<div ng-class="{item.name : item.name}">
So that the value of item.name is added to the class. This doesn't seem to do anything though. Any suggestions on how to do this?
Thanks!
EDIT:
This is actually being done within a select, using ng-options. For example:
<select ng-options="c.code as c.name for c in countries"></select>
Now, I want to apply a class name that has the value of c.code
I found the following directive, which seems to work, but not with interpolation of the value:
angular.module('directives.app').directive('optionsClass', ['$parse', function ($parse) {
'use strict';
return {
require: 'select',
link: function(scope, elem, attrs, ngSelect) {
// get the source for the items array that populates the select.
var optionsSourceStr = attrs.ngOptions.split(' ').pop(),
// use $parse to get a function from the options-class attribute
// that you can use to evaluate later.
getOptionsClass = $parse(attrs.optionsClass);
scope.$watch(optionsSourceStr, function(items) {
// when the options source changes loop through its items.
angular.forEach(items, function(item, index) {
// evaluate against the item to get a mapping object for
// for your classes.
var classes = getOptionsClass(item),
// also get the option you're going to need. This can be found
// by looking for the option with the appropriate index in the
// value attribute.
option = elem.find('option[value=' + index + ']');
// now loop through the key/value pairs in the mapping object
// and apply the classes that evaluated to be truthy.
angular.forEach(classes, function(add, className) {
if(add) {
angular.element(option).addClass(className);
}
});
});
});
}
};
}]);
Better later than never.
<div ng-class="{'{{item.name}}' : item.condition}">
yes. ' and {{ for classname.
I'm on angular 1.5.5 and none of these solutions worked for me.
It is possible to use the array and map syntax at once though it's only shown in the last example here
<div ng-class="[item.name, {'other-name' : item.condition}]">
Simply using the variable should be sufficient:
<div ng-class="item.name" />
This is also documented in the official documentation.
I think you missed the concept.
A conditional css class looks like this:
<div ng-class="{'<css_class_name>': <bool_condition>}">
And I dont think you want:
<div ng-class="{'true': true}">
You probally want to use:
<div ng-class="item.name"></div>
Angularjs Apply class with condition:
<div ng-class="{true:'class1',false:'class2'}[condition]" >
This can be useful in some cases:
HTML:
<div ng-class="getCssClass()"></div>
JS:
$scope.getCssClass = function () {
return { item.name: item.name };
};

Angular.js $compile returns array of html but not actual html

I have the following code:
app.directive('mySample', function($compile) {
return {
//template:"<input type='text' ng=model='sampleData'/> {{sampleData}} <br/>"
link: function(scope, element, atts, controller) {
var markup = "<input type='text' ng=model='sampleData'/> {{sampleData}} <br/>";
angular.element(element).html($compile(markup)(scope));
console.log($compile(markup)(scope));
}
};
});
And I would expect it to generate an input, some span that's coupled via the scope and a break. However I get this output:
[[object HTMLInputElement], [object HTMLSpanElement], [object HTMLBRElement]]
I also tried the template, in comment here, separately and then commenting out the link part. That generates the input and break elements but not the span that shows the coupled model input sampleData.
I have a non-working sample at http://jsfiddle.net/KvdM/nwbsT/ that demonstrates it.
Try this:
element.html(markup);
$compile(element.contents())(scope);
Running the function returned by the $compile service gives you DOM elements rather than html.
So you need to insert them into your page using append (or equivalent):
angular.element(element).append($compile(markup)(scope));
Maybe the easiest way is to use a hard-coded template rather than a dynamic generated one
<div ng-app="myApp">
<my-sample sample-data="'test'"></my-sample>
</div>
var app = angular.module('myApp', []);
app.directive('mySample', function ($compile) {
return {
restrict: 'E',
scope: {
sampleData: '=sampleData'
},
template: "<input type='text'/> {{sampleData}} <br/>",
};
});
FIDDLE
Depends on what kind of data should to be compiled, some times Angular returns a comment node type.
The relevant node that we want to use is the next() (its first sibling).
var tpl = '<div class="myWidget" ng-include="'templates/myWidget.html'"></div>;
var _myWidget = $compile(tpl)(scope);
var myWidget = null;
scope.$on('$includeContentLoaded', function () {
myWidget = _myWidget.next();
});
You just needed to add the jquery to use ".html" and fixed the naming of ng-model

AngularJS ng-keydown directive only working for <input> context?

I am pretty new to AngularJS but found it quite to my liking so far. For my current project I need hotkey functionality and was happy to see that it is supported since the 1.1.2 release.
The ng-keydown directive (http://code.angularjs.org/1.1.3/docs/api/ng.directive:ngKeydown) works as expected for input types but fails me for any other context like div etc. which seems odd given that the documentation says otherwise.
Here is an minimal example (http://jsfiddle.net/TdXWW/12/) of the working respectively the not working:
<input ng-keydown="keypress($event)">
<div ng-keydown="keypress($event)">
NOTE: I know this could be handled with plain jQuery (http://www.mkyong.com/jquery/how-to-check-if-an-enter-key-is-pressed-with-jquery/) but I much prefer to understand how to deal with it in AngularJS.
I was having the same problem and was able to fix it by following this simple tip provided in this comment: https://stackoverflow.com/a/1718035/80264
You need to give the div a tabindex so it can receive focus.
<div id="testdiv" tabindex="0"></div>
Thanks! To wrap this up I got this working by, injecting $document into my directive, then:
MyApp.directive('myDirective', function($document) {
return {
...
$document.keydown(function(e){
console.log(e)
})
}
This was the way I got it working in the end.
Add ng-app to the html element and ng-keyup and ng-keydown to the body element:
<html ng-app="myApp" ng-controller="MainCtrl">
.....
<body ng-keydown="keyPress($event);" ng-keyup="keyRelease($event);">
Then the funcitons in my controller deal with the event calling event.which to get the key code (in my implementation I set a var to the rootScope but you could also broadcast to other controllers)
$scope.keyPress = function(eve) {
if (eve.which === 16) { // shift
// $rootScope.$broadcast('doShift');
$rootScope.shiftOn = true;
};
};
The comment by charlietfl cleared things up and binding the event to $(document) worked as expected! Take away message: The AngularJS documentation is not really exhaustive, i.e. demands background knowledge.
angular.module('app').directive('executeOnEnter', function () {
return {
restrict: 'A',
link: function (scope, el, attrs, $rootScope) {
$('body').on('keypress', function (evt) {
if (evt.keyCode === 13) {
el.trigger('click', function () {
});
}
})
},
controller: function ($rootScope) {
function removeEvent() {
$("body").unbind("keypress");
}
$rootScope.$on('$stateChangeStart', removeEvent);
}
}
})
it worker fine for me, just add tabindex attribute. make sure that ng-keydown contains correct angularjs expression
<div ng-keydown="keypress($event)" tabindex="0">
$scope.keypress = function(ev) {
console.log('keyprez', ev);
}

Resources