javascriptangularjsangularjs-directive

How can I add dynamic directives to my angular app on the fly?


I have an application where as I need to emulate an os window type behavior. So I need the ability to add multiple windows each with their own context and potentially their own nested directives.

I already have a windowDirective and userProfile directives, I would like to instantiate a new instance of each and add it or remove it from the dom as necessary.

I have thus far attempted to accomplish this with a WindowService using $compile to essentially compile a string with the directive markup in it i.e. $compile("<window><user-profile></user-profile></window>")(scope), which appears to have created a dom tree but spewed a bunch of errors.

Any ideas on how I might approach this?


Solution

  • I have done something similar.

    For starter you need to create a directive which will be your main container (for other windows to be placed inside). Assign a controller to that main container which will retain the list of all other sub-windows you plan to insert/remove. This is needed to clean-up scopes and memory whenever you want to destroy those.

    var module = angular.module('myModule', []);
    
    module.directive('myContainer', ContainerDirective);
    function ContainerDirective() {
        return {
           restrict: 'E',
           controller: ['$scope', '$element', '$compile', function ContainerController($scope, $element, $compile) {
                var winId = 0;
                this.subWindows = {};
                //Create a window
                this.createWindow = function() {
                    var subWinElem = angular.element('<my-window>');
                    this.subWindows[winId++] = subWinElem;
                    $element.append(subWinElem);
                    $compile(subWinElem)(scope);
                    subWinElem.data('window-id', winId);
                    return winId;
                };
                //Destroy a window
                this.destroyWindow = function(winId) {
                    if(this.subWindows[winId]) {
                       var subWinElem = this.subWindows[winId],
                           subWinScope = subWinElem.scope();
                       subWinElem.remove();
                       subWinScope.$destroy();
                       this.subWindows[winId] = null;
                    }
                };
                //Clean up on container destroy
                this.dispose = function() {
                    angular.forEach(this.subWindows, function(subWinElem) {
                        if(subWinElem) {
                           var subWinScope = subWinElem.scope();
                           subWinElem.remove();
                           subWinScope.$destroy();
                        }
                    }); 
                };
           }],
           link: function($scope, elem, attrs, ContainerController) {
              //On click of a button you would create a sub window
              scope.createWindow = function() {
                   ContainerController.createWindow();
              };
              //Cleanup anything left in the controller
              $scope.$on('$destroy', function() {
                 ContainerController.dispose();
              });
           }
        };
    }
    

    Sub-windows should be directives that would 'require' parent controller. To invoke them dynamically what you can do is append the directive tag first and then $compile the reference to that element (much better then $compile('string')). Because you append element first and then compile you, are able to require parent controller without a problem (since it uses inheritedData).

    module.directive('myWindow', WindowDirective);
    function WindowDirective() {
       return {
           restrict: 'E',
           scope: true,
           require: '?^myContainer',
           link: function($scope, elem, attrs, ContainerController) {
              var winId = elem.data('window-id');
              //You would destroy window like so
              $scope.$on('$destroy', function() {
                  ContainerController.destroyWindow(winId);
              });
           }
       }
    }
    

    PS: this is a much simplified example (and may contain typos :P) but you get the gist of it.