Make directives without attributes, but embedded HTML in view. But without jQuery. - javascript

I'm reading an article on transclusion and have got to about here here. At this point, the author has said that making a directive in this way
angular.module('app', [])
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
logoSrc: '#',
header: '#',
body: '#'
}
};
});
<my-directive header="header"
body="body"
logo-src="logoSrc.png">
</my-directive>
Is awkward and not desirable. It suggests instead to make the HTML inside the tag
app.directive('myCard', function() {
return {
scope: {
title: '=myTitle',
pic: '=myPic'
},
templateUrl: 'myCard',
transclude: true,
link: function(scope, el, attrs, ctrl, transclude) {
el.find('.content').append(transclude());
}
};
});
Is it necessary to use jQuery and DOM manipulation to use directives this way? Is there really any advantage other than cleaner looking templates which don't need to be appended in this way?

Related

Angularjs directive duplicates the elements inside it

(function () {
'use strict';
angular.module('product')
.directive('sampledirective', ['$document') {
return {
restrict: 'E',
replace: true,
scope: {
data: '=',
btnClick: '&'
},
link: function (scope, element, attr) {
var compiled = $compile(template)(scope);
angular.element(element).replaceWith(compiled);
element = compiled;
};
};
}]);
})();
I have a directive which replaces the elements inside it.
I have a weird issue which replaces the elements mulitple time in the directive .
Duplicates the elements in the below bolded line which should not happen.
angular.element(element).replaceWith(compiled);
Please let me know why the elemenst are duplicated and let me know how to avoid it .
sample
Actual
cool cool
expected
cool
The following directive only replaces the content once in my case. If this dosn't solve your problem maybe you could provide a small working example or so. Also note that if you use an isolated scope for your directive you should provide a template, as stated in this post.
angular.module('product').directive("sampledirective", function ($compile) {
return {
template: '',
restrict: 'E',
scope: {
data: "=data",
btnClick: '&'
},
link: function (scope, element, attrs) {
var template = "<div>foo</div>"
var compiled = $compile(template)(scope);
element.replaceWith(compiled);
}
}
});

How to pass transclusion down through nested directives in Angular?

I am trying to figure out how to pass a transclusion down through nested directives and bind to data in the inner-most directive. Think of it like a list type control where you bind it to a list of data and the transclusion is the template you want to use to display the data. Here's a basic example bound to just a single value (here's a plunk for it).
html
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data"><div>{{ source.name }}</div></outer>
</body>
javascript
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
ctrl.welcomeMessage = 'Welcome to Angular';
}])
.directive('outer', function(){
return {
restrict: 'E',
transclude: true,
scope: {
model: '='
},
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template :'<div class="inner" my-transclude></div>'
};
})
.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: 'element',
link: function(scope, element, attrs, controller, transclude) {
transclude(scope, function(clone) {
element.after(clone);
})
}
}
});
As you can see, the transcluded bit doesn't appear. Any thoughts?
In this case you don't have to use a custom transclude directive or any trick. The problem I found with your code is that transclude is being compiled to the parent scope by default. So, you can fix that by implementing the compile phase of your directive (this happens before the link phase). The implementation would look like the code below:
app.directive('inner', function () {
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template: '<div class="inner" ng-transclude></div>',
compile: function (tElem, tAttrs, transclude) {
return function (scope, elem, attrs) { // link
transclude(scope, function (clone) {
elem.children('.inner').append(clone);
});
};
}
};
});
By doing this, you are forcing your directive to transclude for its isolated scope.
Thanks to Zach's answer, I found a different way to solve my issue. I've now put the template in a separate file and passed it's url down through the scopes and then inserting it with ng-include. Here's a Plunk of the solution.
html:
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data" row-template-url="template.html"></outer>
</body>
template:
<div>{{ source.name }}</div>
javascript:
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
}])
.directive('outer', function(){
return {
restrict: 'E',
scope: {
model: '=',
rowTemplateUrl: '#'
},
template: '<div class="outer"><inner my-data="model" row-template-url="{{ rowTemplateUrl }}"></inner></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
scope: {
source: '=myData',
rowTemplateUrl: '#'
},
template :'<div class="inner" ng-include="rowTemplateUrl"></div>'
};
});
You can pass your transclude all the way down to the third directive, but the problem I see is with the scope override. You want the {{ source.name }} to come from the inner directive, but by the time it compiles this in the first directive:
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
the {{ source.name }} has already been compiled using the outer's scope. The only way I can see this working the way you want is to manually do it with $compile... but maybe someone smarter than me can think of another way.
Demo Plunker

transcluded content requires the controller of the transcluding directive

I have a directive which is transcluding it's content. And in the transcluded content is a directive which requires the controller of the transcluding directive. This throws an error if i'm creating a transclude function in the transcluding directive. I think this is because the transcluded content gets cloned when you provide a transclude function (https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L846).
I also have a plunker describing my problem: http://plnkr.co/edit/rRKWW6zfjZuUiw1BY4zs?p=preview
What i want to do is i want to transclude the content and parse all of the transcluded content and then put it in the right place in the DOM and compile it myself. The transcluded content is actually the configuration for my directive.
I've also tried emptying the cloned array i receive in the transcluding function, because i actually don't need the content to be transcluded automatically. I just need to parse it and transclude it manually on a later point in time. Angular doesn't need to do anything with my transcluded content. But this doesn't work because the directives are already identified when the transcluding function is called. So when i empty the array i receive an error here (https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L961).
kind regards,
Daan
When you use require: "^controller" you are telling Angular that the directive requires controller to be attached to an ancestor DOM element at the time the link function is run.
When you do transclusion without using the ngTransclude directive, your parent directive link function get's passed a transclude method. (You already knew that; this is just for completeness.) This transclude method does the following:
Extracts the content for transclusion
If a cloneAttachFn was passed in, clone the content and call the cloneAttachFn
Calls $compile() to compile and link the content (or cloned content) using the scope supplied by the call to transclude (defaults to a new scope that inherits from the $parent of the directive scope).
If you call transclude, and end up not attaching the content as a descendant of an element with the required controller (or not adding the content to the DOM at all), then the content will have no parent with the required controller. Because it can't find the required controller, you get an error.
In your example, if you use kmBar with require: "^kmFoo", you are restricted to adding the transcluded content to DOM nodes that are descendants of nodes that have kmFoo.
The easiest fix is to go ahead and append it to kmFoo's element for the purposes of $compile() and linking, but immediately detach it.
Detach (as opposed to remove) maintains click handlers, etc, so everything will continue to work if you append the element later. If you are using early versions of AngularJS, you may need to include jQuery to get detach since it wasn't included in early versions of jqLite.
Here's a snippet of a Plunk I put together
app.directive('kmFoo', function() {
return {
restrict: 'A',
scope: true,
template: '<div></div>',
transclude: true,
controller: function() {
// ...
},
link: function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// We are going to temporarily add it to $element so it can be linked,
// but after it's linked, we detach it.
transcludeFn(scope, function(clone) {
console.log('transcluding foo');
$element.append(clone);
c = clone;
}).detach();// <- Immediately detach it
}
};
});
app.directive('kmBar', function() {
return {
restrict: 'A',
scope: true,
require: '^kmFoo',
link: function(scope, $element, attrs, fooCtrl) {
console.log('linking bar');
// Right now it's a child of the element containing kmFoo,
// but it won't be after this method is complete.
// You can defer adding this element to the DOM
// for as long as you want, and you can put it wherever you want.
}
};
});
First of all, why do need the transclude function? Is this not sufficient?
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'controller': function() {
this.tryMe = function() { console.log("Success!") };
},
'link': function(scope, element, attrs, ctrl) {
console.log('linking foo');
var innerHtml = element.html();
// do something with innerHtml
element.html("<div>Empty</div>");
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': true,
'require': '^kmFoo',
'link': function(scope, element, attrs, fooCtrl) {
fooCtrl.tryMe();
}
};
});
But if you really want to get access to the fooController and have a transclude function in kmFoo, you can access the controller via element.controller() after all linking is done and all controllers are initialized.
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'template': '<div ng-transclude></div>',
'transclude': true,
'controller': function() {
this.tryMe = function() { console.log("Success!") };
},
'link': function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// when you put the transclude function in comments it won't throw an error
transcludeFn(scope, function(clone) {
console.log('transcluding foo');
});
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': true,
'template': "<button ng-click='tryMe()'>Feeling lucky?</button>",
'link': function(scope, element, attrs) {
scope.getFooCtrl = function() {
return element.parent().controller('kmFoo');
};
console.log('linking bar');
console.log('parent not yet known: ' + element.parent().toString());
},
'controller': function($scope) {
$scope.tryMe = function() {
$scope.getFooCtrl().tryMe();
};
}
};
});
See it in action with this plnkr.
To perform this without having it displayed in the DOM, you can use a
transclude: 'element'
on the second directive.
This will avoid using some tricks to get the information you need.
app.directive('kmFoo', function() {
return {
'restrict': 'A',
'scope': true,
'template': '<div ng-transclude></div>',
'transclude': true,
'controller': function() {
},
'link': function(scope, $element, attrs, ctrl, transcludeFn) {
console.log('linking foo');
// when you put the transclude function in comments it won't throw an error
//transcludeFn(scope, function(clone) {
// console.log('transcluding foo');
//});
}
};
});
app.directive('kmBar', function() {
return {
'restrict': 'A',
'scope': {},
'require': '^kmFoo',
'link': function(scope, $element, attrs, fooCtrl) {
console.log('linking bar');
}
};
});
app.directive('kmBarWithElement', function() {
return {
'restrict': 'A',
'scope': {},
'transclude': 'element',
'require': '^kmFoo',
'link': function(scope, $element, attrs, fooCtrl, transclude) {
transclude(function(clone) {
console.log('here the element: ', clone);
});
}
};
});
Here is a working example: http://plnkr.co/edit/lbT7oz74Yz7IZvEKp77T?p=preview

Is it possible to 'transclude' while keeping the scope of the directive in Angular?

Is possible to do the following in Angular?
<div ng-controller="MainCtrl" ng-init="name='World'">
<test name="Matei">Hello {{name}}!</test> // I expect "Hello Matei"
<test name="David">Hello {{name}}!</test> // I expect "Hello David"
</div>
My directive is simple but it's not working:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
replace: true,
transclude: true,
template: '<div class="test" ng-transclude></div>'
};
});
I also tried to use the transclude function to change the scope and it works. The only problem is that I loose the template.
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
transclude: true,
template: '<div class="test" ng-transclude></div>',
link: function(scope, element, attrs, ctrl, transclude) {
transclude(scope, function(clone) {
element.replaceWith(clone);
});
}
};
});
Is it possible to do this while keeping the template in place and clone to be appended into the element with ng-transclude attribute?
Here is a Plnkr link you could use to test your solution: http://plnkr.co/edit/IWd7bnhzpLmlBpoaoJct?p=preview
That happened to you because you were too aggressive with the element.
You can do different things instead of replaceWith that will... replace the current element.
You can append it to the end, prepend it... pick one template element and insert it inside... Any DOM manipulation. For example:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
name: '#'
},
transclude: true,
template: '<div class="test">This is test</div>',
link: function(scope, element, attrs, ctrl, transclude) {
transclude(scope, function(clone) {
element.append(clone);
});
}
};
});
Here I decided to put it after the element, so you could expect:
This is test
Hello Matei!
This is test
Hello David!
Notice I did not included ng-transclude on the template. It is not needed because you're doing that by hand.
So TL;DR;: Play with the element, don't just replace it, you can insert the transcluded html where you want it, just pick the right element, and call the right method on it.
For the sake of completeness here is another example: http://plnkr.co/edit/o2gkfxEUbxyD7QAOELT8?p=preview

Create a simplified version of/interface to a more complex directive in Angular?

I have a rather complex modal dialog directive that includes some child directives, transclusion, isolated scope. Here is an example of what it looks like:
<dm-modal reference="previewDialog" additional-class="datasourcePreview">
<dm-modal-header><div class="dialog-title" ng-controller="dsPreviewCtrl">Preview of {{previewData.dataSourceName}}</div> </dm-modal-header>
<dm-modal-body>
<div ng-controller="dsPreviewCtrl" ng-include="dsPreview.html'" ></div>
</dm-modal-body>
<dm-modal-footer>
Choose one
</dm-modal-footer>
<dm-modal-footer-buttons>
<dm-modal-footer-button type="done" ng-click="doSomething()"></dm-modal-footer-button>
<dm-modal-footer-button type="delete"></dm-modal-footer-button>
</dm-modal-footer-buttons>
</dm-modal>
When using this directive, 80%+ of the time I only need to fiddle with a couple things because it's just a confirm dialog, and the rest of the options are standard. Rather than a hairy interface that looks like the above, I'd like to implement a directive that would look like the following, but ultimately just produce the directive above.
<dm-simple-modal reference="confirmDialog" title="Are you sure?" okClick="handleOk()">
<div ng-controller="dsPreviewCtrl" ng-include="'dsPreview.html'"></div>
</dm-simple-modal>
So it seems simplest to me to basically transform the above markup into the full markup for the other "more complete" directive which would look like the following:
<dm-modal reference="confirmDialog">
<dm-modal-header>
Are you sure?
</dm-modal-header>
<dm-modal-body>
<div ng-controller="dsPreviewCtrl" ng-include="'dsPreview.html'"></div>
</dm-modal-body>
<dm-modal-footer-buttons>
<dm-modal-footer-button text="Yes" ng-click="handleOk()"></dm-modal-footer-button>
<dm-modal-footer-button type="No"></dm-modal-footer-button>
</dm-modal-footer-buttons>
</dm-modal>
And then angular would compile this directive into the working html.
Putting this problem into a more contrived, but simpler example, I have created a fiddle http://jsfiddle.net/josepheames/JEYJa/1/
I even included an attempt to make the simpler directive using a template patterned after the more complex directive but I cannot get that to work.
the directives in this fiddle look like this:
<div ng-controller="MyCtrl" ng-click="doClick()">
<complex-directive handle="complexDir" titleColor="blue">
<h1>{{title}}</h1>
</complex-directive>
<simple-directive handle="simpleDir" title="another {{title}}" color="red" />
</div>
And the working complex directive is this:
myApp.directive('complexDirective', function() {
return {
restrict: 'E',
scope: {
handle: '='
},
replace: true,
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope, element, attr) {
scope.handle= {
doSomething: function() {
element.css('background-color', attr.titlecolor);
}
}
}
}
});
And my failing attempt at a simple directive is this:
myApp.directive('simpleDirective', function() {
return {
restrict: 'E',
scope: {
handle: '='
},
replace: true,
transclude: true,
template: '<complex-directive handle="{{thehandle}}" titleColor="{{color}}"><h1>{{title}}</h1></complex-directive>',
link: function(scope, element, attr) {
scope.thehandle = attr.handle,
scope.color = attr.titlecolor,
scope.title = element.html(),
scope.handle= {
doSomething: function() {
element.css('background-color', attr.titlecolor);
}
}
}
}
});

Categories

Resources