+// `;
+// component.docViewer.doc = { metadata: fakeDocMetadata, content };
+
+// // necessary to trigger Bar's projection within ngOnInit
+// fixture.detectChanges();
+
+// const foos = docViewerEl.querySelectorAll('aio-foo');
+// expect(foos.length).toBe(2, 'should have 2 foos');
+
+// const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
+// expect(barHtml).toContain('###bar content###', 'should have bar with projected content');
+// });
+
+// it(('should not include Bar within Foo'), () => {
+// const content = `
+//
Top
+//
+//
+// ###bar content###
+//
+//
+//
+//
Bottom
+// `;
+// component.docViewer.doc = { metadata: fakeDocMetadata, content };
+
+// // necessary to trigger Bar's projection within ngOnInit
+// fixture.detectChanges();
+
+// const foos = docViewerEl.querySelectorAll('aio-foo');
+// expect(foos.length).toBe(2, 'should have 2 foos');
+
+// const bars = docViewerEl.querySelectorAll('aio-bar');
+// expect(bars.length).toBe(0, 'did not expect Bar inside Foo');
+// });
+
+// // because FooComponents are processed before BazComponents
+// it(('should include Foo within Bar'), () => {
+// const content = `
+//
Top
+//
+//
+// Inner
+//
+//
+//
+//
Bottom
+// `;
+// component.docViewer.doc = { metadata: fakeDocMetadata, content };
+
+// // necessary to trigger Bar's projection within ngOnInit
+// fixture.detectChanges();
+
+// const foos = docViewerEl.querySelectorAll('aio-foo');
+// expect(foos.length).toBe(2, 'should have 2 foos');
+
+// const bars = docViewerEl.querySelectorAll('aio-bar');
+// expect(bars.length).toBe(1, 'should have a bar');
+// expect(bars[0].innerHTML).toContain('Bar Component', 'should have bar template content');
+// });
+
+// // The tag and its inner content is copied
+// // But the BazComponent is not created and therefore its template content is not displayed
+// // because BarComponents are processed before BazComponents
+// // and no chance for first Baz inside Bar to be processed by builder.
+// it(('should NOT include Bar within Baz'), () => {
+// const content = `
+//
Top
+//
+//
+// Inner ---baz stuff---
+//
+//
+//
---More baz--
+//
Bottom
+// `;
+// component.docViewer.doc = { metadata: fakeDocMetadata, content };
+
+// // necessary to trigger Bar's projection within ngOnInit
+// fixture.detectChanges();
+// const bazs = docViewerEl.querySelectorAll('aio-baz');
+
+// // Both baz tags are there ...
+// expect(bazs.length).toBe(2, 'should have 2 bazs');
+
+// expect(bazs[0].innerHTML).not.toContain('Baz Component',
+// 'did not expect 1st Baz template content');
+
+// expect(bazs[1].innerHTML).toContain('Baz Component',
+// 'expected 2nd Baz template content');
+
+// });
+// });
diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts
new file mode 100644
index 0000000000..11f6c6575f
--- /dev/null
+++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.ts
@@ -0,0 +1,132 @@
+import {
+ Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
+ DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation
+} from '@angular/core';
+
+import { EmbeddedComponents } from 'app/embedded';
+import { DocumentContents } from 'app/documents/document.service';
+
+interface EmbeddedComponentFactory {
+ contentPropertyName: string;
+ factory: ComponentFactory;
+}
+
+// Initialization prevents flicker once pre-rendering is on
+const initialDocViewerElement = document.querySelector('aio-doc-viewer');
+const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElement.innerHTML : '';
+
+@Component({
+ selector: 'aio-doc-viewer',
+ template: '',
+ styles: [ `
+ :host >>> doc-title.not-found h1 {
+ color: white;
+ background-color: red;
+ }
+ `]
+ // TODO(robwormald): shadow DOM and emulated don't work here (?!)
+ // encapsulation: ViewEncapsulation.Native
+})
+export class DocViewerComponent implements DoCheck, OnDestroy {
+
+ private displayedDoc: DisplayedDoc;
+ private embeddedComponentFactories: Map = new Map();
+ private hostElement: HTMLElement;
+
+ constructor(
+ componentFactoryResolver: ComponentFactoryResolver,
+ elementRef: ElementRef,
+ embeddedComponents: EmbeddedComponents,
+ private injector: Injector
+ ) {
+ this.hostElement = elementRef.nativeElement;
+ // Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
+ this.hostElement.innerHTML = initialDocViewerContent;
+
+ for (const component of embeddedComponents.components) {
+ const factory = componentFactoryResolver.resolveComponentFactory(component);
+ const selector = factory.selector;
+ const contentPropertyName = this.selectorToContentPropertyName(selector);
+ this.embeddedComponentFactories.set(selector, { contentPropertyName, factory });
+ }
+ }
+
+ @Input()
+ set doc(newDoc: DocumentContents) {
+ console.log(newDoc);
+ this.ngOnDestroy();
+ if (newDoc) {
+ window.scrollTo(0, 0);
+ this.build(newDoc);
+ }
+ }
+
+ /**
+ * Add doc content to host element and build it out with embedded components
+ */
+ private build(doc: DocumentContents) {
+
+ const displayedDoc = this.displayedDoc = new DisplayedDoc(doc);
+
+ // security: the doc.content is always authored by the documentation team
+ // and is considered to be safe
+ this.hostElement.innerHTML = doc.contents || '';
+
+ if (!doc.contents) { return; }
+
+ // TODO(i): why can't I use for-of? why doesn't typescript like Map#value() iterators?
+ this.embeddedComponentFactories.forEach(({ contentPropertyName, factory }, selector) => {
+ const embeddedComponentElements = this.hostElement.querySelectorAll(selector);
+
+ // cast due to https://github.com/Microsoft/TypeScript/issues/4947
+ for (const element of embeddedComponentElements as any as HTMLElement[]){
+ // hack: preserve the current element content because the factory will empty it out
+ // security: the source of this innerHTML is always authored by the documentation team
+ // and is considered to be safe
+ element[contentPropertyName] = element.innerHTML;
+ displayedDoc.addEmbeddedComponent(factory.create(this.injector, [], element));
+ }
+ });
+ }
+
+ ngDoCheck() {
+ if (this.displayedDoc) { this.displayedDoc.detectChanges(); }
+ }
+
+ ngOnDestroy() {
+ // destroy components otherwise there will be memory leaks
+ if (this.displayedDoc) {
+ this.displayedDoc.destroy();
+ this.displayedDoc = undefined;
+ }
+ }
+
+ /**
+ * Compute the component content property name by converting the selector to camelCase and appending
+ * 'Content', e.g. live-example => liveExampleContent
+ */
+ private selectorToContentPropertyName(selector: string) {
+ return selector.replace(/-(.)/g, (match, $1) => $1.toUpperCase()) + 'Content';
+ }
+}
+
+class DisplayedDoc {
+
+ private embeddedComponents: ComponentRef[] = [];
+
+ constructor(private doc: DocumentContents) {}
+
+ addEmbeddedComponent(component: ComponentRef) {
+ this.embeddedComponents.push(component);
+ }
+
+ detectChanges() {
+ this.embeddedComponents.forEach(comp => comp.changeDetectorRef.detectChanges());
+ }
+
+ destroy() {
+ // destroy components otherwise there will be memory leaks
+ this.embeddedComponents.forEach(comp => comp.destroy());
+ this.embeddedComponents.length = 0;
+ }
+}
diff --git a/aio/src/app/layout/nav-item/nav-item.component.scss b/aio/src/app/layout/nav-item/nav-item.component.scss
new file mode 100644
index 0000000000..19ba2a0b86
--- /dev/null
+++ b/aio/src/app/layout/nav-item/nav-item.component.scss
@@ -0,0 +1,114 @@
+
+/************************************
+
+ Media queries
+
+To use these, put this snippet in the approriate selector:
+
+ @include bp(tiny) {
+ background-color: purple;
+ }
+
+ Replace "tiny" with "medium" or "big" as necessary.
+
+*************************************/
+
+@mixin bp($point) {
+
+ $bp-xsmall: "(min-width: 320px)";
+ $bp-teeny: "(min-width: 480px)";
+ $bp-tiny: "(min-width: 600px)";
+ $bp-small: "(min-width: 650px)";
+ $bp-medium: "(min-width: 800px)";
+ $bp-big: "(min-width: 1000px)";
+
+ @if $point == big {
+ @media #{$bp-big} { @content; }
+ }
+ @else if $point == medium {
+ @media #{$bp-medium} { @content; }
+ }
+ @else if $point == small {
+ @media #{$bp-small} { @content; }
+ }
+ @else if $point == tiny {
+ @media #{$bp-tiny} { @content; }
+ }
+ @else if $point == teeny {
+ @media #{$bp-teeny} { @content; }
+ }
+ @else if $point == xsmall {
+ @media #{$bp-xsmall} { @content; }
+ }
+}
+
+/************************************/
+
+.vertical-menu {
+ padding-left: 0;
+}
+
+a.vertical-menu {
+ color: #545454;
+ cursor: pointer;
+ display: block;
+ padding-bottom: 10px;
+ padding-top: 10px;
+ padding-right: 10px;
+ text-decoration: none;
+ text-align: left;
+ &:hover {
+ background-color: #ddd;
+ }
+}
+
+.vertical-menu.selected {
+ color:#018494;
+}
+
+.heading {
+ color: #444;
+ cursor: pointer;
+ font-size: .85rem;
+ min-width: 200px;
+ padding-left: 10px;
+ position: relative;
+ text-transform: uppercase;
+}
+
+.material-icons {
+ display: none;
+}
+
+.material-icons.active {
+ display: inline-block;
+ position: absolute;
+ top: 6px;
+ // left: 4px;
+}
+
+.heading-children {
+ display: none;
+}
+
+.heading-children.active {
+ display: block;
+}
+
+
+.heading.selected.level-1,
+.heading-children.selected.level-1 {
+ border-left: 3px #00bcd4 solid;
+}
+
+.level-1 {
+ padding-left: 10px;
+}
+
+.level-2 {
+ padding-left: 20px;
+}
+
+.level-3 {
+ padding-left: 30px;
+}
diff --git a/aio/src/app/layout/nav-item/nav-item.component.ts b/aio/src/app/layout/nav-item/nav-item.component.ts
new file mode 100644
index 0000000000..3a4c8b001c
--- /dev/null
+++ b/aio/src/app/layout/nav-item/nav-item.component.ts
@@ -0,0 +1,50 @@
+import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
+import { NavigationService, NavigationNode } from 'app/navigation/navigation.service';
+
+@Component({
+ selector: 'aio-nav-item',
+ template: `
+