I created a very simple directive which displays a key/value pair. I would like to be able to automatically hide the element if the transcluded content is empty (either zero length or just whitespace).
I cannot figure out how to access the content that gets transcluded from within a directive.
app.directive('pair', function($compile) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div><span>{{label}}</span><span ng-transclude></span></div>"
}
});
For example, I would like the following element to be displayed.
<pair label="My Label">Hi there</pair>
But the next two elements should be hidden because they don't contain any text content.
<pair label="My Label"></pair>
<pair label="My Label"><i></i></pair>
I am new to Angular so there may be a great way handle this sort of thing out of the box. Any help is appreciated.
Here's an approach using ng-show on the template and within compile transcludeFn checking if transcluded html has text length.
If no text length ng-show is set to hide
app.directive('pair', function($timeout) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div ng-show='1'><span>{{label}} </span><span ng-transclude></span></div>",
compile: function(elem, attrs, transcludeFn) {
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcludded*/
var show=clone.text().length?'1':'0'
attrs.ngShow=show;
});
}
}
});
Plunker demo
Maybe a bit late but you can also consider using the CSS Pseudo class :empty.
So, this will work (IE9+)
.trancluded-item:empty {
display: none;
}
The element will still be registered in the dom but will be empty and invisible.
The previously provided answers were helpful but didn't solve my situation perfectly, so I came up with a different solution by creating a separate directive.
Create an attribute-based directive (i.e. restrict: 'A') that simply checks to see if there is any text on all the element's child nodes.
function hideEmpty() {
return {
restrict: 'A',
link: function (scope, element, attr) {
let hasText = false;
// Only checks 1 level deep; can be optimized
element.children().forEach((child) => {
hasText = hasText || !!child.text().trim().length;
});
if (!hasText) {
element.attr('style', 'display: none;');
}
}
};
}
angular
.module('directives.hideEmpty', [])
.directive('hideEmpty', hideEmpty);
If you only want to check the main element:
link: function (scope, element, attr) {
if (!element.text().trim().length) {
element.attr('style', 'display: none;');
}
}
To solve my problem, all I needed was to check if there were any child nodes:
link: function (scope, element, attr) {
if (!element.children().length) {
element.attr('style', 'display: none;');
}
}
YMMV
If you don't want to use ng-show every time, you can create a directive to do it automatically:
.directive('hideEmpty', ['$timeout', function($timeout) {
return {
restrict: 'A',
link: {
post: function (scope, elem, attrs) {
$timeout(function() {
if (!elem.html().trim().length) {
elem.hide();
}
});
}
}
};
}]);
Then you can apply it on any element. In your case it would be:
<span hide-empty>{{label}}</span>
I am not terribly familiar with transclude so not sure if it helps or not.
but one way to check for empty contents inside the directive code is to use iElement.text() or iElement.context object and then hide it.
I did it like this, using controllerAs.
/* inside directive */
controllerAs: "my",
controller: function ($scope, $element, $attrs, $transclude) {
//whatever controller does
},
compile: function(elem, attrs, transcludeFn) {
var self = this;
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcluded*/
var showTransclude = clone.text().trim().length ? true : false;
/* I set a property on my controller's prototype indicating whether or not to show the div that is ng-transclude in my template */
self.controller.prototype.showTransclude = showTransclude;
});
}
/* inside template */
<div ng-if="my.showTransclude" ng-transclude class="tilegroup-header-trans"></div>
Related
I'm trying to generate a smart-table directive from within a custom directive I've defined:
<div ng-controller="myContrtoller">
<containing-directive></containing-directive>
</div>
The directive definition:
angular.module('app')
.directive('containingDirective', function() {
return {
restrict: 'E',
replace: true,
template: '<table st-table="collection" st-pipe="scopeFun"></table>',
link: function(scope, elem, attrs) {
scope.scopeFun = function () {
// solve the misteries of life
}
}
}
});
As you can see my directive tries to replace the element by the template generated by the st-table directive, using the st-pipe directive depending on the first, briefly:
ng.module('smart-table')
.controller('stTableController' function () {
// body...
})
.directive('stTable', function () {
return {
restrict: 'A',
controller: 'stTableController',
link: function (scope, element, attr, ctrl) {
// body
}
};
})
.directive('stPipe', function (config, $timeout) {
return {
require: 'stTable',
scope: {
stPipe: '='
},
link: {
pre: function (scope, element, attrs, ctrl) {
var pipePromise = null;
if (ng.isFunction(scope.stPipe)) { // THIS IS ALWAYS UNDEFINED
// DO THINGS
}
},
post: function (scope, element, attrs, ctrl) {
ctrl.pipe();
}
}
};
});
Problem:
The st-pipe directive checks the scope var stPipe if it is defined or not by: if (ng.isFunction(scope.stPipe)). This turns out to be ALWAYS undefined. By inspecting I found two things:
From the stPipe directive, the value supposed to be scope.stPipe that is my scopeFun defined within my containingDirective is undefined on the scope object BUT defined within the scope.$parent object.
If I define my $scope.scopeFun within the myContrtoller I don't have any problem, everything works.
Solution:
I did find a solutions but I don't know what really is going on:
Set replace: false in the containingDirective
Define the scope.scopeFun in the pre-link function of containingDirective
Questions:
Why is the scopeFun available in the stPipe directive scope object if defined in the controller and why it is available in the scope.$parent if defined in the containingDirective?
What is really going on with my solution, and is it possible to find a cleaner solution?
From the docs: "The replacement process migrates all of the attributes / classes from the old element to the new one" so what was happening was this:
<containing-directive whatever-attribute=whatever></containing-directive>
was being replaced with
<table st-table="collection" st-pipe="scopeFun" whatever-attribute=whatever></table>
and somehow st-table did not enjoy the extra attributes (even with no attributes at all..).
By wrapping the containingDirective directive template within another div fixed the problem (I can now use replace:true):
<div><table st-table="collection" st-pipe="scopeFun"></table></div>
If someone has a more structured answer would be really appreciated
Say I have a directive like such:
<my-directive>This is my entry!</my-directive>
How can I bind the content of the element into my directive's scope?
myApp.directive('myDirective', function () {
return {
scope : {
entry : "" //what goes here to bind "This is my entry" to scope.entry?
},
restrict: "E",
template: "<textarea>{{entry}}</textarea>"
link: function (scope, elm, attr) {
}
};
});
I think there's much simpler solution to the ones already given. As far as I understand, you want to bind contents of an element to scope during initialization of directive.
Given this html:
<textarea bind-content ng-model="entry">This is my entry!</textarea>
Define bind-content as follows:
directive('bindContent', function() {
return {
require: 'ngModel',
link: function ($scope, $element, $attrs, ngModelCtrl) {
ngModelCtrl.$setViewValue($element.text());
}
}
})
Here's a demo.
I may have found a solution. It relies on the transclude function of directives. It works, but I need to better understand transclusion before being sure this is the right way.
myApp.directive('myDirective', function() {
return {
scope: {
},
restrict: 'E',
replace: false,
template: '<form>' +
'<textarea ng-model="entry"></textarea>' +
'<button ng-click="submit()">Submit</button>' +
'</form>',
transclude : true,
compile : function(element,attr,transclude){
return function (scope, iElement, iAttrs) {
transclude(scope, function(originalElement){
scope.entry = originalElement.text(); // this is where you have reference to the original element.
});
scope.submit = function(){
console.log('update entry');
}
}
}
};
});
You will want to add a template config to your directive.
myApp.directive('myDirective', function () {
return {
scope : {
entry : "=" //what goes here to bind "This is my entry" to scope.entry?
},
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "E",
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
myApp.controller('fooController', function($scope){
$scope.foo = "BLAH BLAH BLAH!";
});
And then use your directive like this:
<div ng-controller="fooController">
<!-- sets directive "entry" to "foo" from parent scope-->
<my-directive entry="foo"></my-directive>
</div>
And angular will turn that into:
<div>THIS IS MY ENTRY</div>
Assuming that you have angular setup correctly and are including this JS file onto your page.
EDIT
It sounds like you want to do something like the following:
<my-directive="foo"></my-directive>
This isn't possible with ELEMENT directives. It is, however, with attribute directives. Check the following.
myApp.directive('myDirective', function () {
return {
template: "<div>{{ entry }}</div>", //**YOU DIDN'T HAVE A TEMPLATE**
restrict: "A",
scope : {
entry : "=myDirective" //what goes here to bind "This is my entry" to scope.entry?
},
link: function (scope, elm, attr) {
//You don't need to do anything here yet
}
};
});
Then use it like this:
<div my-directive="foo"></div>
This will alias the value passed to my-directive onto a scope variable called entry. Unfortunately, there is no way to do this with an element-restricted directive. What is preventing it from happening isn't Angular, it is the html5 guidelines that make what you are wanting to do impossible. You will have to use an attribute directive instead of an element directive.
I'm trying to add dynamically a custom validation directive inside other custom directive. It works fine for system angular directive like "required", but not work for custom validate directive.
I have directive 'controlInput' with input, on which i dynamically add directive 'testValidation' (in real application in dependance from data of control-input).
<control-input control-data='var1'></control-input>
Directives:
app.directive('controlInput', function ($compile) {
return {
restrict: 'E',
replace: true,
template: '<div><input type="text" ng-model="var1"></div>',
link: function (scope, elem, attrs) {
var input = elem.find('input');
input.attr('required', true);
input.attr('test-validation', true);
$compile(elem.contents())(scope);
}
};
});
app.directive('testValidation', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function (value) {
if (value) {
var valid = value.match(/^test$/);
ctrl.$setValidity('invalidTest', valid);
}
return valid ? value : undefined;
});
}
};
});
Full example http://plnkr.co/edit/FylMfTugHrotEMSQyTfT?p=preview
In this example I also add simple input to be sure 'testValidation' directive is working.
Thanks for any answers!
EDIT:
I suggest you fix your original program by changing the template in the controlInput directive to:
template: '<div><input type="text" testdir required ng-model="var1"></div>'
I don't see why not do it as mentioned above, but another way would be to replace the input with a new compiled one:
input.replaceWith($compile(elem.html())(scope));
NOTE:
Change
var valid = value.match(/^test$/);
To
var valid = /^test$/.test(value);
From MDN:
String.prototype.match()
Return value
array An Array containing the matched results or null if there were no
matches.
RegExp.prototype.test() returns what you need, a boolean value.
How do I get the input element from within the directive before the template overwrites the contents?
html
<div xxx>
<input a="1" />
</div>
js
app.directive('xxx', function(){
return {
restrict: 'A',
template: '<p></p>',
replace: true, //if false, just leaves the parent div, still no input
compile: function(element, attrs) {
console.log(element);
return function (scope, iElement, iAttrs) {
}
}
};
});
i am on angular 1.0.x, I cannot pass in optional scope parameters with the '=?' syntax and i want to be able to override a portion of the default template of the directive in a very flexible way. instead of adding a scope variable or attribute everytime that I just plan on passing through the directive, I want to be able to supply the whole element to be used.
edit
the input must retain the scope of the directive, and not the parent.
edit
I am trying to include a partial template inside a directive that will overwrite a piece of the actual template. The piece I am including therefore needs to have access to the directive's scope and not the parent's.
Update
It seems if I do not provide a template or a template URL and instead replace the contents manually using the $templateCache I can have access to the inner elements. I want to let angular handle the template and the replacement though and just want to be able to access the contents in the directive naturally before they get replaced.
Solution
Plunkr
html
<body ng-controller="MainCtrl">
<div editable="obj.email">
<input validate-email="error message" ng-model="obj.email" name="contactEmail" type="text" />
</div>
</body>
js
app.controller('MainCtrl', function($scope) {
$scope.obj = {
email: 'xxx'
};
});
app.directive('editable', function($log){
return {
restrict: 'A',
transclude: true,
template: '<div ng-show="localScopeVar">{{value}}<div ng-transclude></div></div>',
scope: {
value: '=editable'
},
link: function(scope) {
scope.localScopeVar = true;
}
};
});
app.directive('validateEmail', function($log){
return {
restrict: 'A',
require: 'ngModel',
scope: true,
link: function(scope, el, attrs, ctrl) {
console.log(attrs['validateEmail']);
}
};
});
I believe you're looking for the transclude function (link is to 1.0.8 docs). You can see what's going on with:
app.directive('xxx', function($log){
return {
restrict: 'A',
transclude: true,
compile: function(element, attrs, transclude) {
$log.info("every instance element:", element);
return function (scope, iElement, iAttrs) {
$log.info("this instance element:", element);
transclude(scope, function(clone){
$log.info("clone:", clone);
});
}
}
};
});
I have a simple directive
angular.module('myApp')
.directive('myDirective', function () {
return {
template: '<p ng-transclude></p>',
restrict: 'A',
transclude: true,
link: function postLink(scope, element, attrs) {
}
}
}
);
I am trying to run code each time the transclusion content changes and the directive is rendered - I need the transcluded content.
Example algorithm I would like to run in this case is:
count words of transcluded content.
I have tried scope.$watch in multiple forms but to no avail.
We can use the jqlite included within Angular inside a watch expression function to accomplish this. Below is code that watches the length of the transcluded element using jqLite (element.text().length). The watch fires whenever the length of the element that this directive is attached to changes.
And the new length is passed in as newValue to the second function within the watch (since we return it from the first watch function).
myApp.directive('myDirective', function () {
return {
template: '<p ng-transclude></p>',
restrict: 'A',
transclude: true,
replace: true,
link: function (scope, element, attrs) {
scope.$watch(function () {
return element.text().length;
},
function (newValue, oldValue) {
console.log('New Length ', newValue);
});
}
}
});
I've got a working jsfiddle here:
http://jsfiddle.net/2erbF/6/
This addresses the word/letter count scenario. But you could write a test on the element.text() itself if you needed it to fire on any changes- not just a length change.