Render a directive inside another directive (within repeater template) - angularjs

I am trying to render a directive inside another directive (not sure if the repeater inside the template is working this), and it seems to just output as text rather than compiling the directive (plunker code here: http://plnkr.co/edit/IRsNK9)
Any ideas on how I can actually get it to render properly my-dir-one, my-dir-two, my-dir-three directives inside the repeater?
index.html
<!doctype html>
<html ng-app="plunker" >
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js"></script>
<script src="app.js"></script>
<script id="partials/addressform.html" type="text/ng-template">
partial of type {{type}}<br>
</script>
</head>
<body>
<div container></div>
<br /><br /><br />
<b>Below is just to test the directives are actually usable outside the repeater</b>
<div my-dir-one></div>
<div my-dir-two></div>
<div my-dir-three></div>
</body>
</html>
app.js
var app = angular.module('plunker', []);
app.directive('container', function () {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<div class="views">' +
' <div class="view" ng-repeat="view in views">' +
' <div {{view.dir}}>{{view.dir}}</div>' +
' </div>' +
'</div>',
link: function (scope, elm) {
scope.views = [
{ dir: 'my-dir-one' },
{ dir: 'my-dir-two' },
{ dir: 'my-dir-three' }
];
}
}
});
app.directive('myDirOne', function () {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<div>This is directive one.</div>'
}
});
app.directive('myDirTwo', function () {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<div>This is directive two.</div>'
}
});
app.directive('myDirThree', function () {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<div>This is directive three.</div>'
}
});

I managed to work around this issue by re-writing the code:
First I updated the template code as follows:
template: '<div class="views">' +
' <div class="view-wrapper" ng-repeat="view in views">' +
' <div view="{{view.dir}}"></div>' +
' </div>' +
'</div>',
Note that I created a new 'view' directive. Next the view directive definition as follows:
app.directive('view', ['$compile', function (compile) {
return {
restrict: 'A',
scope: {
view: '#'
},
replace: true,
template: '<div class="view"></div>',
controller: ['$scope', function (scope) {
scope.$watch('view', function (value) {
scope.buildView(value);
});
}],
link: function (scope, elm, attrs) {
scope.buildView = function (viewName) {
var view = compile('<div ' + viewName + '></div>')(scope);
elm.append(view);
}
}
}
}]);
So essentially, the view.dir variable is passed as an attribute to the 'view' directive, which then watches it's value and compiles a template with the directive in it.

This is in part a timing problem...I think that by the time it's resolving the {{}} expressions, it's already parsed out and rendered directives. It's not the nesting or the repeater that are the problem, per se.
What you're after here, though, is 'decide which directive to render based on the value of a variable'. There are a couple ways to do that.
Here's one that should work, though it might not scale as nicely as you'd like:
<div class='views' ng-repeat='view in views'>
<div ng-switch='view.dir'>
<div ng-when='my-dir-one' my-dir-one />
<div ng-when='my-dir-two' my-dir-two />
<div ng-when='my-dire-three' my-dir-three />
</div>
</div>
Other options for similar tricks: it looks like you could use ngBindTemplate to take a string from your data and use it as the template for an element. This would probably permit some tricky (and illegible) behavior.
You can specify a directive for an element as a class, but I don't know whether using ngClass to do this would allow you to dynamically select the directive, or whether that would come too late in the pipeline.

Related

How to get ng-repeat item inside transcluded template?

How I can use ngRepeat item inside transcluded template? Is it possible?
Directive template:
<ng-transclude ng-repeat="record in records | filter1 | filter2"></ng-transclude>
Directive:
app.directive('myDirective', function () {
return {
templateUrl: '/views/directives/mydirective.html',
restrict: 'A',
transclude: true,
scope: {
records: '='
}
};
});
Controller view:
<div my-directive records="myRecords">
{{ myDirective.record }}
</div>
Doesn't look like it from the way you're doing it.
But you can $compile the template in the directive to achieve this.
http://jsbin.com/mirisixodo/edit?html,js,console,output
(Realizing this is almost certainly too late for you to use...)
Looks like this was discussed in detail in this AngluarJS GitHub issue, and there is, thanks to moneytree-doug, a way to solve your issue without resorting to compile.
The deal seems to be that, at least starting at AngularJS 1.2.18, transcluding creates a child scope of its own, so your iteration variable is no longer accessible.
$parent is accessible, however, and we can use that to access the iteration variable and accomplish what you're looking to do.
Building off of Micah's jsbin...
HTML:
<!DOCTYPE html>
<html ng-app="app">
<head>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div ng-controller="TestCtrl">
<my-directive records="myRecords">
?: {{$parent.record}}
</my-directive>
</div>
</body>
</html>
JavaScript
var app = angular.module('app', [])
.controller('TestCtrl', function($scope) {
$scope.myRecords = ['foo', 'bar', 'baz'];
});
app.directive('myDirective', function () {
return {
scope: {
records: '='
},
transclude: true,
template: 'HELLO' +
'<div id="inner-transclude"' +
' ng-transclude' +
' ng-repeat="record in records">X</div>',
controller: function ($scope) {
console.log($scope.records);
},
restrict: 'E'
};
});
Result
HELLO
?: foo
?: bar
?: baz
JsBin of reasonable success.
So, again, the crux of this is changing the line if your controller from {{ myDirective.record }} to ?: {{$parent.record}}.

Transclude directive fails to modify content

I have defined a transclude directive as beliw in AngularJs 1.3 - but it does not seem to have any effect on the rendering.
A log statement in the link phase shows that the directive is invoked though.
index.html
<html lang="en" ng-app="directivesApp">
<head>
<script src="js/angular.min.js"></script>
<script src="js/app.js"></script>
</head>
<body ng-controller="directiveCtrl">
<label>Geography</label><input type="text" ng-model="geography"/>
<br>
<div transclude-demo>
<button ng-click='showGeography()'>Show Geography</button>
and a link
</div>
</body>
</html>
app.js
angular.module('directivesApp', [])
.controller('directiveCtrl',function($scope){
$scope.showGeography=function(){alert('I am here');}
})
.directive('transcludeDemo',function(){
return{
transclude: true,
template: '<div ng-transclude> This is my directive content</div>',
link:function ($scope, element, attrs) { console.log('invoked in scope');}
}
});
I would have expected the transclude directive to replace/modify the contents of the div,with the contents of its template.
However,I find that the div is rendered as-is.
Is this how a transclude directive is expected to work?
Transclude is used to preserve the content that's already there, so if you just want to replace the content all you really need is the template. You're not seeing much in your example because your containing divs are essentially the same.
Replace content:
.directive('transcludeDemo',function(){
return{
template: '<div>This is my directive content</div>',
link:function ($scope, element, attrs) { console.log('invoked in scope');}
}
});
If you'd like to combine the new/old content in some way, add something in your template outside of the ng-transclude and it will render in combination.
Combine with transclude:
.directive('transcludeDemo',function(){
return{
transclude: true,
template: '<div>' +
'<p>This text will stay in tact</p>' +
'<div ng-transclude>This text will be replaced</div>' +
'</div>',
link:function ($scope, element, attrs) { console.log('invoked in scope');}
}
});
As mentioned in the comments, this second example should give you a better understanding of what's actually happening.

Angular directive scope - template include vs inline transclude

I have an angular directive for displaying a modal window. It can accept the contents either inline between the HTML tags, or be pointed to a template. When using this directive I seem to have normal access to the $scope when I am using the transcluded inline version of this directive, but when I use a template I do not.
What am I missing here? I've made a smaller sample directive that has the same behavior.
Demo: http://fiddle.jshell.net/ahezfaxj/2
Inline Content Usage
<ang-test show="showBoolean">
<p>Content here!</p>
</ang-test>
Template Usage
<ang-test show="showBoolean" template="'myTemplate.html'"></ang-test>
Directive
app.directive("angTest", function () {
return {
template: function () {
return "<div class='test-container'>" +
" <div ng-if='show && template' ng-include='template'></div>" +
" <div ng-if='show && !template' ng-transclude></div>" +
"</div>";
},
restrict: "E",
replace: true,
transclude: true,
scope: {
template: "#",
show: "="
},
link: function ($scope, $element, attrs) {
if(value){
$element[0].style.display="block";
}else{
$element[0].style.display="none";
}
}
};
});
Please see demo below. You created isolated scope in your directive thus your directive scope is not this same as controller $scope. But you can add as well thing to your directive scope like in example below.
I hope that will help.
var app = angular.module("app", []);
app.controller("BaseCtrl", function ($scope) {
$scope.thing = "Hello!";
$scope.showOne=false;
$scope.showTwo=false;
});
app.directive("angTest", function () {
return {
template: function () {
return "<div class='test-container'>" +
" <div ng-if='show && template' ng-include='template'></div>" +
" <div ng-if='show && !template' ng-transclude></div>" +
"</div>";
},
restrict: "E",
replace: true,
transclude: true,
scope: {
template: "#",
show: "=",
thing:'#'
},
link: function ($scope, $element, attrs) {
//Show/hide when `show` changes
$scope.$watch("show", function (value) {
if(value){
$element[0].style.display="block";
}else{
$element[0].style.display="none";
}
});
}
};
});
.test-container{
padding:5px;
background: #EEE;
}
.transcluded {
color:red
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="BaseCtrl">
Outside Directive: <strong>{{thing}}</strong>
<hr />
<button type="button" ng-click="showOne=!showOne">Toggle One</button>
<ang-test show="showOne">
<p class="transcluded">Inside Included Directive: <strong>--> thing transcluded-->{{thing}}</strong></p>
</ang-test>
<hr />
<script type="text/ng-template" id="myTemplate">
<p>Inside Template Directive: <strong>thing from directive scope -->{{thing}}</strong></p>
</script>
<button type="button" ng-click="showTwo=!showTwo" >Toggle Two</button>
<ang-test show="showTwo" template="myTemplate" thing="{{thing}}"></ang-test>
</div>
</div>

Angular.js directive dynamic templateURL

I have a custom tag in a routeProvider template that that calls for a directive template. The version attribute will be populated by the scope which then calls for the right template.
<hymn ver="before-{{ week }}-{{ day }}"></hymn>
There are multiple versions of the hymn based on what week and day it is. I was anticipating to use the directive to populate the correct .html portion. The variable is not being read by the templateUrl.
emanuel.directive('hymn', function() {
var contentUrl;
return {
restrict: 'E',
link: function(scope, element, attrs) {
// concatenating the directory to the ver attr to select the correct excerpt for the day
contentUrl = 'content/excerpts/hymn-' + attrs.ver + '.html';
},
// passing in contentUrl variable
templateUrl: contentUrl
}
});
There are multiple files in excerpts directory that are labeled before-1-monday.html, before-2-tuesday.html, …
emanuel.directive('hymn', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
// some ode
},
templateUrl: function(elem,attrs) {
return attrs.templateUrl || 'some/path/default.html'
}
}
});
So you can provide templateUrl via markup
<hymn template-url="contentUrl"><hymn>
Now you just take a care that property contentUrl populates with dynamically generated path.
You can use ng-include directive.
Try something like this:
emanuel.directive('hymn', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.getContentUrl = function() {
return 'content/excerpts/hymn-' + attrs.ver + '.html';
}
},
template: '<div ng-include="getContentUrl()"></div>'
}
});
UPD. for watching ver attribute
emanuel.directive('hymn', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.contentUrl = 'content/excerpts/hymn-' + attrs.ver + '.html';
attrs.$observe("ver",function(v){
scope.contentUrl = 'content/excerpts/hymn-' + v + '.html';
});
},
template: '<div ng-include="contentUrl"></div>'
}
});
Thanks to #pgregory, I could resolve my problem using this directive for inline editing
.directive("superEdit", function($compile){
return{
link: function(scope, element, attrs){
var colName = attrs["superEdit"];
alert(colName);
scope.getContentUrl = function() {
if (colName == 'Something') {
return 'app/correction/templates/lov-edit.html';
}else {
return 'app/correction/templates/simple-edit.html';
}
}
var template = '<div ng-include="getContentUrl()"></div>';
var linkFn = $compile(template);
var content = linkFn(scope);
element.append(content);
}
}
})
You don't need custom directive here. Just use ng-include src attribute. It's compiled so you can put code inside. See plunker with solution for your issue.
<div ng-repeat="week in [1,2]">
<div ng-repeat="day in ['monday', 'tuesday']">
<ng-include src="'content/before-'+ week + '-' + day + '.html'"></ng-include>
</div>
</div>
I had the same problem and I solved in a slightly different way from the others.
I am using angular 1.4.4.
In my case, I have a shell template that creates a CSS Bootstrap panel:
<div class="class-container panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">{{title}} </h3>
</div>
<div class="panel-body">
<sp-panel-body panelbodytpl="{{panelbodytpl}}"></sp-panel-body>
</div>
</div>
I want to include panel body templates depending on the route.
angular.module('MyApp')
.directive('spPanelBody', ['$compile', function($compile){
return {
restrict : 'E',
scope : true,
link: function (scope, element, attrs) {
scope.data = angular.fromJson(scope.data);
element.append($compile('<ng-include src="\'' + scope.panelbodytpl + '\'"></ng-include>')(scope));
}
}
}]);
I then have the following template included when the route is #/students:
<div class="students-wrapper">
<div ng-controller="StudentsIndexController as studentCtrl" class="row">
<div ng-repeat="student in studentCtrl.students" class="col-sm-6 col-md-4 col-lg-3">
<sp-panel
title="{{student.firstName}} {{student.middleName}} {{student.lastName}}"
panelbodytpl="{{'/student/panel-body.html'}}"
data="{{student}}"
></sp-panel>
</div>
</div>
</div>
The panel-body.html template as follows:
Date of Birth: {{data.dob * 1000 | date : 'dd MMM yyyy'}}
Sample data in the case someone wants to have a go:
var student = {
'id' : 1,
'firstName' : 'John',
'middleName' : '',
'lastName' : 'Smith',
'dob' : 1130799600,
'current-class' : 5
}
I have an example about this.
<!DOCTYPE html>
<html ng-app="app">
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<div class="container-fluid body-content" ng-controller="formView">
<div class="row">
<div class="col-md-12">
<h4>Register Form</h4>
<form class="form-horizontal" ng-submit="" name="f" novalidate>
<div ng-repeat="item in elements" class="form-group">
<label>{{item.Label}}</label>
<element type="{{item.Type}}" model="item"></element>
</div>
<input ng-show="f.$valid" type="submit" id="submit" value="Submit" class="" />
</form>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
<script src="app.js"></script>
</body>
</html>
angular.module('app', [])
.controller('formView', function ($scope) {
$scope.elements = [{
"Id":1,
"Type":"textbox",
"FormId":24,
"Label":"Name",
"PlaceHolder":"Place Holder Text",
"Max":20,
"Required":false,
"Options":null,
"SelectedOption":null
},
{
"Id":2,
"Type":"textarea",
"FormId":24,
"Label":"AD2",
"PlaceHolder":"Place Holder Text",
"Max":20,
"Required":true,
"Options":null,
"SelectedOption":null
}];
})
.directive('element', function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
scope.contentUrl = attrs.type + '.html';
attrs.$observe("ver", function (v) {
scope.contentUrl = v + '.html';
});
},
template: '<div ng-include="contentUrl"></div>'
}
})

Replace ng-include node with template?

Kinda new to angular. Is it possible to replace the ng-include node with the contents of the included template? For example, with:
<div ng-app>
<script type="text/ng-template" id="test.html">
<p>Test</p>
</script>
<div ng-include src="'test.html'"></div>
</div>
The generated html is:
<div ng-app>
<script type="text/ng-template" id="test.html">
<p>Test</p>
</script>
<div ng-include src="'test.html'">
<span class="ng-scope"> </span>
<p>Test</p>
<span class="ng-scope"> </span>
</div>
</div>
But what I want is:
<div ng-app>
<script type="text/ng-template" id="test.html">
<p>Test</p>
</script>
<p>Test</p>
</div>
I had this same issue and still wanted the features of ng-include to include a dynamic template. I was building a dynamic Bootstrap toolbar and I needed the cleaner markup for the CSS styles to be applied properly.
Here is the solution that I came up with for those who are interested:
HTML:
<div ng-include src="dynamicTemplatePath" include-replace></div>
Custom Directive:
app.directive('includeReplace', function () {
return {
require: 'ngInclude',
restrict: 'A', /* optional */
link: function (scope, el, attrs) {
el.replaceWith(el.children());
}
};
});
If this solution were used in the example above, setting scope.dynamicTemplatePath to 'test.html' would result in the desired markup.
So thanks to #user1737909, I've realized that ng-include is not the way to go. Directives are the better approach and more explicit.
var App = angular.module('app', []);
App.directive('blah', function() {
return {
replace: true,
restrict: 'E',
templateUrl: "test.html"
};
});
In html:
<blah></blah>
I had the same problem, my 3rd party css stylesheet didn't like the extra DOM-element.
My solution was super-simple. Just move the ng-include 1 up. So instead of
<md-sidenav flex class="md-whiteframe-z3" md-component-id="left" md-is-locked-open="$media('gt-md')">
<div ng-include="myService.template"></span>
</md-sidenav>
I simply did:
<md-sidenav flex class="md-whiteframe-z3" md-component-id="left" md-is-locked-open="$media('gt-md')" ng-include="myService.template">
</md-sidenav>
I bet this will work in most situations, even tho it technically isn't what the question is asking.
Another alternative is to write your own simple replace/include directive e.g.
.directive('myReplace', function () {
return {
replace: true,
restrict: 'A',
templateUrl: function (iElement, iAttrs) {
if (!iAttrs.myReplace) throw new Error("my-replace: template url must be provided");
return iAttrs.myReplace;
}
};
});
This would then be used as follows:
<div my-replace="test.html"></div>
This is the correct way of replacing the children
angular.module('common').directive('includeReplace', function () {
return {
require: 'ngInclude',
restrict: 'A',
compile: function (tElement, tAttrs) {
tElement.replaceWith(tElement.children());
return {
post : angular.noop
};
}
};
});
Following directive extends ng-include native directive functionality.
It adds an event listener to replace the original element when content is ready and loaded.
Use it in the original way, just add "replace" attribute:
<ng-include src="'src.html'" replace></ng-include>
or with attribute notation:
<div ng-include="'src.html'" replace></div>
Here is the directive (remember to include 'include-replace' module as dependency):
angular.module('include-replace', []).directive('ngInclude', function () {
return {
priority: 1000,
link: function($scope, $element, $attrs){
if($attrs.replace !== undefined){
var src = $scope.$eval($attrs.ngInclude || $attrs.src);
var unbind = $scope.$on('$includeContentLoaded', function($event, loaded_src){
if(src === loaded_src){
$element.next().replaceWith($element.next().children());
unbind();
};
});
}
}
};
});
I would go with a safer solution than the one provided by #Brady Isom.
I prefer to rely on the onload option given by ng-include to make sure the template is loaded before trying to remove it.
.directive('foo', [function () {
return {
restrict: 'E', //Or whatever you need
scope: true,
template: '<ng-include src="someTemplate.html" onload="replace()"></ng-include>',
link: function (scope, elem) {
scope.replace = function () {
elem.replaceWith(elem.children());
};
}
};
}])
No need for a second directive since everything is handled within the first one.

Resources