import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <split-container>
      <div class="navigation" split-behaviour="fixed">
        Navigation<br>
          Navigation<br>
            Navigation<br>
              Navigation<br>
                Navigation<br>
         Navigation<br>
          Navigation<br>
            Navigation<br>
              Navigation<br>
                Navigation<br>        
      </div>

      <div class="content" split-behaviour="dynamic">
        Content<br>
        Content<br>
        Content<br>
      Content<br>
              Content<br>
    Content<br>
        Content<br>
        Content<br>
      Content<br>
              Content<br>
      </div>
    </split-container>
  `,
})
export class AppComponent { name = 'Angular'; }
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';
import { SplitContainerComponent } from './splitContainer.component';
import { SplitBehaviourDirective } from './splitBehaviour.directive';
import { SplitterComponent } from './splitter.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ 
    AppComponent, 
    SplitBehaviourDirective,
    SplitterComponent,
    SplitContainerComponent
  ],
  entryComponents: [
    SplitterComponent
  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html>
  <head>
    <title>Angular Quickstart</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      h1 {
        color: #369;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 250%;
      }
      
      .navigation {
        overflow:auto;
        color:#fff;
        background-color:#404040;
        padding:4px;
        border: 1px solid black;
      }
      
      .content {
        color:#000;
        background-color:#f0f0f0;
        padding:4px 4px 4px 20px;
        border: 1px solid black;
        margin-top: 25px;
      }
      
      .splitter {
        background-color:#d0d0d0;
      }
    </style>

    <!-- Polyfills -->
    <script src="https://unpkg.com/core-js/client/shim.min.js"></script>

    <script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
    <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading AppComponent content here ...</my-app>
  </body>

</html>
(function (global) {
  System.config({
    // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
    transpiler: 'ts',
    typescriptOptions: {
      // Copy of compiler options in standard tsconfig.json
      "target": "es5",
      "module": "commonjs",
      "moduleResolution": "node",
      "sourceMap": true,
      "emitDecoratorMetadata": true,
      "experimentalDecorators": true,
      "lib": ["es2015", "dom"],
      "noImplicitAny": true,
      "suppressImplicitAnyIndexErrors": true
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },
    paths: {
      // paths serve as alias
      'npm:': 'https://unpkg.com/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',

      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
      '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',

      // other libraries
      'rxjs':                      'npm:rxjs@5.0.1',
      'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
      'ts':                        'npm:plugin-typescript@5.2.7/lib/plugin.js',
      'typescript':                'npm:typescript@2.0.10/lib/typescript.js',

    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        main: './main.ts',
        defaultExtension: 'ts'
      },
      rxjs: {
        defaultExtension: 'js'
      }
    }
  });

})(this);

import { Component, EventEmitter, Output, ViewChild, ElementRef,HostListener, NgZone } from '@angular/core';
import { SplitBehaviourDirective, Position } from './splitBehaviour.directive';

/**
 * A horizontal, draggable divider element between resizable content areas within a split container.
 * This component and its creation is managed by a split container.
 **/
@Component({
    selector: 'splitter',
    template: `
        <div #splitter class="splitter" 
           ></div>
    `,
    host: { 'style': 'position:relative' },
    styles: [`
        .splitter {
            flex: 0 0 auto;
            width: 100%;
            height: 25px;
            cursor: row-resize;

            background-position:50% 50%;
            background-repeat:no-repeat;

            /* Needed for height:100% without having an explicit height given to the parent */
            position:absolute;
        }
    `]
})
export class SplitterComponent {
    private startX: number;

    private startWidth: number;

    private dragging: boolean;

    @ViewChild('splitter')
    private element: ElementRef;

    private _splitBehaviour: SplitBehaviourDirective;

    @Output()
    public positionChanged: EventEmitter<Position> = new EventEmitter();

    constructor(private zone: NgZone) {
        this.dragging = false;
    }

    public set splitBehaviour(value: SplitBehaviourDirective) {
        this._splitBehaviour = value;
    }

    public get splitBehaviour() {
        return this._splitBehaviour;
    }
    
    
    
    @HostListener('mousedown', ['$event'])
    @HostListener('touchstart', ['$event'])
    private onMouseDown(e: MouseEvent): void {
        if (((<any>window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) {
			e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0];
		}
        
        this.dragging = true;
        this.startY = e.clientY;
        this.startWidth = this.splitBehaviour.getElementHeight();

        this.zone.runOutsideAngular(() => {
           window.document.addEventListener('mousemove', this.onMouseMove.bind(this));
           window.document.addEventListener('touchmove', this.onMouseMove.bind(this));
        });
        
        
    }

    @HostListener('document:mouseup', ['$event'])
    @HostListener('document:touchend', ['$event'])
    private onMouseUp(e): void {
        this.dragging = false;
    }

 
    private onMouseMove(e: MouseEvent): void {
        if (((<any>window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) {
			e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0];
		}
        
        if(this.dragging) {

            this.positionChanged.emit(new Position(this.startWidth + e.clientY - this.startY, e.pageX));
        }
    }

    private onMouseLeave(e: MouseEvent): void {
        this.dragging = false;
    }
}
import { Directive, ElementRef, Renderer, Input, OnInit } from '@angular/core';

export class Position {
    constructor(public x: number, public y: number) {
    }
}

export enum SplitBehaviour {
    fixed,
    dynamic
}

/**
 * Marks an element as content area inside a resizable split container.
 * The input value of the directive can either be 'fixed' or 'dynamic' and describes the way
 * the content area is expected to resize within the split container space. 
 * - A fixed element is expected to have an explicitly defined size, which can be resized by the user through a split element
 * - A dynamic elment is expected to fill up the space next to fixed elements.
 * 
 * Initially, the directive will set CSS flexbox attributes to stack them horizontally.
 * It also serves as a data provider to expose the element width of the hosting content area, which is most likely a div.
 * Last but not least, the directive takes care of resizing the host element.
 * 
 * @example 
 * <div style="width:100px" split-behaviour="fixed">
 * </div>
 **/
@Directive({
    selector: '[split-behaviour]'
})
export class SplitBehaviourDirective implements OnInit {
    private _behaviour: SplitBehaviour;

    constructor(private el: ElementRef, private renderer: Renderer) {
    }

    @Input('split-behaviour')
    public set behaviour(value: string) {
        this._behaviour = SplitBehaviour[value];
    }

    public get behaviour(): string {
        return SplitBehaviour[this._behaviour];
    }

    public resize(vector: Position) {
        this.renderer.setElementStyle(this.el.nativeElement, 'height', `${vector.x}px`);
    }

    public getElementHeight(): number {
        let paddingT = parseInt(window.getComputedStyle(this.el.nativeElement, null).getPropertyValue("padding-top"));
        let paddingB = parseInt(window.getComputedStyle(this.el.nativeElement, null).getPropertyValue("padding-bottom"));
        return <number>this.el.nativeElement.offsetHeight - paddingT - paddingB;
    }

    public ngOnInit() {
        if(this._behaviour.valueOf() == SplitBehaviour.fixed.valueOf()) {
            this.renderer.setElementStyle(this.el.nativeElement, 'flex', '0 0 auto');
        }
        else if(this._behaviour.valueOf() == SplitBehaviour.dynamic.valueOf()) {
            this.renderer.setElementStyle(this.el.nativeElement, 'flex', '1 1 auto');
        }
        this.renderer.setElementStyle(this.el.nativeElement, 'overflow', 'auto');
    }
}
import { Component, ContentChildren, QueryList, ComponentFactoryResolver, ViewContainerRef, AfterContentInit } from '@angular/core';

import { SplitterComponent } from './splitter.component';
import { SplitBehaviourDirective, SplitBehaviour, Position } from './splitBehaviour.directive';

/**
 * Hosts resizable content areas divided by a draggable border (splitter). 
 * 
 * The split container defined flex attributes to allow the horizontal arrangement of child content areas.
 * On initialization, it will query all child elements in the light DOM annotated by the split-behaviour directive
 * and separate them by splitters.
 * As the splitBehaviour directive manages concrete content area resizing, dragging events of the splitter (positionChanged) are subscribed
 * and propagated to the directive.
 **/
@Component({
    selector: 'split-container',
    template: `
        <div class="split-container">
            <ng-content></ng-content>
        </div>
    `,
    styles: [`
        .split-container {
            display: flex;
            flex-direction: column;
            flex-wrap: no-wrap;
            flex-grow: 1;
            height:500px;
        }
    `]
})
export class SplitContainerComponent implements AfterContentInit {
    // Workaround: We want to query all child elements hosting a SplitBehaviourDirective instance,
    // but we both need the respective ViewContainerRef (for splitter element creation)
    // as well as the respective Directive implementation instance.
    // There might be a better way to achive this...
    @ContentChildren(SplitBehaviourDirective, {read: ViewContainerRef})
    private panesVcr: QueryList<ViewContainerRef>;

    @ContentChildren(SplitBehaviourDirective)
    private panes: QueryList<SplitBehaviourDirective>;

    constructor(private resolver: ComponentFactoryResolver) {
    }

    public ngAfterContentInit(): void {
        let splitterFactory = this.resolver.resolveComponentFactory(SplitterComponent);

        let paneDirectives = this.panes.toArray();
        this.panesVcr.map((vcr, idx) => {
            if(paneDirectives[idx].behaviour == SplitBehaviour[SplitBehaviour.fixed]) {
                let splitter = vcr.createComponent(splitterFactory);
                splitter.instance.splitBehaviour = paneDirectives[idx];
                splitter.instance.positionChanged.subscribe((pos: Position) => {
                    paneDirectives[idx].resize(pos);
                });
            }
        });
    }
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   width="2mm"
   height="7mm"
   viewBox="0 0 7.086614 24.803149"
   id="svg2"
   version="1.1"
   inkscape:version="0.91 r13725"
   sodipodi:docname="split-horizontal.svg">
  <defs
     id="defs4" />
  <sodipodi:namedview
     id="base"
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1.0"
     inkscape:pageopacity="0.0"
     inkscape:pageshadow="2"
     inkscape:zoom="11.2"
     inkscape:cx="11.262352"
     inkscape:cy="21.820265"
     inkscape:document-units="px"
     inkscape:current-layer="layer1"
     showgrid="false"
     inkscape:window-width="1920"
     inkscape:window-height="1005"
     inkscape:window-x="-9"
     inkscape:window-y="-9"
     inkscape:window-maximized="1" />
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     inkscape:label="Ebene 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(0,-1027.5591)">
    <circle
       style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1"
       id="path4136"
       cx="3.6556082"
       cy="1030.9225"
       r="3.0174825" />
    <circle
       r="3.0174825"
       cy="1039.877"
       cx="3.6556082"
       id="circle4138"
       style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1" />
    <circle
       style="opacity:1;fill:#909090;fill-opacity:1;stroke:none;stroke-width:8;stroke-opacity:1"
       id="circle4140"
       cx="3.6556082"
       cy="1048.8313"
       r="3.0174825" />
  </g>
</svg>