Angular - condition, transclude - angularjs

I've written a sample directive with a conditional content (component.html):
<div class="panel panel-default">
<div class="panel-heading">{{title}}</div>
<div class="panel-body">
<p ng-show="loadingVisible()" class="text-center">Loading...</p>
<div ng-show="!loadingVisible()" ng-transclude></div>
</div>
Directive code (component.js):
app.directive('component', function () {
return {
restrict : 'A',
transclude : true,
replace : true,
templateUrl : 'component.html',
require : '^title',
scope : {
title : '#',
loadingVisible : '&'
},
controller : [ '$scope', function ($scope) {
if (!$scope.loadingVisible) {
$scope.loadingVisible = function () {
return false;
};
}
} ]
};
});
The main use of this directive is something like this (sample.html):
<div component title="Title">
<div id="t"></div>
</div>
And the controller code (sample.js):
app.directive('sample', function () {
return {
restrict: 'A',
templateUrl: 'sample.html',
controller: [ '$scope', function ($scope) {
$('#id');
} ]
};
});
0
The problem is that the div aquired by using jQuery selector isn't visible. I guess it's because the loadingVisible method (conditional content) hides that div in the construction phase. So when the sample directive tries to get it it fails. Am I doing something wrong? What's the coorect resolution of this problem? Or maybe my knowledge of directives is wrong?
I'll appreciate any help :)

if you're trying to interact with the DOM (or the directive element itself), you'll want to define a link function. The link function gets fired after angular compiles your directive and gives you access to the directives scope, the element itself, and any attributes on the directive. so, something like:
link: function (scope, elem, attrs) {
/* interact with elem and/or scope here */
}
I'm still a little unclear about what your directive is trying to accomplish though, so it's tough to provide much more help. Any additional details?
so if you want to ensure that a title is specified, you can just check for the presence of the title scope var when the directive gets linked, then throw an error if it's not there.
also, is there any reason loadingVisible needs to be an expression? (using '&' syntax). If you just need to show/hide content based on this value, you could just do a normal one-way '#' binding. so overall, something like:
app.directive('component', function () {
return {
restrict : 'A',
transclude : true,
replace : true,
templateUrl : 'component.html',
scope : {
title : '#',
loadingVisible : '#'
},
link : function (scope, elem, attrs) {
if (!scope.title) {
throw 'must specify a title!';
}
if (!attrs.loadingVisible) {
scope.loadingVisible = false;
}
}
};
});
If you need to get access to any of your transcluded content, you can use the elem value that gets injected into your link function, like so:
elem.find('#a');
an (updated) working plnkr: http://embed.plnkr.co/JVZjQAG8gGhcV2tz1ImK/preview

The problem is that directive structure is like this:
<div component>
<div id="a"></div>
</div>
The directive is used somewhere like this:
asd
The test directive uses a "a" element in its controller (or link) function. The problem is that the test controller code is run before the div is transcluded and it cannot see the content :/ A simple workaround is that the component should be before the test directive. Do you have any other solutions to this problem?

Related

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

AngularJS : directives which take a template through a configuration object, and show that template multiple times

I'm looking to create a custom directive that will take a template as a property of a configuration object, and show that template a given number of times surrounded by a header and footer. What's the best approach to create such a directive?
The directive would receive the configuration object as a scope option:
var app = angular.module('app');
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '=?'
}
...
}
}
This object (called config) is passed optionally to the directive using two way binding, as show in the code above. The configuration object can include a template and a number indicating the number of times the directive should show the template. Consider, for example, the following config object:
var config = {
times: 3,
template: '<div>my template</div>'
};
It would, when passed to the directive, cause the directive to show the template five times (using an ng-repeat.) The directive also shows a header and a footer above and below the template(s):
<div>the header</div>
<div>my template</div>
<div>my template</div>
<div>my template</div>
<div>the footer</div>
What's the best way to implement this directive? Note: When you reply, please provide a working example in a code playground such as Plunker, as I've run into problems with each possible implementation I've explored.
Update, the solutions I've explored include:
The use of the directive's link function to append the head, template with ng-repeat, and footer. This suffers from the problem of the template not being repeated, for some unknown reason, and the whole solutions seems like a hack.
The insertion of the template from the configuration object into middle of the template of the directive itself. This proves difficult because jqLite seems to have removed all notion of a CSS selector from its jQuery-based API, leading me to wonder if this solution is "the Angular way."
The use of the compile function to build out the template. This seems right to me, but I don't know if it will work.
You could indeed use ng-repeat but within your directive template rather than manually in the link (as that wouldn't be compiled, hence not repeated).
One question you didn't answer is, should this repeated template be compiled and linked by Angular, or is it going to be static HTML only?
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
config: '=?'
},
templateUrl: 'myTemplate',
link: function(scope) {
scope.array = new Array(config.times);
}
}
}
With myTemplate being:
<header>...</header>
<div ng-repeat="item in array" ng-bind-html="config.template"></div>
<footer>...</footer>
I'd think to use ng-transclude in this case, because the header & footer wrapper will be provided by the directive the inner content should change on basis of condition.
Markup
<my-directive>
<div ng-repeat="item in ['1','2','3']" ng-bind-html="config.template| trustedhtml"><div>
</my-directive>
Directive
var app = angular.module('app');
app.directive('myDirective', function($sce) {
return {
restrict: 'E',
transclude: true,
template: '<div>the header</div>'+
'<ng-transclude></ng-transclude>'+
'<div>the footer</div>',
scope: {
config: '=?'
}
.....
}
}
Filter
app.filter('trustedhtml', function($sce){
return function(val){
return $sce.trustedHtml(val);
}
})

Acessing form from directive's link function

In my custom directive I'm declaring new form <div ng-form="inputForm"></div> with input in it.
How can I access to this form within link function? scope.inputForm is undefined:/
Edit: code sample
.directive('ifInput', ['$system', function ($system) {
return {
restrict: "E",
replace: true,
scope: {},
link: function (scope, element, attrs) {
scope.temp = function () {
console.log(scope.inputForm);
}
},
templateUrl: "template/if-input-pola/index.html"
};
}])
.run(["$templateCache", function ($templateCache) {
$templateCache.put("template/if-input-pola/index.html",
"<div ng-form=\"inputForm\" class=\"form-group ng-fadeInLeft\" ng-class=\"{'has-error': inputForm[kolumna.id].$invalid && inputForm.$dirty}\">" +
"{{inputForm|json}}"+ //here is correct object (with $error and so on)
"<a ng-click=\"temp()\">a</a>" + //here is undefined
"</div>"
);
}])
EDIT 2:
Problem was that I wan's using object like this ng-form="model.inputForm" - now everything is ok
You can just define ng-model e.g.
<input ng-model="myInput"> and
link: function (scope, element) {
console.log(scope.myInput);
}
According to the documentation, using ng-form="inputForm" should post the controller on the local scope. If you cannot see it, you're either looking at a nested scope with that property overwritten, or at an ancestor scope. Keep in mind, that some directives, such as ng-repeat or ng-if, create local scopes, so in the following code:
<div ng-controller="myController">
<div ng-if="someCondition">
<div ng-form="myInput">
...
</div>
</div>
</div>
myInput would NOT be visible on the scope managed by myController.
When accesing the scope property within a link function, it also depends where your linked directive is, as the property may not be assigned to the scope yet. Try $watching the property and you just might find out it's being assigned later.

Is it possible to conditionally apply transclution to directive?

Is it possible to decide whether to apply transclusion to an element based on a scope variable ?
For example ( Stupid simplified reduced example of what i'm trying to achieve )
app.directive('myHighlight', function () {
return {
transclude : true,
template : "<div style='border:1px solid red'><span ng-transclude></span></div>"
}
});
app.directive('myDirective', function () {
return {
template : "<span>some text</span>",
link : function (scope,element,attr) {
if ( 'shouldHighlight' in attr) {
// wrap this directive with my-highlight
}
}
}
});
And then in the html
<span my-directive></span>
<span my-directive should-highlight></span>
Note, please don't tell me to just add the highlight instead of should-highlight, as i said this is a dumb reduced example. Thanks.
Instead of optionally applying the highlight directive, always apply it and do the optional wrapping inside that directive. The optional wrapping is achieved with an ng-if and a boolean passed from myDirective to myHighlight via markup:
<div my-highlight="someBooleanValue">some text</div>
The myHighlight template:
<div ng-if="actuallyTransclude" style="border:1px solid red">
<span ng-transclude></span>
</div>
<div ng-if="!actuallyTransclude" ng-transclude></div>
Working jsfiddle: http://jsfiddle.net/wilsonjonash/X6eB5/
Sure. When you specify the transclude option, you know that you can declaratively indicate where the content should go using ng-transclude.
In the linking function of the directive, you will also get a reference to a transclude function (https://docs.angularjs.org/api/ng/service/$compile, see link section):
function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
The transcludeFn will return the transcluded content, so you can conditionally insert that were and when you want to in the link function of your directive.
Example (http://jsfiddle.net/DKLY9/22/)
HTML
<parentdir flg="1">
Child Content
</parentdir>
JS
app.directive('parentdir', function(){
return {
restrict : 'AE',
scope: {
flg : "="
},
transclude : true,
template : "<div>Parent {{childContent}} Content</div>",
link : function(scope, elem, attr, ctrl, transcludeFn){
if (scope.flg==1){
scope.childContent="Include Me instead";
}
else {
scope.childContent = transcludeFn()[0].textContent;
}
}
}
});
This is a simplified example. To get a better idea of how to use the transclude function, refer to the following : http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
When I approach these kind of problems I just look at what angular did. Usually their source code is very readable and easy to re-use. ngTransclude is no different:
https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
I leave the rest to you. You can either create your own transclusion directive that receives also a condition, or just duplicate the code into your specific directive when the if condition is true.
If you still have trouble, please let me know and we'll set up a plunker.

Triggering a function with ngClick within ngTransclude

I have an unordered list loaded with four items from an array while using ngRepeat. The anchor tag in the list item has a function in the ngClick attribute that fires up a message. The function call works well when used like this:
<ul>
<li ng-repeat="n in supsNames">
<a ng-click="myAlert(n.name)">{{n.name}}</a>
</li>
</ul>
I created a simple directive for inserting unordered lists with list items. The list is loaded just fine but the same functioned I previously mentioned does not fire up. The code is as follows:
<div list items="supsNames">
<a ng-click="myAlert({{item.name}})">{{item.name}}</a>
</div>
Here is my javascript and angularjs code:
var app = angular.module('myapp', []);
app.controller('myCtrl', function($scope) {
$scope.title = 'ngClick within ngTransclude';
$scope.supsNames = [
{"name" : "Superman"},
{"name" : "Batman"},
{"name" : "Aquaman"},
{"name" : "Flash"}
];
$scope.myAlert = function(name) {
alert('Hello ' + name + '!');
};
});
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '='
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
console.log(scope);
}
};
});
I also have a plnkr in case you want to see what I tried to do:
http://plnkr.co/edit/ycaAUMggKZEsWaYjeSO9?p=preview
Thanks for any help.
I got the plunkr working. I'm not sure if its exactly what you're looking for. I copied the main code changes below.
Here's the plunkr:
http://plnkr.co/edit/GEiGBIMywkjWAaDMKFNq?p=preview
The modified directive looks like this now:
app.directive('list', function() {
return {
restrict: 'A',
scope: {
items: '=',
ctrlFn: '&' //this function is defined on controller
},
templateUrl: 'list.html',
transclude: true,
link: function(scope, element, attrs, controller) {
//directive fn that calls controller defined function
scope.dirFn = function(param) {
if(scope.ctrlFn && typeof scope.ctrlFn == 'function') { //make sure its a defined function
scope.ctrlFn( {'name': param} ); //not sure why param has to be passed this way
}
}
}
};
});
And here's how it's called in the html file that's bound to your controller:
<div list items="supsNames" ctrl-fn="myAlert(name)">
<a ng-click="dirFn(item.name)">{{item.name}}</a>
</div>
I think what was happening before is that you were trying to use a function defined in your controller within the isolated scope of the directive, so it wasn't working--that function was undefined in the directive. So what I did was added another parameter to the directive that accepts method binding (I think that's what its called) with the '&'.
So basically you pass your controller method to the directive, and that method gets invoked however you want by the directive defined method I creatively named "dirFn". I don't know if this is the best way per se, but I've used it in an existing project with good results ;)
you need to pass the function to the directive
scope: {
items: '=', 'myAlert': '='
},
The ng-repeat inside the template of the directive insert a new scope and it require to call transclude funcion manually to work. I suggest remove ng-repeat and make the transclusion manually passing a copy of the controller scope and setting the item on each copy:
for(var i=0,len=scope.items.length;i<len;i++){
var item=scope.items[i];
var itemScope=scope.$parent.$new();
$transcludeFn(itemScope, function (clone,scope) {
// be sure elements are inserted
// into html before linking
scope.item=item;
element.after(clone);
});
};
I edit the pluker and I hope that could be helpfull: http://plnkr.co/edit/97ueb8SFj3Ljyvx1a8U1?p=preview
For more info about transclusion see: Transclusion: $transcludeFn

Resources