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. =)

4
  • Can you add an example for the variation, now sure what you are asking for? Commented Jul 11, 2013 at 11:09
  • Sorry for that. Edited to clarify. Commented Jul 11, 2013 at 11:26
  • You can use ng-include as a tag <ng-include src='url'></ng-include> and not tag other than the content of the url be emitted. Commented Jul 11, 2013 at 12:22
  • 1
    Right, but I'm trying to avoid having to touch my stylesheets that are closely coupled to the DOM tree (with hierarchy selectors) and using nginclude would force me to (as the resulting include template becomes a child of ng-include). Commented Jul 11, 2013 at 13:20

5 Answers 5

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)

Sign up to request clarification or add additional context in comments.

1 Comment

Won't work with dynamic content (ng-repeat, ...). Please update and use this preferred answer, which worked for me : stackoverflow.com/a/33508032/704246
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>

5 Comments

I'd have to argue that my solution is still better for what was the actual question. If this works for you, then fine.
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.
... 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 :)
Very nice (the 2nd example) solution! It even works on <td> elements and they don't get thrown out of the table :)!
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!
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.

4 Comments

Is there a way of making templateUrl dynamic, so that I can re-use the directive?
For the time being, no. At least not out of the box. I've heard of some workarounds which I wouldn't recommend. @filiptc
I guess you're referring to this: github.com/angular/angular.js/issues/1039 Seems to be possible in angularjs >= 1.1.4
To my knowledge, the "possible in 1.1.4+" part refers to templateUrl in the $routeProvider config. But perhaps you'll be able to find something interesting following that thread. @filiptc
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

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.