AngularJS : transcluding multiple sub elements in a single Angular directive - angularjs

I was reading about ng-transclude in the AngularJS docs on Creating a Directive that Wraps Other Elements and I think I understand properly what it does.
If you have a directive that applies to an element that has content inside it such as the following:
<my-directive>directive content</my-directive>
it will allow you to tag an element within the directive's template with ng-transclude and the content included in the element would be rendered inside the tagged element.
So if the template for myDirective is
<div>before</div>
<div ng-transclude></div>
<div>after</div>
it would render as
<div>before</div>
<div ng-transclude>directive content</div>
<div>after</div>
My question is if it is possible to somehow pass more then a single block of html into my directive?
For example, suppose the directive usage would look like this:
<my-multipart-directive>
<part1>content1</part1>
<part2>content2</part2>
</my-multipart-directive>
and have a template like:
<div>
this: <div ng-transclude="part2"></div>
was after that: <div ng-transclude="part1"></div>
but now they are switched
<div>
I want it to render as follows:
<div>
this: <div ng-transclude="part2">content2</div>
was after that: <div ng-transclude="part1">content1</div>
but now they are switched
<div>
Perhaps I could somehow bind the HTML value of a node to the model so that I will be able to use it in such a way without calling it "transclude"?

Starting Angular 1.5, it's now possible to create multiple slots. Instead of transclude:true, you provide an object with the mappings of each slot:
https://docs.angularjs.org/api/ng/directive/ngTransclude
angular.module('multiSlotTranscludeExample', [])
.directive('pane', function(){
return {
restrict: 'E',
transclude: {
'title': '?pane-title',
'body': 'pane-body',
'footer': '?pane-footer'
},
template: '<div style="border: 1px solid black;">' +
'<div class="title" ng-transclude="title">Fallback Title</div>' +
'<div ng-transclude="body"></div>' +
'<div class="footer" ng-transclude="footer">Fallback Footer</div>' +
'</div>'
};
})

Cool question. I'm not sure there is a built in way, but you can do it yourself in a pretty generic way.
You can access the transcluded element by passing in the $transclude service like this:
$transclude(function(clone, $scope) {
Where clone will be a copy of the pre-linked transcluded content. Then if you label the content in the element like this:
<div id="content">
<div id="content0">{{text}}</div>
<div id="content1">{{title}}</div>
</div>
You can loop over the content and compile it like this:
$scope.transcludes.push($compile(clone[1].children[i])($scope));
Great! now you just need to put the content in the correct place in your template
'<div id="transclude0"></div>' +
'<div id="transclude1"></div>' +
Then you can in your link function assign the content correctly
angular.element(document.querySelector('#transclude' + i)).append(scope.transcludes[i]);
I set up a fiddle you can play with that has this set up.
Hope this helped!

In our project we have modeled multi site trasclusion after JSF 2's ui:composition, ui:insert, ui:define (see ui:composition).
Implementation consists of three simple directives: ui-template, ui-insert, ui-define (see angularjs-api/template/ui-lib.js).
To define a template one writes the following markup (see angularjs-api/template/my-page.html):
<table ui-template>
<tr>
<td ui-insert="menu"></td>
</tr>
<tr>
<td ui-insert="content"></td>
</tr>
</table>
and declares a directive (see angularjs-api/template/my-page.js):
var myPage =
{
templateUrl: "my-page.html",
transclude: true
};
angular.module("app").
directive("myPage", function() { return myPage; });
and finally, to instantiate the directive one needs to write (see angularjs-api/template/sample.html):
<my-page>
<div ui-define="content">
My content
</div>
<div ui-define="menu">
File
Edit
View
</div>
</my-page>
The working sample can be seen through rawgit: sample.html
See also: Multisite Transclusion in AngularJS

Related

How to display html after all angularjs processing

I'm working with angularjs and I have a table with rows and columns dynamically created with ng-repeat. Each cell has a directive that has ng-include. While the contents of the cell are processed, ng-repeat is terminated and the table is displayed with empty cells for a moment. I would like to block the page view until all angular processing has been finalized. It is possible?
I load the rows and columns to load a table, ex:
<table>
<tr ng-repeat="row in rows">
<td ng-repeat="col in cols">
<my-directive row="row" col="col" tamplate-cell-url="{{templateCellUrl}}">
</my-directive>
</td>
</tr>
</table>
my directive:
angular.module('app').directive('myDirective', function () {
return {
restrict: 'E',
scope: {
row: '=',
col: '=',
templateCellUrl: '#'
},
replace: true,
link: function (scope, element, attrs, ctrl) {
scope.url = function () {
return scope.templateCellUrl || 'cell-default.html'
}
},
template: '<div ng-include="url()"></div>'
}
})
Yes there is one simple solution for this. Use ng-show or ng-hide for this
<div ng-hide="variable == undefined">
//Your HTML Code
</div>
In this above code example it will hide your html content if the $scope.variable is undefined(means it is not yet initialized). If something gets assigned to it, it will show you the DOM content.
For multiple variables use and condition.
Sounds like a job for ng-cloak directive.
The ngCloak directive is used to prevent the AngularJS html template
from being briefly displayed by the browser in its raw (uncompiled)
form while your application is loading. Use this directive to avoid
the undesirable flicker effect caused by the html template display.
See it here https://docs.angularjs.org/api/ng/directive/ngCloak
So in your cell directive you could do:
<div ng-cloak>
<ng-include src="something.html'"></ng-include>
</div>

Passing data dynamically to directive's transcluded controller

I have a directive for a single popup element. Depending on which of many hundreds of elements that are clicked on, I need to pass different data to the popup when it is displayed. Using a service to store/fetch the data seems like a side-band hack. Storing the data in my top-level controller and allowing the popup controller to inherit seems like poor encapsulation. I'd like to have an isolate scope in the directive and have the popup controller inherit from that. This doesn't seem to work. Any thoughts? Is $broadcast my only option?
I assume that you are working with AngularJS 1.x. I think directive Transclusion is what you are looking for. Transclusion provides a way to include markup that is bound to the parent controller inside the content area of a directive.
Check out this Plunker which is taken from the AngularJS documentation.
.directive('pane', function() {
return {
restrict: 'E',
transclude: {
'title': '?paneTitle',
'body': 'paneBody',
'footer': '?paneFooter'
},
template: '<div style="border: 1px solid black;">' +
'<div class="title" ng-transclude="title">Fallback Title</div>' +
'<div **ng-transclude="body"**></div>' +
'<div class="footer" **ng-transclude="footer"**>Fallback Footer</div>' +
'</div>'
};
<div ng-controller="ExampleController">
<input ng-model="title" aria-label="title"> <br/>
<textarea ng-model="text" aria-label="text"></textarea> <br/>
<pane>
<pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
<pane-body><p>{{text}}</p></pane-body>
</pane>
</div>
Pane is the directive. Note that the content contained in pane-title and pane-body are being compiled against the parent controller, but are displayed in the ng-transclude portions of the directive mark-up.
You should be able to use this to get your custom data into your popup without the need of a service or $broadcast.

AngularJS: Using directives as templating

I'm using a directive to provide a basic template for many of the pages in my Angular app. It looks like this:
angular.module('app.basicLayout', [])
.directive('basicLayout', function () {
return {
restrict: 'E',
transclude: true,
templateUrl: '/assets/views/templates/basicLayout.html'
}
});
And HTML:
<basic-layout>
<h1>My layout goes here</h1>
</basic-layout>
On some of these pages I would like to add a sidebar and still be able to use the layout from my <basic-layout> directive. Is it possible to make something like the following?
<basic-layout>
<h1>My content goes here</h1>
<the-sidebar>
<h2>Sidebar content here</h2>
</the-sidebar>
</basic-layout>
Update
My template file of the directive currently look like this:
<div class="container basic-layout">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div ng-transclude></div>
</div>
</div>
</div>
If <the-sidebar> is set, I would like to change the template file to something like this:
<div class="container basic-layout">
<div class="row">
<div class="col-md-8">
<!-- The content here -->
<div ng-transclude></div>
</div>
<div class="col-md-4">
<!-- The sidebar here -->
<div ng-transclude></div>
</div>
</div>
</div>
That's the exact case for transclusion. You can parametrize your directive layout with some variable layout (sidebar in this case). To do this your have to set the transclude property in the directive config object to true and also specify where in your directive's layout the changing content should be injected by using the ng-transclude directive. Like this:
return {
...
transluce: true,
...
}
and now in the directive template:
//other layout here
<div ng-transclude></div>
This way all the content you put inside the <basic-layout> directive will be transfered into the element on which you use ng-transclude.
For this to work, you need to manually transclude using the transclude function passed as a 5th parameter to the link function. To make it easier, I would change the template to have placeholders:
<div>
<content-placeholder></content-placeholder>
<div>
<sidebar-placeholder></sidebar-placeholder>
</div>
</div>
Then, place each content where it belongs:
transclude: true,
link: function(scope, element, attrs, ctrls, transclude){
transclude(function(clone){
var sidebar = clone.find("the-sidebar").remove();
element.find("content-placeholder").replaceWith(clone);
element.find("sidebar-placeholder").replaceWith(sidebar);
});
}
This should work for you, but it's not clear to me why you want to build a directive for a general layout.
If you have many pages in the Web app (in a classical non-SPA sense), then it's probably better to create the scaffolding in a "master page" on the server.
Otherwise, if you mean that you have many "views" of the same app, the I suggest looking into ui-router - specifically into a section of Nested States and Nested Views.

How can I replace a number of DIVs with a single element in AngularJS?

I have some DIVs that appear on the bottom of each of my pages:
<div id="message" ng-show="test.stateService.displayMessage">
<div>
<div>
<div>
{{ test.stateService.message }}
</div>
</div>
</div>
</div>
Is there a way that I can use Angular to simplify this code so that I do not need to add the same code block to every page?
Yes, you could use a directive like so:
app.directive('codeblock', function() {
return {
scope: true, // use a child scope that inherits from parent
restrict: 'A',
replace: 'false',
template: '<div>\
<div>\
<div>\
{{ test.stateService.message }}\
</div>\
</div>\
</div>'
};
});
You'd call it like this:
<div codeblock id="message" ng-show="test.stateService.displayMessage">
Check https://docs.angularjs.org/api/ng/directive/ngInclude
You can save this div as a file, say footer.htm and add the below code to the bottom of all pages.
<div ng-include="footer.htm"></div>
But this code also needs to be given at the bottom of all pages. So I am not sure how it can save your time.
I don't think you can get rid of adding at least some code on each of the pages. Using an html template with ng-include would be one of the approaches.

Angular Directive not replacing Element

I'm trying to create a directive that will work against xml that i am injecting into the dom via a service.
Here is my a relatively reduced example, having removed the async service call for data and also the template: jsBin - transforming elements using directive
Everything works well with regard getting my post elements' header attribute into an h2 tag but it is leaving a element within my page which will fail for some browsers.
to clarify, this is what i get:
<post class="ng-isolate-scope ng-scope" heading="test heading">
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
</post>
and this is what i would expect:
<div class="ng-scope">
<h2 class="ng-binding">test heading</h2>
<div>test</div>
</div>
I think Adam's answer is the better route, but alternatively and easier if you include jquery you can do this in your link function:
var e =$compile(template)(scope);
element.replaceWith(e);
You aren't using template correctly in your directive. Your link function should not applying your template as you are in the example code.
var linker = function(scope, element, attrs) {
var template = getTemplate();
element.html(template);
$compile(element.contents())(scope);
};
Instead of that, just do this:
return {
restrict: 'E',
replace: true,
scope: {
heading: '#'
},
template: '<div><h2>{{heading}}</h2><div>test</div></div>'
};
In your post directive. 'replace: true' will not impact anything if you are compiling and manipulating the DOM yourself.
At the same time, though, I have no idea what your compile directive is for and why you have a factory that returns an HTML string. All of that code looks like it could be reduced to a single directive. I can't really comment on what you're trying to do, but once you start using $compile all over the place, odds are you aren't doing things the 'Angular way'.

Resources