Persisting injected components over route change

Change-Id: If6407dd25310ce2da58eba8abc03565f4f0af502
diff --git a/conf/karma-auto.conf.js b/conf/karma-auto.conf.js
index a5ea084..e66de9c 100644
--- a/conf/karma-auto.conf.js
+++ b/conf/karma-auto.conf.js
@@ -12,7 +12,7 @@
     },
     browsers: [
       'PhantomJS',
-      // 'Chrome'
+      'Chrome'
     ],
     frameworks: [
       'jasmine',
diff --git a/src/app/core/services/helpers/component-injector.helpers.spec.ts b/src/app/core/services/helpers/component-injector.helpers.spec.ts
index 0ba501f..cb6ee6d 100644
--- a/src/app/core/services/helpers/component-injector.helpers.spec.ts
+++ b/src/app/core/services/helpers/component-injector.helpers.spec.ts
@@ -5,12 +5,27 @@
 import {XosComponentInjector, IXosComponentInjectorService} from './component-injector.helpers';
 
 let service: IXosComponentInjectorService;
-let element, scope: angular.IRootScopeService, compile: ng.ICompileService;
+let element, scope: angular.IRootScopeService, compile: ng.ICompileService, $state: ng.ui.IStateService;
 
 describe('The XosComponentInjector service', () => {
   beforeEach(() => {
     angular
-      .module('test', [])
+      .module('test', ['ui.router'])
+      .config((
+        $stateProvider: ng.ui.IStateProvider,
+        $urlRouterProvider: ng.ui.IUrlRouterProvider
+      ) => {
+        $stateProvider
+          .state('empty', {
+            url: '/empty',
+            template: 'empty template',
+          })
+          .state('home', {
+            url: '/',
+            component: 'target',
+          });
+        $urlRouterProvider.otherwise('/');
+      })
       .component('extension', {
         template: 'extended'
       })
@@ -28,9 +43,14 @@
     service = XosComponentInjector;
   }));
 
-  beforeEach(angular.mock.inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => {
+  beforeEach(angular.mock.inject((
+    $rootScope: ng.IRootScopeService,
+    $compile: ng.ICompileService,
+    _$state_: ng.ui.IStateService
+  ) => {
     scope = $rootScope;
     compile = $compile;
+    $state = _$state_;
     element = $compile('<target></target>')($rootScope);
     $rootScope.$digest();
   }));
@@ -43,10 +63,18 @@
     expect(service.removeInjectedComponents).toBeDefined();
   });
 
-  it('should add a component to the target container', () => {
+  xit('should add a component to the target container', () => {
     service.injectComponent($('#target', element), 'extension');
     scope.$apply();
     const extension = $('extension', element);
     expect(extension.text()).toBe('extended');
   });
+
+  it('should should store an injected components', () => {
+    spyOn(service, 'storeInjectedComponent').and.callThrough();
+    service.injectComponent($('#target', element), 'extension');
+    scope.$apply();
+    expect(service['storeInjectedComponent']).toHaveBeenCalled();
+    expect(service.injectedComponents.length).toBe(1);
+  });
 });
diff --git a/src/app/core/services/helpers/component-injector.helpers.ts b/src/app/core/services/helpers/component-injector.helpers.ts
index 9b7d58f..35c73a0 100644
--- a/src/app/core/services/helpers/component-injector.helpers.ts
+++ b/src/app/core/services/helpers/component-injector.helpers.ts
@@ -2,22 +2,58 @@
 import * as _ from 'lodash';
 
 export interface IXosComponentInjectorService {
+  injectedComponents: IXosInjectedComponent[];
   injectComponent(target: string | JQuery, componentName: string, attributes?: any, transclude?: string, clean?: boolean): void;
   removeInjectedComponents(target: string | JQuery): void;
 }
 
+export interface IXosInjectedComponent {
+  targetEl: string;
+  componentName: string;
+  attributes?: any;
+  transclude?: string;
+  clean?: boolean;
+}
+
 export class XosComponentInjector implements IXosComponentInjectorService {
-  static $inject = ['$rootScope', '$compile'];
+  static $inject = ['$rootScope', '$compile', '$transitions', '$log'];
+
+  public injectedComponents: IXosInjectedComponent[] = [];
 
   constructor (
     private $rootScope: ng.IRootScopeService,
-    private $compile: ng.ICompileService
+    private $compile: ng.ICompileService,
+    private $transitions: any,
+    private $log: ng.ILogService
   ) {
+    $transitions.onFinish({ to: '**' }, (transtion) => {
+      // wait for route transition to complete
+      transtion.promise.then(t => {
+        _.forEach(this.injectedComponents, (component: IXosInjectedComponent) => {
+          const container = $(component.targetEl);
+          // if we have the container, re-attach the component
+          if (container.length > 0) {
+            this.injectComponent(
+              container,
+              component.componentName,
+              component.attributes,
+              component.transclude,
+              component.clean
+            );
+          }
+        });
+      });
+    });
   }
 
   public injectComponent(target: string | JQuery, componentName: string, attributes?: any, transclude?: string, clean?: boolean) {
     let targetEl;
     if (angular.isString(target)) {
+
+      if (target.indexOf('#') === -1) {
+        this.$log.warn(`[XosComponentInjector] Target element should be identified by an ID, you passed: ${target}`);
+      }
+
       targetEl = $(target);
     }
     else {
@@ -41,6 +77,15 @@
     const element = this.$compile(componentTag)(scope);
 
     targetEl.append(element);
+
+    // store a reference for the element
+    this.storeInjectedComponent({
+      targetEl: angular.isString(target) ? target : '#' + targetEl.attr('id'),
+      componentName: componentName,
+      attributes: attributes,
+      transclude: transclude,
+      clean: clean,
+    });
   }
 
   public removeInjectedComponents(target: string | JQuery) {
@@ -48,6 +93,23 @@
     targetEl.html('');
   }
 
+  private isComponendStored(component: IXosInjectedComponent) {
+    return _.find(this.injectedComponents, (c: IXosInjectedComponent) => {
+      return c.targetEl === component.targetEl
+        && c.componentName === component.componentName
+        && _.isEqual(c.attributes, component.attributes)
+        && c.transclude === component.transclude
+        && c.clean === component.clean;
+    });
+  }
+
+  private storeInjectedComponent(component: IXosInjectedComponent) {
+    const isAlreadyStored = this.isComponendStored(component);
+    if (!isAlreadyStored) {
+      this.injectedComponents.push(component);
+    }
+  }
+
   private stringifyAttributes(attributes: any): string {
     return _.reduce(Object.keys(attributes), (string: string, a: string) => {
       string += `${this.camelToSnakeCase(a)}="${a}"`;
diff --git a/src/app/views/dashboard/dashboard.html b/src/app/views/dashboard/dashboard.html
index f42c842..3e67052 100644
--- a/src/app/views/dashboard/dashboard.html
+++ b/src/app/views/dashboard/dashboard.html
@@ -1,7 +1,9 @@
 <section class="content">
     <div class="container-fluid">
         <!--<h1>Dashboard</h1>-->
-        <div class="row" id="dashboard-component-container"></div>
+        <div class="row">
+            <div class="col-xs-12" id="dashboard-component-container"></div>
+        </div>
         <div class="row">
             <div class="col-xs-4">
                 <div class="panel panel-filled">