171

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, …

Palec
  • 12,743
  • 8
  • 69
  • 138
Alen Giliana
  • 2,144
  • 3
  • 17
  • 30
  • 1
    possible duplicate of [Dynamic templateUrl - AngularJS](http://stackoverflow.com/questions/17074454/dynamic-templateurl-angularjs) – Nick Grealy May 05 '15 at 10:37
  • if you're using AngularJS 1.5+, check this elegant solution: https://stackoverflow.com/a/41743424/1274852 – hkong Jun 14 '17 at 16:44

6 Answers6

315
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.

Andrej Kaurin
  • 11,592
  • 13
  • 46
  • 54
  • 4
    Nice, but... can I access to scope attributes from the templateUrl function? The templateUrl depends on a scope value, but I can't access to it :( – josec89 Jun 10 '14 at 11:56
  • Nope. Maybe you should explain exactly what you need so we can find solution. It is probably that you can send everything you need via attrs. – Andrej Kaurin Jun 10 '14 at 12:22
  • I found the solution! :) I've created a directive called 'entry'. An entry has a 'type' (int, string, date...) and, depending on the type of the entry, you have one template or another (an input type='text', input type='number', some plugin for date....). The thing is that I pass the entry type through the scope and I couldn't use it to decide which template should be loaded. I used the method written in the other answer. I had problems accessing to the scope with ng-include (because it creates a new scope), but I solved it useing $parent. Thanks btw :) – josec89 Jun 10 '14 at 16:06
  • 1
    I am glad you found solution. I would NOT recommend directive having dependency on its parent unless it is controller set in require part of directive. – Andrej Kaurin Jun 11 '14 at 07:43
  • 11
    Finally! Exactly what I was looking for! I didn't realize I had access to elem and attrs from a templateUrl function. THANKS! – coryvb123 Jun 18 '14 at 20:05
  • 8
    templateUrl is called once per directive, it is not being called upon each directive instance initialization, be careful!!! It may be bug in angular though... – Lu4 Jul 10 '15 at 15:47
  • 1
    @Lu4 I have to disagree with that, as I am using the directive twice in a template with a different attribute value passed in, and it renders the different template as per the attribute correctly. – Ian Vaughan Jul 27 '15 at 20:27
  • 3
    I haven't checked it yet but according to my latest findings, It's probably worth mentioning that it's `once per $compile phase`. In other words if you use `ng-repeat` with your directive and want to set individual template based on specific `ng-repeat` item context, it won't work, because `$compile` phase walks through your directive once before actual `ng-repeat` happens. So in that meaning it's being called once... – Lu4 Jul 28 '15 at 09:59
  • 1
    I wonder how this answer got all these upvotes? This won't work because the `attrs` object contains the attribute values as read from the DOM objects i.e., string values. The OP wanted to evaluate expressions against the scope. In your example, `attrs.templateUrl` value will be just the constant string `'contentUrl'`. See this plunk https://next.plnkr.co/edit/N1FHOvaH7I5DVnmO – Ashraf Sabry Feb 08 '19 at 06:50
185

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>'
   }
});
pgregory
  • 2,093
  • 1
  • 12
  • 8
  • 1
    Its great solution. Is there a way to write it that it can handle multiple instances? Currently, once the scope is set it does not recognize new attrs.ver. – Alen Giliana Feb 18 '14 at 02:03
  • 1
    You mean, you want to watch `ver` attribute changes and rerender directive? – pgregory Feb 18 '14 at 09:57
  • Hey @pgregory, I will have more than one tag in a template and I was planning to have the directive render them based on the ver tag. ` ... ... ` – Alen Giliana Feb 18 '14 at 15:16
  • 1
    Thank you for clarifying. If you declare directive the way posted in upd., your use case when you using multiple `` should work well. Or maybe its time to construct a prototype at [jsfilddle](http://jsfiddle.net/)? – pgregory Feb 18 '14 at 16:23
  • I tried to [prototype into JSFiddle](http://jsfiddle.net/AJGiliana/JQgG5/5/) but I couldn't get it to link directives to external pages URL. At least you will see how it is formatted. [sample page on my site](http://emanuel.alenjohnglina.com/#/1-1_ramsha) – Alen Giliana Feb 19 '14 at 07:06
  • 1
    Hello @AlenGiliana, I`ve take a look at your site, and changed [JSFiddle](http://jsfiddle.net/JQgG5/6/). All you need is `scope:{}` in directive declaration - [scope isolation](http://docs.angularjs.org/guide/directive). Also I strongly recommend you to use last version of angular. ` – pgregory Feb 19 '14 at 10:25
  • 1
    Do you mean to use Angular 1.2.1? Thank you for the help by the way, this learning curve is insane :) – Alen Giliana Feb 19 '14 at 15:24
  • Yes, using last stable versions of 3rd party libraries is good practice) At the moment angularjs 1.2.13 is last – pgregory Feb 19 '14 at 19:31
  • Awesome. Would change the ng-include to a scope variable instead of a function. If it's a function it repeats alot of times. That's what happens to me atleast.. – Are Almaas Aug 19 '14 at 14:55
  • With this solution my double bindings don't work anymore. The solution of Andrej Kaurin works for me. – Andi Giga Sep 01 '15 at 12:53
6

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);
        }
    }
})
VishAl
  • 388
  • 3
  • 17
Shilan
  • 813
  • 1
  • 11
  • 17
5

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>
icem
  • 707
  • 7
  • 12
2

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
}
igasparetto
  • 1,086
  • 1
  • 12
  • 28
0

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>'
        }
    })
ddagsan
  • 1,786
  • 18
  • 21