58

I'm just getting started with angularJS and struggling to figure out proper architecture for what I'm trying to do. I have a single page app but the URL always should stay the same; I don't want the user to be able to navigate to any routes beyond the root. In my app, there is one main div that will need to host different views. When a new view is accessed, I want it to take over the display in the main div. Views loaded in this way can be throw-away or stick around as hidden in the DOM - I'm interested in seeing how each might work.

I've come up with a rough working example of what I'm trying to do. See working example here in this Plunk. Basically I want to dynamically load HTML into the DOM and have standard angularJS controllers be able to hook into that new HTML. Is there a better/simpler way to do this than by using the custom directive I have here and using $compile() to hook up to angular? Perhaps there's something sort of like the router, but doesn't require URL has changes to operate?

Here's the special directive I'm using so far (taken from another SO post):

// Stolen from: http://stackoverflow.com/questions/18157305/angularjs-compiling-dynamic-html-strings-from-database
myApp.directive('dynamic', function ($compile) {
  return {
    replace: true,
    link: function (scope, ele, attrs) {
      scope.$watch(attrs.dynamic, function(html) {
        if (!html) {
            return;
        }
        ele.html((typeof(html) === 'string') ? html : html.data);
        $compile(ele.contents())(scope);
      });
    }
  };
});

Thanks,

Andy

Andy
  • 2,709
  • 5
  • 37
  • 64
  • I just found https://github.com/angular-ui/ui-router. The url properties can be ommited and this may work. – Andy Nov 07 '13 at 21:11

5 Answers5

64

I would use the built-in ngInclude directive. In the example below, you don't even need to write any javascript. The templates can just as easily live at a remote url.

Here's a working demo: http://plnkr.co/edit/5ImqWj65YllaCYD5kX5E?p=preview

<p>Select page content template via dropdown</p>
<select ng-model="template">
    <option value="page1">Page 1</option>
    <option value="page2">Page 2</option>
</select>

<p>Set page content template via button click</p>
<button ng-click="template='page2'">Show Page 2 Content</button>

<ng-include src="template"></ng-include>

<script type="text/ng-template" id="page1">
    <h1 style="color: blue;">This is the page 1 content</h1>
</script>

<script type="text/ng-template" id="page2">
    <h1 style="color:green;">This is the page 2 content</h1>
</script>
Giacomo1968
  • 25,759
  • 11
  • 71
  • 103
jessegavin
  • 74,067
  • 28
  • 136
  • 164
  • This is good stuff. Thank you very much! I need to spend some time learning the built-in angular functionality! – Andy Nov 07 '13 at 21:34
  • 2
    Check out the official video about the subject -http://campus.codeschool.com/courses/shaping-up-with-angular-js/level/2/section/2/video/1 – Alex Klaus Aug 05 '14 at 11:17
17

There is a another way also

  1. step 1: create a sample.html file
  2. step 2: create a div tag with some id=loadhtml Eg : <div id="loadhtml"></div>
  3. step 3: in Any Controller

        var htmlcontent = $('#loadhtml ');
        htmlcontent.load('/Pages/Common/contact.html')
        $compile(htmlcontent.contents())($scope);
    

This Will Load a html page in Current page

Stephan Muller
  • 27,018
  • 16
  • 85
  • 126
varun joshi
  • 195
  • 1
  • 3
  • 7
    Should be with AngularJs, not with jQuery – MaicolBen Nov 18 '14 at 19:22
  • Is there a way to do it from a factory? (since we can't access $scope from factories...) – mila Feb 25 '15 at 15:28
  • @mila how would you know which 'scope' you want to bind to in a factory? I've done this by having a variable called scope in the factory and have the viewModels pass the $scope to the factory on initialization. – Jonny Lin Mar 05 '15 at 01:31
  • @JonnyLin I ended up doing something a little complicated. In my factory I set 'globalVariablesService.viewReset = true' when needed. Then, in my controller, I have a $watch for viewReset: $scope.$watch(function () { return globalVariables.viewReset }, function () { if (globalVariables.viewReset) { $http.get("/views/social_template.html") .success(function(data) { $scope.social_content = data; });}}); ...continuing in next comment – mila Mar 05 '15 at 10:46
  • @JonnyLin In addition, In my view I have a directive 'social' And I'm watching its attr.content: .directive('social', function($compile, $parse) { return { restrict: 'E', link: function(scope, element, attr) { scope.$watch(attr.content, function() { element.html($parse(attr.content)(scope)); $compile(element.contents())(scope); ....} – mila Mar 05 '15 at 10:50
  • 1
    @JonnyLin this helped me a lot: http://stackoverflow.com/questions/21370080/ng-click-not-working-after-compile-ng-bind-html/21374642 – mila Mar 05 '15 at 10:51
  • This should be fixed to have the call to `$compile` in a `load()` callback: htmlcontent.load('/Pages/Common/contact.html', function(){ $compile(htmlcontent.contents())($scope); }); – Seán Hayes Jun 16 '16 at 04:53
12

For those, like me, who did not have the possibility to use angular directive and were "stuck" outside of the angular scope, here is something that might help you.

After hours searching on the web and on the angular doc, I have created a class that compiles HTML, place it inside a targets, and binds it to a scope ($rootScope if there is no $scope for that element)

/**
 * AngularHelper : Contains methods that help using angular without being in the scope of an angular controller or directive
 */
var AngularHelper = (function () {
    var AngularHelper = function () { };

    /**
     * ApplicationName : Default application name for the helper
     */
    var defaultApplicationName = "myApplicationName";

    /**
     * Compile : Compile html with the rootScope of an application
     *  and replace the content of a target element with the compiled html
     * @$targetDom : The dom in which the compiled html should be placed
     * @htmlToCompile : The html to compile using angular
     * @applicationName : (Optionnal) The name of the application (use the default one if empty)
     */
    AngularHelper.Compile = function ($targetDom, htmlToCompile, applicationName) {
        var $injector = angular.injector(["ng", applicationName || defaultApplicationName]);

        $injector.invoke(["$compile", "$rootScope", function ($compile, $rootScope) {
            //Get the scope of the target, use the rootScope if it does not exists
            var $scope = $targetDom.html(htmlToCompile).scope();
            $compile($targetDom)($scope || $rootScope);
            $rootScope.$digest();
        }]);
    }

    return AngularHelper;
})();

It covered all of my cases, but if you find something that I should add to it, feel free to comment or edit.

Hope it will help.

RPDeshaies
  • 1,826
  • 1
  • 24
  • 29
  • what is the targetDom if the page loaded dynamically? – Asqan Jun 08 '15 at 11:22
  • Then wait for the `$targetDom` to be loaded and then call `AngularHelper.Compile` afterwards with the dom in which the html will be inserted and the html to insert in it – RPDeshaies Jun 08 '15 at 11:55
  • I get the error: " Uncaught TypeError: $(...).html(...).scope is not a function " if i give an id of a "loaded" dom. – Asqan Jun 08 '15 at 12:40
  • You need to pass a jQuery element to the Compile method like AngularHelper.Compile($("body"), "
    toto
    "). Also, is angular correctly loaded in your application ? http://i.imgur.com/tlQwPiM.png
    – RPDeshaies Jun 08 '15 at 12:44
  • this is actually what i try to fix :) i should load angular each time to get my dynamically loading html page work with it. [more info](http://stackoverflow.com/questions/30506907/is-it-possible-to-use-scripts-of-extjs-in-external-html) – Asqan Jun 08 '15 at 13:02
  • I think I understand. But angular should only be loaded one time in the life cycle of your application. First you include angular.min.js then if you get dynamic html without using the ng-include or ng-view directive, you can compile it using the class above. When you include angular.min.js in your solution, the `scope` function should be acessible to every dom elemetn selected using the jQuery method – RPDeshaies Jun 08 '15 at 13:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/79957/discussion-between-asqan-and-tareck117). – Asqan Jun 08 '15 at 13:19
  • @RPDeshaies, where should I put this class? Also, In my case I have a jQuery plugin that i use in my Angular app. my plugin drag n drops html in my angular page, I want to bind that html with my angular code. Will your class help in this case? – DShah Mar 05 '18 at 11:01
  • @DShah If you are still using Angular 1.X, yes I guess it could. I wrote that answer like 3 years ago so I strongly suggest you upgrade your project from AngularJS to Angular (5) and use a more standard approach if possible – RPDeshaies Mar 06 '18 at 15:18
2

See if this example provides any clarification. Basically you configure a set of routes and include partial templates based on the route. Setting ng-view in your main index.html allows you to inject those partial views.

The config portion looks like this:

  .config(['$routeProvider', function($routeProvider) {
    $routeProvider
      .when('/', {controller:'ListCtrl', templateUrl:'list.html'})
      .otherwise({redirectTo:'/'});
  }])

The point of entry for injecting the partial view into your main template is:

<div class="container" ng-view=""></div>
rg88
  • 20,742
  • 18
  • 76
  • 110
  • 1
    Thanks for the answer although I don't think this will work since I have multiple views but need to keep the URL always at the root. – Andy Nov 11 '13 at 14:54
2

I needed to execute an directive AFTER loading several templates so I created this directive:

utilModule.directive('utPreload',
    ['$templateRequest', '$templateCache', '$q', '$compile', '$rootScope',
    function($templateRequest, $templateCache, $q, $compile, $rootScope) {
    'use strict';
    var link = function(scope, element) {
        scope.$watch('done', function(done) {
            if(done === true) {
                var html = "";
                if(scope.slvAppend === true) {
                    scope.urls.forEach(function(url) {
                        html += $templateCache.get(url);
                    });
                }
                html += scope.slvHtml;
                element.append($compile(html)($rootScope));
            }
        });
    };

    var controller = function($scope) {
        $scope.done = false;
        $scope.html = "";
        $scope.urls = $scope.slvTemplate.split(',');
        var promises = [];
        $scope.urls.forEach(function(url) {
            promises.add($templateRequest(url));
        });
        $q.all(promises).then(
            function() { // SUCCESS
                $scope.done = true;
            }, function() { // FAIL
                throw new Error('preload failed.');
            }
        );
    };

    return {
        restrict: 'A',
        scope: {
            utTemplate: '=', // the templates to load (comma separated)
            utAppend: '=', // boolean: append templates to DOM after load?
            utHtml: '=' // the html to append and compile after templates have been loaded
        },
        link: link,
        controller: controller
    };
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>

<div class="container-fluid"
     ut-preload
     ut-append="true"
     ut-template="'html/one.html,html/two.html'"
     ut-html="'<my-directive></my-directive>'">
 
</div>
TekTimmy
  • 3,066
  • 2
  • 29
  • 33