Angular Multiple ngIncludes with Transclusion on Page Losing Scope(?)

109 views Asked by At

I am trying to build a modular ngInclude. By this, I mean that the ngIncluded JSP will pull in all the dependencies it needs and recompile itself before rendering.

This same file is used multiple times on a single page. The main page has several tabs - all of which use this ngInclude just with some different query string parameters. Also, on each page, I have a "preview" version of the included file, and then when they click on the preview a modal pops up, which again ngIncludes the same file, but has another query string parameter which puts it in edit mode.

I've gotten this to work everywhere except IE9 (gah), and, of course, I have to support IE9 and am having to refactor everything. Because of wanting to make this ngInclude modular and having it include all of its own dependencies, every time it was ngIncluded it was bringing in those same dependencies again. This was fine in more modern browsers that handled that more gracefully, but in IE9 it would blow up with multiple directives asking for template or multiple directives asking for transclusion. The only leads I could find that were applicable to my situation indicated that it was because of the dependencies being pulled in multiple times. And, with what I've been able to get working thus far, I've confirmed that that seems to be the case.

So, here I am. I'm trying to make this work by only bringing in the dependencies once (with RequireJS). I am not using routes. I have it working on initial page load, but once I change tabs or open the edit modal (both of which actions cause another ngInclude), I don't get anything. I literally get a blank page.

I've put in console.logs and can see that the order of operations is as it should be (hits my recompile directive, it does everything all the way through the $compile and then the inner directives are hit). I can also use the browser console to access $('selectorForController').scope().items or $('selectorForController').scope().params and confirm that they have the data that I'm expecting. The problem is that for some reason the view isn't updating (which is what I expected the $compile to trigger).

App Definition:

var providers = {};
var myApp = angular.module('myApp', ['ngSanitize', 'ngAnimate', 'ngResource', 'pascalprecht.translate', 'angularFileUpload', function($controllerProvider, $compileProvider, $provide) {
    providers = {
        $controllerProvider : $controllerProvider,
        $compileProvider : $compileProvider,
        $provide : $provide
    };
}]);
var queueLen = myApp._invokeQueue.length;

ngIncluded JSP:

<jsp:include page="profileIncludeListTemplate.jsp"/> <%-- Contains the profileItemList.jsp ng-template --%>
<jsp:include page="profileIncludePanelTemplate.jsp"/> <%-- Contains the profileItemPageRightPanel.jsp ng-template --%>

<script type="text/javascript">
    // Define itemsData and params here
</script>

<%-- One of my iterations was trying to use a directive I called lazyscript to load in all of the dependencies and then recompile, but that didn't work --%>
<%--<lazyscript data-src="/assets/js/profile/profileItemPageService.js"></lazyscript>--%>
<%--<lazyscript data-src="/assets/js/profile/profileItemService.js"></lazyscript>--%>
<%--<lazyscript data-src="/assets/js/profile/profileIncludeController.js"></lazyscript>--%>
<%--<lazyscript data-src="/assets/js/profile/profileIncludeDirectives.js"></lazyscript>--%>

<%-- Another iteration was trying to get it to work this way, where I also wrote a directive to overload the default script tag and recompile if necessary, but that didn't work either --%>
<%--<script src="/assets/js/profile/profileItemPageService.js" type="text/javascript"></script>--%>
<%--<script src="/assets/js/profile/profileItemService.js" type="text/javascript"></script>--%>
<%--<script src="/assets/js/profile/profileIncludeDirectives.js" type="text/javascript"></script>--%>
<%--<script src="/assets/js/profile/profileIncludeController.js" type="text/javascript"></script>--%>

<%-- I can't define the ngController here because it will throw an error on initial load and stop the rest of the execution, so I have the recompile apply it --%>
<%-- So I ultimately settled on this, which defines which dependencies need to be pulled in so that I can hand that list to Require --%>
<div
    data-recompile
    data-recompile-controller="ProfileIncludeCtrl"
    data-recompile-dependencies="/assets/js/profile/profileItemPageService.js,/assets/js/profile/profileItemService.js,/assets/js/profile/profileIncludeController.js,/assets/js/profile/profileIncludeDirectives.js"
    data-recompile-finished="false"
    class="profile-include"
    data-ng-class="{'form-horizontal': !params.edit}">

    <profile-item-list-directive></profile-item-list-directive>
    <profile-item-page-right-panel-directive></profile-item-page-right-panel-directive>
</div>

Recompile Directive:

myApp.directive('recompile', function($window, $q) {
    return {
        restrict: 'A',
        link: function ($scope, elem, attrs) {
            if($(elem).attr('data-recompile-finished') == 'true') return;
            var dependencies = $(elem).attr('data-recompile-dependencies').split(',');

            function recompiler() {
                $(elem).attr('data-recompile-finished', 'true');
                $(elem).attr('data-ng-controller', $(elem).attr('data-recompile-controller')); // Link the angular controller

                var queue = myApp._invokeQueue;
                for (var i = queueLen; i < queue.length; i++) {
                    var call = queue[i];
                    var provider = providers[call[0]];
                    if (provider) {
                        provider[call[1]].apply(provider, call[2]);
                    }
                }

                if ($('[data-ng-app="myApp"]').injector()) {
                    $('[data-ng-app="myApp"]').injector().invoke(function ($compile, $rootScope) {
                        $compile($(elem))($rootScope);
                    });
                }
            }

            requirejs(dependencies, recompiler);
        }
    };
});

Relevant portion of the outer controller (defined in profileIncludeController.js):

myApp.controller('ProfileIncludeCtrl', function($scope, $rootScope, $resource, $timeout, ProfileItemService, ProfileItemPageService) {
    $scope.items = [];
    $scope.params = [];

    $scope.params = buildParamsObject(params);
    $scope.items = itemsData.data || [];
});

Relevant portions of the directives (defined in profileIncludeDirectives.js). These directives are transcluded because they need to be able to access each other's scope as well as some functions that are in the ProfileIncludeCtrl because those functions didn't really belong in either directive.

myApp.directive('profileItemListDirective', function($rootScope, $timeout) {
    return {
        restrict : 'E',
        templateUrl : 'profileItemList.jsp',
        replace: true,
        transclude : true,
        scope : true,
        controller : function($scope, ProfileItemService, ProfileItemPageService) {
            console.log("listDirective controller");
        },
        link : function($scope, element, attrs) {
            console.log("listDirective link");
        }
    };
});

myApp.directive('profileItemPageRightPanelDirective', function($rootScope, $timeout) {
    return {
        restrict : 'E',
        templateUrl : 'profileItemPageRightPanel.jsp',
        replace: true,
        transclude : true,
        scope : true,
        controller : function($scope, ProfileItemService, ProfileItemPageService) {
            console.log("panelDirective controller");
        },
        link : function($scope, element, attrs) {
            console.log("panelDirective link");
            // $scope.params is not defined here, but I need and expect it to be
        }
    };
});

Any guidance would be appreciated!

1

There are 1 answers

0
Mike DeMille On BEST ANSWER

Turns out I was able to fix it by changing my recompile function slightly. Using the $controller service and putting the $ngControllerController data attribute on the children is the main idea that did it.

function recompiler() {
    var ctrl = $elem.attr('data-recompile-controller');
    $elem.attr('data-ng-controller', ctrl); // This is for aesthetics only

    $elem.removeAttr("data-recompile")
            .removeAttr("recompile")
            .removeAttr("data-recompile-controller")
            .removeAttr("recompile-controller")
            .removeAttr("data-recompile-dependencies")
            .removeAttr("recompile-dependencies");

    var queue = myApp._invokeQueue;
    for (var i = 0; i < queue.length; i++) {
        var call = queue[i];
        var provider = providers[call[0]];
        if (provider) {
            provider[call[1]].apply(provider, call[2]);
        }
    }

    var templateCtrl = $controller( ctrl, { $scope: $scope } );
    $elem.children().data('$ngControllerController', templateCtrl);
    $compile( $elem.contents() )( $scope );
}