2
\$\begingroup\$

I read this article that I really liked and wanted to implement the ideas it had. It basically says don't use $scope or ng-controller; instead, use directives with templates that get their angular magic from the directive's controller.

angular.module('myAppContestantList', [])
  .directive('myAppContestantList', function() {
    return {
      scope: {},
      templateUrl: 'my_app_contestant_list.html',
      replace: true,
      controller: 'ContestantListCtrl',
      controllerAs: 'ctrl'
    };
  })
  .controller('ContestantListCtrl', function() {
    this.contestants = [
      {firstName: 'Rachel', lastName: 'Washington'},
      {firstName: 'Joshua', lastName: 'Foster'},
      {firstName: 'Samuel', lastName: 'Walker'},
      {firstName: 'Phyllis', lastName: 'Reynolds'}
    ];
  });

where in the html template you can have something like

<li ng-repeat="contestant in ctrl.contestants">
      {{contestant.firstName}} {{contestant.lastName}}
</li>

ctrl only exists in the template however, so my problem was that I was using server sided templates which meant that I couldn't do things like ctrl.anything in my code because it's not part of the directive's template. So here's where the abuse comes in because I've never seen anyone else do this. I took advantage of the tranclude attribute of a directive to export the scope of the directive's controller into the template I was using. For instance here is one of my actual modules from demoing this:

(function() {
    angular.module('features', [])
        .directive('features', function() {
            return {
                scope: {},
                restrict: 'A',
                controller: 'featuresController',
                controllerAs: 'ctrl',
                transclude: true,
                link: function(scope, elem, attrs, ctrl, transclude) {
                    transclude(scope, function(clone) {
                        elem.append(clone);
                    });
                }
            };
        })
        .controller('featuresController', function(modalList) {

            var activeModal;
            var modals = modalList.modals;

            this.openModal = function(modalId) {

                for (var i = 0; i < modals.length; i++) {
                    if (modals[i].modalId === modalId) {
                        activeModal = modals[i];
                        activeModal.open();
                    }
                }
            };

            this.closeModal = function() {
                activeModal.close();
            };
        });
})();

The magic happens in the link function using the transclude argument:

link: function(scope, elem, attrs, ctrl, transclude) {
    transclude(scope, function(clone) {
        elem.append(clone);
    });
}

The first argument of the tranclude function is the scope of the directive, and in function(clone), the clone is the HTML inside the directive. The clone is simply being appended to the element where it already existed, but it now has the directive's scope.

This allows me to do this without having a template:

<section class="features-page" features> //This is the directive
    //All of this would be the "clone"
    ...All the other HTML
    <modal data-ng-click="ctrl.closeModal()" class="modal-closed">
</section>

I can now use ctrl.closeModal() outside of a directive template. There's also one more added benefit. By not having everything inside of a directive template, the template can load before angularJS has been loaded. This allowed me to move all my scripts to the bottom of my page (One important thing to note is that you can't use ng-cloak if angular is defined last, meaning that unless you apply some CSS for display: none your page will look unfinished for the time it takes angular to load).

And the abuse didn't stop there. I now had a features module, but I wasn't exactly sure how I was going to dependency inject it into my main app module. At first I had an init file that looked like this that would be concatenated first to my full JavaScript file.

(function() {
    angular.module('features', []);
    angular.module('modal', []);
    angular.module('app', ['features', 'ngRoute']);
})();

and in my actual directives file I would define their directives and stuff just using

angular.module('myModuleName').whatever

because the form with the [] is a setter for the module and without it's a getter. So I could define my modules in the init file, and then populate them later.

This solved the module loading errors, but it was getting super bloated as I added more modules.

So at the top of my template I added:

<script>
    function loadModules() {
        angular.module('app').requires.push('features', 'modal');
    }
</script>

and at the bottom where I had angular and the rest defined:

<script src="assets/js/vendor.js" type="text/javascript"></script>
<script src="assets/js/app.js" type="text/javascript"></script>
<script type="text/javascript">
    loadModules();
</script>

This way my modules could be loaded on the pages on which they were needed.

Now all of this works fine for me, but I feel like I've broken a lot of conventions. Are there any problems with what I'm doing?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.