23

I'm struggling to wrap my mind around how to have an ng-include not use an extra DOM element as I'm building an angular app from a plain-HTML demo. I'm working with pretty slim HTML with fully developed, tightly DOM-coupled CSS (built from SASS) and refactoring is something I want to avoid at all costs.

Here's the actual code:

<div id="wrapper">
    <header
        ng-controller="HeaderController"
        data-ng-class="headerType"
        data-ng-include="'/templates/base/header.html'">
    </header>
    <section
        ng-controller="SubheaderController"
        data-ng-class="subheaderClass"
        ng-repeat="subheader in subheaders"
        data-ng-include="'/templates/base/subheader.html'">
    </section>
    <div
        class="main"
        data-ng-class="mainClass"
        data-ng-view>
    </div>
</div>

I need <section> to be a repeating element but have its own logic and different content. Both, content and number of repetitions are dependent on business logic. As you can see, putting the ng-controller and the ng-repeat on the <section> element will not work. What would, however, is to insert a new DOM node, which is what I'm trying to avoid.

What am I missing out? Is this best practice or is there a better way?


EDIT: just to clarify as asked in comments, the final HTML I'm trying to generate would be:

<div id="wrapper">
    <header>...</header>
    <section class="submenuX">
        some content from controller A and template B (e.g. <ul>...</ul>)
    </section>
    <section class="submenuY">
        different content from same controller A and template B (e.g. <div>...</div>)
    </section>
    <section class="submenuZ">
        ... (number of repetitions is defined in controller A e.g. through some service)
    </section>

    <div>...</div>
</div>

The reason I want to use the same template B (subheader.html), is for code cleanliness. I conceive subheader.html to have some kind of ng-switch in order to return dynamic content.

But basically, the underlaying quiestion is: is there a way to include the contents of a template transparently, without using a DOM node?


EDIT2: The solution needs to be reusable. =)

Phil Thomas
  • 1,207
  • 1
  • 12
  • 32

5 Answers5

27

Some of the other answers suggest replace:true, but keep in mind that replace:true in templates is marked for deprecation.

Instead, in an answer to a similar question, we find an alternative: It allows you to write:

<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());
        }
    };
});

(cut'n'paste from the other answer)

Community
  • 1
  • 1
Peter V. Mørch
  • 9,433
  • 5
  • 46
  • 65
  • 1
    Won't work with dynamic content (ng-repeat, ...). Please update and use this preferred answer, which worked for me : http://stackoverflow.com/a/33508032/704246 – Saad Benbouzid Jun 21 '16 at 14:06
20

Edit: After some research and for the sake of completeness, I've added some info. Since 1.1.4, the following works:

app.directive('include',
    function () {
        return {
            replace: true,
            restrict: 'A',
            templateUrl: function (element, attr) {
                return attr.pfInclude;
            }
        };
    }
);

Usage:

<div include="'path/to/my/template.html'"></div>

There is, however, one gotcha: the template cannot be dynamic (as in, passing a variable through scope because $scope, or any DI for that matter, is not accessible in templateUrl - see this issue), only a string can be passed (just like the html snippet above). To bypass that particular issue, this piece of code should do the trick (kudos to this plunker):

app.directive("include", function ($http, $templateCache, $compile) {
    return {
        restrict: 'A',
        link: function (scope, element, attributes) {
            var templateUrl = scope.$eval(attributes.include);
            $http.get(templateUrl, {cache: $templateCache}).success(
                function (tplContent) {
                    element.replaceWith($compile(tplContent.data)(scope));
                }
            );
        }
    };
});

Usage:

<div include="myTplVariable"></div>
Hugh Guiney
  • 1,162
  • 2
  • 17
  • 31
Phil Thomas
  • 1,207
  • 1
  • 12
  • 32
  • I'd have to argue that my solution is still better for what was the actual question. If this works for you, then fine. – holographic-principle Jul 11 '13 at 16:53
  • Your answer is absolutely fine and put me in the right direction (kudos!). I just definitely need this feature to be reusable along my entire template structure. I can't afford to declare one new directive for each include. – Phil Thomas Jul 11 '13 at 17:04
  • ... which is another matter entirely (and wasn't mentioned -- or I didn't read well?) :) Since you basically re-invented `ng-include` with this, check its source if you run into problems along the way. Cheers :) – holographic-principle Jul 11 '13 at 17:14
  • Very nice (the 2nd example) solution! It even works on elements and they don't get thrown out of the table :)! – ioleo Jan 17 '14 at 11:58
  • Edited: Added `.data` to `tplContent` in the second example. `tplContent` is an HTTP response object, not a string, so $compile chokes on it. Please test code before posting! – Hugh Guiney Aug 23 '15 at 23:14
6

You can create a custom directive, linking to the template with the templateUrl property, and setting replace to true:

app.directive('myDirective', function() {
  return {
    templateUrl: 'url/to/template',
    replace: true,
    link: function(scope, elem, attrs) {

    }
  }
});

That would include the template as-is, without any wrapper element, without any wrapper scope.

holographic-principle
  • 19,590
  • 10
  • 44
  • 62
1

For anyone who happens to visit this question:

As of angular 1.1.4+ you can use a function in the templateURL to make it dynamic.

Check out this other answer here

Community
  • 1
  • 1
ssobus
  • 61
  • 6
0

With the right setup, you can define your own ngInclude directive that can run instead of the one provided by Angular.js and prevent the built-in directive to execute ever.

To prevent the Angular-built-in directive from executing is crucial to set the priority of your directive higher than that of the built-in directive (400 for ngInclude and set the terminal property to true.

After that, you need to provide a post-link function that fetches the template and replaces the element's DOM node with the compiled template HTML.

A word of warning: this is rather draconian, you redefine the behavior of ngInclude for your whole application. I therefore set the directive below not on myApp but inside one of my own directives to limit its scope. If you want to use it application-wide, you might want to make its behavior configurable, e.g. only replace the element if a replace attribute is set in the HTML and per default fall back to setting innerHtml.

Also: this might not play well with animations. The code for the original ngInclude-directive is way longer, so if you use animations in your application, c&p the original code and shoehorn the `$element.replaceWith() into that.

var includeDirective = ['$http', '$templateCache', '$sce', '$compile',
                        function($http, $templateCache, $sce, $compile) {
    return {
        restrict: 'ECA',
        priority: 600,
        terminal: true,
        link: function(scope, $element, $attr) {
            scope.$watch($sce.parseAsResourceUrl($attr.src), function ngIncludeWatchAction(src) {    
                if (src) {
                    $http.get(src, {cache: $templateCache}).success(function(response) {
                        var e =$compile(response)(scope);
                        $element.replaceWith(e);
                    });       
                }
            }); 
        }
    };
}];

myApp.directive('ngInclude', includeDirective);
Johannes Jander
  • 4,735
  • 2
  • 28
  • 46