Directive within another directive - scope var undefined - angularjs

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

Related

Get element scope from attribute directive

I'm trying to extend functionality of any directive by simply attaching an attribute directive, but I'm having trouble getting the scope of the element on which the attribute is defined.
For example, I have this template:
<div class="flex-item-grow flex-item flex-column report-area">
<sv-report sv-reloadable id="reportId"></sv-report>
</div>
Here, sv-reloadable has some implicit understanding of sv-report, but sv-report has no idea about sv-reloadable.
I've defined sv-reloadable as:
angular
.module( 'sv-reloadable', [
'sv.services',
])
.directive('svReloadable', function(reportServices, $timeout) {
return {
restrict: 'A',
controller: function($scope, $timeout) {
$scope.$on('parameter-changed', function(evt, payload) {
evt.stopPropagation();
$scope.viewModel = getNewViewModel(payload);/* hit the server to retrieve new data */
});
}
};
});
Now, $scope in sv-reloadable is the parent scope of sv-report. I'm wanting sv-reloadable to be able to attach a listener to sv-report's scope, and swap out properties of that scope. I understand that it's possible to grab the sibling scopes, but that causes problems when trying to figure out exactly which element it's attached to.
I attempted the following:
link: function(scope, element, attrs) {
ele = element;
var actualScopyThingy = element.scope();
},
Which I had assumed would give me the scope of the element the attribute was defined on, but alas, it still returns the parent scope of the element.
If it's important, sv-report is defined as the following, but I'd like to be able to keep it the same (since sv-reloadable is going to be attached to many different elements, all of which must have viewModel defined on their scope)
return {
restrict: 'E',
replace: true,
templateUrl: 'sv-report/sv-report.tpl.html',
scope: {
id: '=',
reportParameters: '='
},
controller: function ($scope, svAnalytics) {
/* unrelated code here */
},
link: function(scope, element, attrs) {
initialLoadReport(scope);
}
};
After a bit of digging around, isolateScope() is what I was after (rather than scope()). sv-reloadable's directive becomes:
return {
restrict: 'A',
link: function(scope, element, attrs) {
var elementScope = element.isolateScope();
elementScope.$on('parameter-changed', function(evt, payload) {
...
});
}
};

dynamically add ui-sref to an element

I have the following directive
.directive('famAction', function () {
var directive = {
restrict: 'A',
scope: {action: '='},
link: link
};
function link(scope, element) {
if (scope.action.hasOwnProperty('state')) {
element.attr('ui-sref', scope.action.state);
}
if (scope.action.hasOwnProperty('func')) {
element.bind('click', scope.action.func);
}
}
return directive;
})
The problem is that, when adding the ui-sref attribute, the attribute isn't compiled ant therefore I don't have the generater href tag, so the link doesn't work.
How can I do to dynamically add a ui-sref attribute to an element and then compile it ?
I even tried this, without success:
.directive('famAction', function () {
var directive = {
restrict: 'A',
scope: {action: '='},
compile: compile
};
function compile(tElement, tAttrs) {
return {
pre: function preLink(scope, element, attrs) {
if (scope.action.hasOwnProperty('state')) {
element.attr('ui-sref', scope.action.state);
}
},
post: function postLink(scope, element, attrs) {
if (scope.action.hasOwnProperty('func')) {
element.bind('click', scope.action.func);
}
}
};
}
return directive;
})
PS: My action object can be one of the following:
{state: 'app.flaws.relevant', icon: 'chain-broken'}
{func: vm.ignoreFlaw, icon: 'ambulance'}
You cannot add directives to the element currently being compiled! The reason is that Angular has already parsed it and extracted the directives to be compiled. In contrast, if you wanted to add an attribute directive to an internal element, you should do it in the compile function (not in preLink - the template has already been compiled there and the addition of the attribute would have no effect). For cases like this, where you want access to the scope, it will not work: compile cannot access the scope (it has not been created yet).
What can you do in your case?
Manual compilation:
Remove the element that has the ui-sref from the template of your famAction directive.
In pre- or postLink, use $compile to compile the template of the element that contains the ui-sref, i.e. something along the lines of:
var uiSrefElem = $compile(
'<button ui.sref="' + scope.action.state + '">Go!</button>'
);
Add this uiSrefElem to the element of the directive.
Programmatic navigation: Place a function in the scope/controller that uses $state.go(scope.action.state). Call this function when the directive element is clicked.
I would go for (2).

How to bind content of tag into into directive's scope?

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.

How to override image source within a directive

I want to override an image src within a directive. I've tried the following, none of which work.
app.directive('imgTransform', function () {
return {
retrict: 'A',
link: function (scope, elem, attrs) {
elem.attr('ng-src', 'foo');
attrs.ngSrc = 'foo';
elem.attr('src', 'foo');
attrs.src = 'foo';
}
};
});
Does the link function execute before the DOM binding takes place?
I also tried creating an isolate scope and binding to the ngSrc attribute:
scope: {
src: '#ngSrc'
},
Then setting scope.ngSrc in the link function. This did not work either.
Am I missing something?
http://jsfiddle.net/benfosterdev/y7JE9/
You need to modify it before you get to your linking function.
I'd suggest in the controller function, as opposed to digging to deep into the $compiler.
restrict: "A",
controller: function ($scope, $element, $attrs) {
$attrs.$set('ngSrc', 'someOtherValue');
},
link: function (scope, el, attrs) {...}
Here's a fiddle: http://jsfiddle.net/y7JE9/5/
Also, use $observe when looking up interpolated values in your link function. Good luck : )

How to hide element if transcluded contents are empty?

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>

Resources