Sam
Sam

Reputation: 15761

Load Angular Directive Template Async

I want to be able to load the directive's template from a promise. e.g.

template: templateRepo.get('myTemplate')

templateRepo.get returns a promise, that when resolved has the content of the template in a string.

Any ideas?

Upvotes: 4

Views: 5682

Answers (4)

rockywu
rockywu

Reputation: 1

````
/**
 * async load template 
 * eg : 
 * <div class="ui-header">
 *     {{data.name}}
 *     <ng-transclude></ng-transclude>
 * </div>
 */
Spa.Service.factory("RequireTpl", [
    '$q',
    '$templateCache',
    'DataRequest',
    'TplConfig',
 function(
    $q,
    $templateCache,
    DataRequest,
    TplConfig
) {
    function getTemplate(tplName) {
        var name = TplConfig[tplName];
        var tpl = "";
        if(!name) {
            return $q.reject(tpl);
        } else {
            tpl = $templateCache.get(name) || "";
        }
        if(!!tpl) {
            return $q.resolve(tpl);
        }
        //加载还未获得的模板
        return new $q(function(resolve, reject) {
            DataRequest.get({
                url : "/template/",
                action : "components",
                responseType : "text",
                components : name
            }).success(function(tpl) {
                $templateCache.put(name, tpl);
                resolve(tpl);
            }).error(function() {
                reject(null);
            });
        });
    }
    return getTemplate;
}]);
/**
 * usage:
 * <component template="table" data="info">
 *     <span>{{info.name}}{{name}}</span>
 * </component>
 */
Spa.Directive.directive("component", [
    "$compile",
    "RequireTpl",
function(
    $compile,
    RequireTpl
) {
    var directive = {
        restrict : 'E',
        scope : {
            data : '='
        },
        transclude : true,
        link: function ($scope, element, attrs, $controller, $transclude) {
            var linkFn = $compile(element.contents());
            element.empty();
            var tpl = attrs.template || "";
            RequireTpl(tpl)
            .then(function(rs) {
                var tplElem = angular.element(rs);
                element.replaceWith(tplElem);
                $transclude(function(clone, transcludedScope) {
                    if(clone.length) {
                        tplElem.find("ng-transclude").replaceWith(clone);
                        linkFn($scope);
                    } else {
                        transcludedScope.$destroy()
                    }
                    $compile(tplElem.contents())($scope);
                }, null, "");
            })
            .catch(function() {
                element.remove();
                console.log("%c component tpl isn't exist : " + tpl, "color:red")
            });
        }
    };
    return directive;
}]);
````

Upvotes: -1

This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.

Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:

  1. Create generic service to control indicator with.
  2. Manually load template inside link function, show indicator on request send and hide on response.

Here's very simplified example you can start with:

<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
    position: absolute;
    top: calc(50% - 16px);
    left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
    $rootScope.items = ["One", "Two"];
    $rootScope.more = function () {
        $rootScope.items.push(Math.random());
    };
})
.factory("throbber", function () {
    var visible = false;
    var throbber = document.createElement("img");
    throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
    throbber.classList.add("throbber");
    function show () {
        document.body.appendChild(throbber);
    }
    function hide () {
        document.body.removeChild(throbber);
    }
    return {
        show: show,
        hide: hide
    };
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
    var template = "<div>{{text}}</div>";
    var templateUrl = "templateUrl";

    return {
        link: function (scope, el, attr) {
            var tmpl = $templateCache.get(templateUrl);

            if (!tmpl) {
                throbber.show();
                tmpl = $timeout(function () {
                    return template;
                }, 1000);
            }

            $q.when(tmpl).then(function (value) {
                $templateCache.put(templateUrl, value);
                el.html(value);
                $compile(el.contents())(scope);
                throbber.hide();
            });
        },
        scope: {
            text: "=test"
        }
    };
});

JSBin example.

In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.

How template loading works in my example:

  1. Check if there's our template in $templateCache.
  2. If no, fetch it from URL and show indicator.
  3. Manually put template inside element and [$compile][2] it.
  4. Hide indicator.

If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.

Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.

I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.

Upvotes: 2

Rob
Rob

Reputation: 2706

You could load your html inside your directive apply it to your element and compile.

.directive('myDirective', function ($compile) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            //Some arbitrary promise.
            fetchHtml()
             .then(function(result){
                 element.html(result);
                 $compile(element.contents())(scope); 
              }, function(error){

              });
        }
    }
});

Upvotes: 13

Dalorzo
Dalorzo

Reputation: 20014

What I would do is to add an ng-include in my directive to selectively load what I need

Check this demo from angular page. It may help:

http://docs.angularjs.org/api/ng.directive:ngInclude

Upvotes: 0

Related Questions