fix(aio): remove previous files
This commit is contained in:
parent
e6e8123bdd
commit
93c0ab7131
|
@ -1,306 +0,0 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
|
||||
import { ComponentFactoryResolver, ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
|
||||
|
||||
import { Doc, DocMetadata } from '../nav-engine';
|
||||
import { DocViewerComponent } from '../doc-viewer/doc-viewer.component';
|
||||
|
||||
import { embeddedComponents, EmbeddedComponents } from '../embedded';
|
||||
|
||||
|
||||
/// Embedded Test Components ///
|
||||
|
||||
///// FooComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-foo',
|
||||
template: `Foo Component`
|
||||
})
|
||||
class FooComponent { }
|
||||
|
||||
///// BarComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-bar',
|
||||
template: `
|
||||
<hr>
|
||||
<h2>Bar Component</h2>
|
||||
<p #barContent></p>
|
||||
<hr>
|
||||
`
|
||||
})
|
||||
class BarComponent implements OnInit {
|
||||
|
||||
@ViewChild('barContent') barContentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in ngOnInit just like CodeExampleComponent
|
||||
ngOnInit() {
|
||||
// Security: this is a test component; never deployed
|
||||
this.barContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBarContent;
|
||||
}
|
||||
}
|
||||
|
||||
///// BazComponent /////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-baz',
|
||||
template: `
|
||||
<div>++++++++++++++</div>
|
||||
<h2>Baz Component</h2>
|
||||
<p #bazContent></p>
|
||||
<div>++++++++++++++</div>
|
||||
`
|
||||
})
|
||||
class BazComponent implements OnInit {
|
||||
|
||||
@ViewChild('bazContent') bazContentRef: ElementRef;
|
||||
|
||||
constructor(public elementRef: ElementRef) { }
|
||||
|
||||
// Project content in ngOnInit just like CodeExampleComponent
|
||||
ngOnInit() {
|
||||
// Security: this is a test component; never deployed
|
||||
this.bazContentRef.nativeElement.innerHTML = this.elementRef.nativeElement.aioBazContent;
|
||||
}
|
||||
}
|
||||
///// Test Module //////
|
||||
|
||||
const embeddedTestComponents = [FooComponent, BarComponent, BazComponent, ...embeddedComponents];
|
||||
|
||||
@NgModule({
|
||||
entryComponents: embeddedTestComponents
|
||||
})
|
||||
class TestModule { }
|
||||
|
||||
//// Test Component //////
|
||||
|
||||
@Component({
|
||||
selector: 'aio-test',
|
||||
template: `
|
||||
<aio-doc-viewer>Test Component</aio-doc-viewer>
|
||||
`
|
||||
})
|
||||
class TestComponent {
|
||||
private currentDoc: Doc;
|
||||
|
||||
@ViewChild(DocViewerComponent) docViewer: DocViewerComponent;
|
||||
|
||||
setDoc(doc: Doc) {
|
||||
if (this.docViewer) {
|
||||
this.docViewer.doc = doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////// Tests //////////////
|
||||
|
||||
describe('DocViewerComponent', () => {
|
||||
const fakeDocMetadata: DocMetadata = { docId: 'fake', title: 'fake Doc' };
|
||||
let component: TestComponent;
|
||||
let docViewerDE: DebugElement;
|
||||
let docViewerEl: HTMLElement;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ TestModule ],
|
||||
declarations: [
|
||||
TestComponent,
|
||||
DocViewerComponent,
|
||||
embeddedTestComponents
|
||||
],
|
||||
providers: [
|
||||
{provide: EmbeddedComponents, useValue: {components: embeddedTestComponents}}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
docViewerDE = fixture.debugElement.children[0];
|
||||
docViewerEl = docViewerDE.nativeElement;
|
||||
});
|
||||
|
||||
it('should create a DocViewer', () => {
|
||||
expect(component.docViewer).toBeTruthy();
|
||||
});
|
||||
|
||||
it(('should display nothing when set DocViewer.doc to doc w/o content'), () => {
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content: '' };
|
||||
expect(docViewerEl.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it(('should display simple static content doc'), () => {
|
||||
const content = '<p>Howdy, doc viewer</p>';
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
expect(docViewerEl.innerHTML).toEqual(content);
|
||||
});
|
||||
|
||||
it(('should display nothing after reset static content doc'), () => {
|
||||
const content = '<p>Howdy, doc viewer</p>';
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
fixture.detectChanges();
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content: '' };
|
||||
expect(docViewerEl.innerHTML).toEqual('');
|
||||
});
|
||||
|
||||
it(('should apply FooComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Foo</p>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Below Foo</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
||||
expect(fooHtml).toContain('Foo Component');
|
||||
});
|
||||
|
||||
it(('should apply multiple FooComponents'), () => {
|
||||
const content = `
|
||||
<p>Above Foo</p>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<div style="margin-left: 2em;">
|
||||
Holds a
|
||||
<aio-foo>Ignored text</aio-foo>
|
||||
</div>
|
||||
<p>Below Foo</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||
expect(foos.length).toBe(2);
|
||||
});
|
||||
|
||||
it(('should apply BarComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Bar</p>
|
||||
<aio-bar></aio-bar>
|
||||
<p>Below Bar</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||
expect(barHtml).toContain('Bar Component');
|
||||
});
|
||||
|
||||
it(('should project bar content into BarComponent'), () => {
|
||||
const content = `
|
||||
<p>Above Bar</p>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
<p>Below Bar</p>
|
||||
`;
|
||||
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||
|
||||
// necessary to trigger projection within ngOnInit
|
||||
fixture.detectChanges();
|
||||
|
||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||
expect(barHtml).toContain('###bar content###');
|
||||
});
|
||||
|
||||
|
||||
it(('should include Foo and Bar'), () => {
|
||||
const content = `
|
||||
<p>Top</p>
|
||||
<p><aio-foo>ignored</aio-foo></p>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
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 = `
|
||||
<p>Top</p>
|
||||
<div>
|
||||
<aio-foo>
|
||||
<aio-bar>###bar content###</aio-bar>
|
||||
</aio-foo>
|
||||
</div>
|
||||
<p><aio-foo></aio-foo><p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
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 = `
|
||||
<p>Top</p>
|
||||
<aio-bar>
|
||||
<div style="margin-left: 2em">
|
||||
Inner <aio-foo></aio-foo>
|
||||
</div>
|
||||
</aio-bar>
|
||||
<p><aio-foo></aio-foo></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
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 <aio-baz> 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 = `
|
||||
<p>Top</p>
|
||||
<aio-bar>
|
||||
<div style="margin-left: 2em">
|
||||
Inner <aio-baz>---baz stuff---</aio-baz>
|
||||
</div>
|
||||
</aio-bar>
|
||||
<p><aio-baz>---More baz--</aio-baz></p>
|
||||
<p>Bottom</p>
|
||||
`;
|
||||
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');
|
||||
|
||||
});
|
||||
});
|
|
@ -1,139 +0,0 @@
|
|||
import {
|
||||
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
|
||||
DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
||||
import { Doc, DocMetadata, DocMetadataService, NavNode } from '../nav-engine';
|
||||
import { EmbeddedComponents } from '../embedded';
|
||||
|
||||
interface EmbeddedComponentFactory {
|
||||
contentPropertyName: string;
|
||||
factory: ComponentFactory<any>;
|
||||
}
|
||||
|
||||
// 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: '',
|
||||
providers: [ DocMetadataService ],
|
||||
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<string, EmbeddedComponentFactory> = new Map();
|
||||
private hostElement: HTMLElement;
|
||||
|
||||
constructor(
|
||||
componentFactoryResolver: ComponentFactoryResolver,
|
||||
elementRef: ElementRef,
|
||||
embeddedComponents: EmbeddedComponents,
|
||||
private docMetadataService: DocMetadataService,
|
||||
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: Doc) {
|
||||
this.ngOnDestroy();
|
||||
if (newDoc) {
|
||||
this.docMetadataService.metadata = newDoc.metadata;
|
||||
window.scrollTo(0, 0);
|
||||
this.build(newDoc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add doc content to host element and build it out with embedded components
|
||||
*/
|
||||
private build(doc: Doc) {
|
||||
|
||||
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.content || '';
|
||||
|
||||
if (!doc.content) { 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 {
|
||||
|
||||
metadata: DocMetadata;
|
||||
|
||||
private embeddedComponents: ComponentRef<any>[] = [];
|
||||
|
||||
constructor(doc: Doc) {
|
||||
// ignore doc.content ... don't need to keep it around
|
||||
this.metadata = doc.metadata;
|
||||
}
|
||||
|
||||
addEmbeddedComponent(component: ComponentRef<any>) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
import { DocFetchingService } from './doc-fetching.service';
|
||||
// Write tests when/if this service is retained.
|
|
@ -1,80 +0,0 @@
|
|||
import { Http, Response } from '@angular/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { Doc, DocMetadata } from './doc.model';
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocFetchingService {
|
||||
|
||||
private base = 'content/';
|
||||
|
||||
constructor(
|
||||
private http: Http,
|
||||
private logger: Logger) { }
|
||||
|
||||
getPath(docId: string) {
|
||||
return this.base + addPathExtension(docId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch document from server.
|
||||
* NB: pass 404 response to caller as Doc with empty string content
|
||||
* Other errors and non-OK status responses are thrown errors.
|
||||
* TODO: add timeout and retry for lost connection
|
||||
*/
|
||||
getDocFile(docId: string): Observable<Doc> {
|
||||
|
||||
if (!docId) {
|
||||
const emsg = 'getFile: no document id';
|
||||
this.logger.error(emsg);
|
||||
throw new Error(emsg);
|
||||
}
|
||||
|
||||
// TODO: Metadata will be updated from file
|
||||
const metadata: DocMetadata = { docId, title: docId };
|
||||
const url = this.getPath(docId);
|
||||
|
||||
this.logger.log(`Fetching document file at '${url}'`);
|
||||
|
||||
return this.http.get(url)
|
||||
.map(res => <Doc> { metadata, content: res.text() }) // TODO: It will come as JSON soon
|
||||
.do(content => this.logger.log(`Fetched document file at '${url}'`) )
|
||||
.catch((error: Response) => {
|
||||
if (error.status === 404) {
|
||||
this.logger.error(`Document file not found at '${url}'`);
|
||||
return of({metadata, content: ''} as Doc);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addPathExtension(path: string) {
|
||||
if (path) {
|
||||
if (path.endsWith('/')) {
|
||||
return path + 'index.html';
|
||||
} else if (!path.endsWith('.html')) {
|
||||
return path + '.html';
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// function removePathExtension(path: string) {
|
||||
// if (path) {
|
||||
// if (path.endsWith('/index.html')) {
|
||||
// return path.substring(0, path.length - 10);
|
||||
// } else if (path.endsWith('.html')) {
|
||||
// return path.substring(0, path.length - 5);
|
||||
// }
|
||||
// }
|
||||
// return path;
|
||||
// }
|
|
@ -1,7 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { DocMetadata } from './doc.model';
|
||||
|
||||
@Injectable()
|
||||
export class DocMetadataService {
|
||||
metadata: DocMetadata;
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
export interface DocMetadata {
|
||||
docId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Doc {
|
||||
metadata: DocMetadata;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI navigation node that describes a document or container of documents
|
||||
* Each node corresponds to a link in the UI.
|
||||
*/
|
||||
export interface NavNode {
|
||||
/** unique integer id for this node */
|
||||
id: number;
|
||||
/** Document id if this is a document node */
|
||||
docId: string;
|
||||
/** Document path (calculated from docId) if this is a document node */
|
||||
docPath: string;
|
||||
/** url to an external web page; docPath and url are mutually exclusive. */
|
||||
url?: string;
|
||||
/** Title to display in the navigation link; typically shorter */
|
||||
navTitle: string;
|
||||
/** Tooltip for links */
|
||||
tooltip: string;
|
||||
/** Ids of ancestor nodes higher in the hierarchy */
|
||||
ancestorIds: number[];
|
||||
/** true if should not be displayed in UI. Can still be loaded directly */
|
||||
// hide?: boolean; NO NO. If the JSON says, hide, simply omit it from this map
|
||||
/** If defined, this node is a container of child nodes */
|
||||
children?: NavNode[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigation for the site.
|
||||
* - nodes: the top-level navigation nodes; node can have children.
|
||||
* - docs: find node for a given document id.
|
||||
*/
|
||||
export interface NavMap {
|
||||
/**
|
||||
* Map that drives the UI navigation.
|
||||
* Each node correspond to a navigation link in the UI.
|
||||
* Supports unlimited node nesting.
|
||||
*/
|
||||
nodes: NavNode[];
|
||||
|
||||
/**
|
||||
* NavNode for a document id in the NavMap.
|
||||
*/
|
||||
docs: Map<string, NavNode>;
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
|
||||
import { DocService } from './doc.service';
|
||||
import { Doc, DocMetadata, NavNode } from './doc.model';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/delay';
|
||||
import 'rxjs/add/operator/first';
|
||||
|
||||
describe('DocService', () => {
|
||||
let docFetchingService: DocFetchingService;
|
||||
let getFileSpy: jasmine.Spy;
|
||||
let loggerSpy: any;
|
||||
let docService: DocService;
|
||||
let testDoc: Doc;
|
||||
let testDocId: string;
|
||||
let testContent: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDocId = 'fake';
|
||||
testContent = 'fake file contents';
|
||||
testDoc = {
|
||||
metadata: {docId: testDocId, title: 'Fake Title'} as DocMetadata,
|
||||
content: testContent
|
||||
};
|
||||
|
||||
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
||||
docFetchingService = new DocFetchingService(null, loggerSpy);
|
||||
getFileSpy = spyOn(docFetchingService, 'getDocFile').and
|
||||
.returnValue(of(testDoc).delay(0).first()); // first() -> completes
|
||||
|
||||
docService = new DocService(docFetchingService, loggerSpy);
|
||||
});
|
||||
|
||||
it('should return fake doc for fake id', fakeAsync(() => {
|
||||
docService.getDoc(testDocId).subscribe(doc =>
|
||||
expect(doc.content).toBe(testContent)
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should retrieve file once for first file request', fakeAsync(() => {
|
||||
let doc: Doc;
|
||||
expect(getFileSpy.calls.count()).toBe(0, 'no call before tick');
|
||||
docService.getDoc(testDocId).subscribe(d => doc = d);
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after tick');
|
||||
expect(doc).toBe(testDoc, 'expected doc');
|
||||
}));
|
||||
|
||||
it('should retrieve file from cache the second time', fakeAsync(() => {
|
||||
docService.getDoc(testDocId).subscribe(doc =>
|
||||
expect(doc).toBe(testDoc, 'expected doc from server')
|
||||
);
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after 1st request');
|
||||
|
||||
docService.getDoc(testDocId).subscribe(doc =>
|
||||
expect(doc).toBe(testDoc, 'expected doc from cache')
|
||||
);
|
||||
tick();
|
||||
expect(getFileSpy.calls.count()).toBe(1, 'still only one call after 2nd request');
|
||||
}));
|
||||
|
||||
it('should pass along file error through its getDoc observable result', fakeAsync(() => {
|
||||
|
||||
const err = 'deliberate file error';
|
||||
getFileSpy.and.returnValue(
|
||||
// simulate async error in the file retrieval
|
||||
of('').delay(0).map(() => { throw new Error(err); })
|
||||
);
|
||||
|
||||
docService.getDoc(testDocId).subscribe(
|
||||
doc => expect(false).toBe(true, 'should have failed'),
|
||||
error => expect(error.message).toBe(err)
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
|
||||
import { Doc, NavNode } from './doc.model';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
import { NavMapService } from './nav-map.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocService {
|
||||
private cache = new Map<string, Doc>();
|
||||
private notFoundContent: string;
|
||||
|
||||
constructor(
|
||||
private fileService: DocFetchingService,
|
||||
private logger: Logger
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Get document for id, from cache if found else server.
|
||||
* Pass server errors along to caller
|
||||
* Constructs and caches a "Not Found" doc when fileservice returns a doc with no content.
|
||||
*/
|
||||
getDoc(docId: string): Observable<Doc> {
|
||||
const cached = this.cache.get(docId);
|
||||
if (cached) {
|
||||
this.logger.log(`Returned cached document for '${docId}'`);
|
||||
return of(cached);
|
||||
}
|
||||
|
||||
return this.fileService.getDocFile(docId)
|
||||
.switchMap(doc => {
|
||||
this.logger.log(`Fetched document for '${docId}'`);
|
||||
return doc.content ? of(doc) :
|
||||
this.getNotFound()
|
||||
.map(nfContent => <Doc> {metadata: {docId, title: docId}, content: nfContent});
|
||||
})
|
||||
.do(doc => this.cache.set(docId, doc));
|
||||
}
|
||||
|
||||
getNotFound(): Observable<string> {
|
||||
if (this.notFoundContent) { return of(this.notFoundContent); }
|
||||
const nfDocId = 'not-found';
|
||||
return this.fileService.getDocFile(nfDocId)
|
||||
.map(doc => {
|
||||
this.logger.log(`Fetched "not found" document for '${nfDocId}'`);
|
||||
this.notFoundContent = doc.content;
|
||||
return doc.content;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { DocService } from './doc.service';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
import { NavLinkDirective } from './nav-link.directive';
|
||||
import { NavMapService } from './nav-map.service';
|
||||
|
||||
export { Doc, DocMetadata, NavNode, NavMap } from './doc.model';
|
||||
export { DocMetadataService } from './doc-metadata.service';
|
||||
export { NavEngine } from './nav-engine.service';
|
||||
export { NavMapService } from './nav-map.service';
|
||||
|
||||
export const navDirectives = [
|
||||
NavLinkDirective
|
||||
];
|
||||
|
||||
export const navProviders = [
|
||||
DocService,
|
||||
DocFetchingService,
|
||||
NavEngine,
|
||||
NavMapService,
|
||||
];
|
|
@ -1,39 +0,0 @@
|
|||
import { fakeAsync, tick} from '@angular/core/testing';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/delay';
|
||||
|
||||
import { DocService } from './doc.service';
|
||||
import { Doc, DocMetadata, NavNode } from './doc.model';
|
||||
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
|
||||
describe('NavEngine', () => {
|
||||
|
||||
let fakeDoc: Doc;
|
||||
let navEngine: NavEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeDoc = {
|
||||
metadata: {
|
||||
docId: 'fake',
|
||||
title: 'All about the fake'
|
||||
},
|
||||
content: 'fake content'
|
||||
};
|
||||
|
||||
const docService: any = jasmine.createSpyObj('docService', ['getDoc']);
|
||||
docService.getDoc.and.returnValue(of(fakeDoc).delay(0));
|
||||
|
||||
navEngine = new NavEngine(docService);
|
||||
});
|
||||
|
||||
it('should set currentDoc to fake doc when navigate to fake id', fakeAsync(() => {
|
||||
navEngine.navigate('fake');
|
||||
navEngine.currentDoc.subscribe(doc =>
|
||||
expect(doc.content).toBe(fakeDoc.content)
|
||||
);
|
||||
tick();
|
||||
}));
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { Doc } from './doc.model';
|
||||
import { DocService } from './doc.service';
|
||||
|
||||
@Injectable()
|
||||
export class NavEngine implements OnDestroy {
|
||||
|
||||
private docSubject = new ReplaySubject<Doc>(1);
|
||||
private subscription: Subscription;
|
||||
|
||||
/** Observable of the most recent document from a `navigate` call */
|
||||
currentDoc = this.docSubject.asObservable();
|
||||
|
||||
constructor(private docService: DocService) {}
|
||||
|
||||
/**
|
||||
* Navigate pushes new doc for the given `id` into the `currentDoc` observable.
|
||||
* TODO: handle document retrieval error
|
||||
*/
|
||||
navigate(docId: string) {
|
||||
this.ngOnDestroy();
|
||||
this.subscription = this.docService.getDoc(docId).subscribe(doc => this.docSubject.next(doc));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.subscription) { this.subscription.unsubscribe(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { Directive, HostListener, Input } from '@angular/core';
|
||||
import { NavEngine } from './nav-engine.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[aioNavLink]'
|
||||
})
|
||||
export class NavLinkDirective {
|
||||
|
||||
@Input()
|
||||
aioNavLink: string;
|
||||
|
||||
constructor(private navEngine: NavEngine) { }
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
onClick($event) {
|
||||
this.navEngine.navigate(this.aioNavLink);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { Http, Response } from '@angular/http';
|
||||
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/operator/delay';
|
||||
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { NavNode, NavMap } from './doc.model';
|
||||
import { NavMapService } from './nav-map.service';
|
||||
|
||||
import { getTestNavMapResponse } from '../../testing/nav-map-json-response';
|
||||
|
||||
describe('NavMapService', () => {
|
||||
let httpSpy: any;
|
||||
let loggerSpy: any;
|
||||
let navMapService: NavMapService;
|
||||
let navMap: NavMap;
|
||||
|
||||
beforeEach(done => {
|
||||
httpSpy = jasmine.createSpyObj('http', ['get']);
|
||||
httpSpy.get.and.returnValue(of(getTestNavMapResponse()).delay(0).first()); // first() -> completes
|
||||
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
||||
|
||||
navMapService = new NavMapService(new DocFetchingService(null, null), httpSpy, loggerSpy);
|
||||
|
||||
navMapService.navMap.first().subscribe(
|
||||
nm => navMap = nm,
|
||||
null,
|
||||
done);
|
||||
});
|
||||
|
||||
it('should return a navMap', () => {
|
||||
expect(navMap).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have filtered away the "cli-quickstart" because `hide`===true', () => {
|
||||
const item = navMap.nodes.find(n => n.docId === 'guide/cli-quickstart');
|
||||
expect(item).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('Quickstart', () => {
|
||||
let item: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
item = navMap.nodes.find(n => n.navTitle === 'Quickstart');
|
||||
});
|
||||
|
||||
it('should have expected item', () => {
|
||||
expect(item).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have expected docId', () => {
|
||||
expect(item.docId).toBe('guide/quickstart');
|
||||
});
|
||||
|
||||
it('should have calculated expected docPath', () => {
|
||||
expect(item.docPath).toBe('content/guide/quickstart.html');
|
||||
});
|
||||
|
||||
it('should have no ancestors because it is a top-level item', () => {
|
||||
expect(item.ancestorIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Getting Started', () => {
|
||||
let section: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
section = navMap.nodes.find(n => n.navTitle === 'Getting started');
|
||||
});
|
||||
|
||||
it('should have an id', () => {
|
||||
expect(section.id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have distinct tooltip', () => {
|
||||
expect(section.tooltip).not.toBe(section.navTitle);
|
||||
});
|
||||
|
||||
it('should have 2 children', () => {
|
||||
expect(section.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should have itself as ancestor because it has children', () => {
|
||||
expect(section.ancestorIds).toEqual([section.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tutorial', () => {
|
||||
let section: NavNode;
|
||||
let intro: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
section = navMap.nodes.find(n => n.navTitle === 'Tutorial');
|
||||
if (section && section.children) {
|
||||
intro = section.children.find(n => n.navTitle === 'Introduction');
|
||||
}
|
||||
});
|
||||
|
||||
it('should have 2 children', () => {
|
||||
expect(section.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it('intro child\'s docId ends in "/"', () => {
|
||||
expect(intro.docId[intro.docId.length - 1]).toEqual('/');
|
||||
});
|
||||
|
||||
it('intro child\'s calculated docPath ends in "index.html"', () => {
|
||||
expect(intro.docPath).toMatch(/index.html$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Core (3-level)', () => {
|
||||
let section: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
section = navMap.nodes.find(n => n.navTitle === 'Core');
|
||||
});
|
||||
|
||||
it('should have 2 children', () => {
|
||||
expect(section.children.length).toBe(2);
|
||||
});
|
||||
|
||||
describe('->directives', () => {
|
||||
let directives: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
directives = section.children.find(n => n.navTitle === 'Directives');
|
||||
});
|
||||
|
||||
it('should have a heading docId', () => {
|
||||
expect(directives.docId).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have calculated expected docPath', () => {
|
||||
expect(directives.docPath).toBe('content/guide/directives.html');
|
||||
});
|
||||
|
||||
it('should have 2 children', () => {
|
||||
expect(directives.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it('children should have two ancestor ids in lineal order', () => {
|
||||
const expectedAncestors = [section.id, directives.id];
|
||||
expect(directives.children[0].ancestorIds).toEqual(expectedAncestors, '#1');
|
||||
expect(directives.children[1].ancestorIds).toEqual(expectedAncestors, '#2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty Heading', () => {
|
||||
let section: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
section = navMap.nodes.find(n => n.navTitle === 'Empty Heading');
|
||||
});
|
||||
|
||||
it('should have no children', () => {
|
||||
expect(section.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should have itself as ancestor because it has a `children` array', () => {
|
||||
expect(section.ancestorIds).toEqual([section.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('External', () => {
|
||||
let section: NavNode;
|
||||
let gitter: NavNode;
|
||||
|
||||
beforeEach(() => {
|
||||
section = navMap.nodes.find(n => n.navTitle === 'External');
|
||||
if (section && section.children) {
|
||||
gitter = section.children[0];
|
||||
}
|
||||
});
|
||||
|
||||
it('should have one child (gitter)', () => {
|
||||
expect(section.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it('child should have a url', () => {
|
||||
expect(gitter.url).toBeTruthy();
|
||||
});
|
||||
|
||||
it('child should not have a docId', () => {
|
||||
expect(gitter.docId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('child should not have a docPath', () => {
|
||||
expect(gitter.docPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it('child should have parent as only ancestor id', () => {
|
||||
expect(gitter.ancestorIds).toEqual([section.id]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Http } from '@angular/http';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
|
||||
import { Doc, NavNode, NavMap } from './doc.model';
|
||||
import { DocFetchingService } from './doc-fetching.service';
|
||||
import { Logger } from '../logger.service';
|
||||
|
||||
const navMapUrl = 'content/navmap.json';
|
||||
|
||||
@Injectable()
|
||||
export class NavMapService {
|
||||
|
||||
private getDocPath: (string) => string;
|
||||
private navMapSubject: ReplaySubject<NavMap>;
|
||||
private nextNodeId = 1;
|
||||
|
||||
constructor(
|
||||
docFetchingService: DocFetchingService,
|
||||
private http: Http,
|
||||
private logger: Logger) {
|
||||
this.getDocPath = docFetchingService.getPath.bind(docFetchingService);
|
||||
}
|
||||
|
||||
get navMap(): Observable<NavMap> {
|
||||
return (this.navMapSubject ? this.navMapSubject : this.createNavMapSubject()).asObservable() ;
|
||||
}
|
||||
|
||||
private createNavMapSubject(): ReplaySubject<NavMap> {
|
||||
this.navMapSubject = new ReplaySubject<NavMap>(1);
|
||||
|
||||
this.http.get(navMapUrl)
|
||||
.map(res => res.json().nodes)
|
||||
.do(() => this.logger.log(`Fetched navigation map JSON at '${navMapUrl}'`))
|
||||
.subscribe(
|
||||
navNodes => this.navMapSubject.next(this.createNavMap(navNodes))
|
||||
);
|
||||
|
||||
return this.navMapSubject;
|
||||
}
|
||||
|
||||
|
||||
////// private helper functions ////
|
||||
|
||||
private createNavMap(nodes: NavNode[]) {
|
||||
nodes = this.removeHidden(nodes);
|
||||
const navMap: NavMap = { nodes, docs: new Map<string, NavNode>()};
|
||||
nodes.forEach(node => this.adjustNode(node, navMap, []));
|
||||
return navMap;
|
||||
}
|
||||
|
||||
// Adjust properties of a node from JSON and build navMap.docs
|
||||
private adjustNode(node: NavNode, navMap: NavMap, ancestorIds: number[] ) {
|
||||
node.id = this.nextNodeId++;
|
||||
node.ancestorIds = ancestorIds;
|
||||
if ( node.tooltip === undefined ) { node.tooltip = node.navTitle; }
|
||||
|
||||
if (node.docId) {
|
||||
// This node is associated with a document
|
||||
node.docId = node.docId.toLocaleLowerCase();
|
||||
node.docPath = this.getDocPath(node.docId);
|
||||
navMap.docs.set(node.docId, node);
|
||||
}
|
||||
|
||||
|
||||
if (node.children) {
|
||||
// Ancestors include self when this node has children
|
||||
node.ancestorIds = ancestorIds.concat(node.id);
|
||||
node.children.forEach(n => this.adjustNode(n, navMap, node.ancestorIds));
|
||||
}
|
||||
}
|
||||
|
||||
private removeHidden(nodes: NavNode[]) {
|
||||
return nodes.filter(node => {
|
||||
if (node['hide'] === true ) { return false; }
|
||||
if (node.children) {
|
||||
node.children = this.removeHidden(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.fill-remaining-space {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
margin-right: 10px;
|
||||
margin-left: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.nav-link {
|
||||
margin-right: 8px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.nav-link {
|
||||
font-size: 80%;
|
||||
margin-right: 8px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-menu',
|
||||
template: `
|
||||
<span><a class="nav-link" aioNavLink="home">Home</a></span>
|
||||
<!-- <span><a class="nav-link" aioNavLink="api">API</a></span> -->
|
||||
<span><a class="nav-link" aioNavLink="api/common/date-pipe">API</a></span>
|
||||
<span><a class="nav-link" aioNavLink="news">News</a></span>
|
||||
<span><a class="nav-link" aioNavLink="features">Features</a></span>
|
||||
`,
|
||||
styleUrls: ['./menu.component.scss'],
|
||||
animations: []
|
||||
})
|
||||
export class MenuComponent {
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<div *ngIf="isItem">
|
||||
<a href={{href}} [ngClass]="classes" target={{target}} title={{tooltip}}
|
||||
(click)="itemClicked()" class="vertical-menu">
|
||||
{{label}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isItem">
|
||||
<a href={{href}} [ngClass]="classes" target={{target}} title={{tooltip}}
|
||||
(click)="headerClicked()" class="vertical-menu heading">
|
||||
{{label}}
|
||||
<md-icon [class.active]="!isActive">keyboard_arrow_right</md-icon>
|
||||
<md-icon [class.active]="isActive">keyboard_arrow_down</md-icon>
|
||||
</a>
|
||||
<div class="heading-children" [ngClass]="classes">
|
||||
<aio-navitem *ngFor="let node of node.children" [level]="level + 1"
|
||||
[node]="node" [selectedNode]="selectedNode"></aio-navitem>
|
||||
</div>
|
||||
</div>
|
|
@ -1,114 +0,0 @@
|
|||
|
||||
/************************************
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import { Component, EventEmitter, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { Doc, NavNode } from '../nav-engine';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-navitem',
|
||||
templateUrl: 'nav-item.component.html',
|
||||
styleUrls: ['nav-item.component.scss']
|
||||
})
|
||||
export class NavItemComponent implements OnInit, OnDestroy {
|
||||
@Input() selectedNode: EventEmitter<NavNode>;
|
||||
@Input() node: NavNode;
|
||||
@Input() level = 1;
|
||||
|
||||
isActive = false;
|
||||
isSelected = false;
|
||||
isItem = false;
|
||||
classes: {[index: string]: boolean };
|
||||
href = '';
|
||||
label = '';
|
||||
selectedNodeSubscription: Subscription;
|
||||
target = '';
|
||||
tooltip = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.label = this.node.navTitle;
|
||||
this.tooltip = this.node.tooltip;
|
||||
this.isItem = this.node.children == null;
|
||||
this.href = this.node.url || this.node.docPath ;
|
||||
this.target = this.node.url ? '_blank' : '_self';
|
||||
this.setClasses();
|
||||
|
||||
if (this.selectedNode) {
|
||||
this.selectedNodeSubscription = this.selectedNode.subscribe((n: NavNode) => {
|
||||
this.isSelected = n &&
|
||||
( n === this.node ||
|
||||
(n.ancestorIds && n.ancestorIds.indexOf(this.node.id) > -1)
|
||||
);
|
||||
// this.isActive = this.isSelected; // disabled per meeting Feb 13
|
||||
this.setClasses();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.selectedNodeSubscription) {
|
||||
this.selectedNodeSubscription.unsubscribe();
|
||||
this.selectedNodeSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
setClasses() {
|
||||
this.classes = {
|
||||
['level-' + this.level]: true,
|
||||
active: this.isActive,
|
||||
selected: this.isSelected
|
||||
};
|
||||
}
|
||||
|
||||
itemClicked() {
|
||||
this.isActive = true;
|
||||
this.isSelected = !!this.node.docId;
|
||||
this.setClasses();
|
||||
if (this.isSelected) {
|
||||
this.selectedNode.emit(this.node);
|
||||
return false;
|
||||
}
|
||||
return !!this.node.url; // let browser handle the external page request.
|
||||
}
|
||||
|
||||
headerClicked() {
|
||||
this.isActive = !this.isActive;
|
||||
if (this.isActive && this.node.docId) {
|
||||
this.isSelected = true;
|
||||
if (this.selectedNode) {
|
||||
this.selectedNode.emit(this.node);
|
||||
}
|
||||
}
|
||||
this.setClasses();
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<md-sidenav-container class="sidenav-container" (window:resize)="onResize($event.target.innerWidth)">
|
||||
<md-sidenav #sidenav class="sidenav" [opened]="isSideBySide">
|
||||
<md-toolbar color="primary" class="app-toolbar" [class.active]="!isSideBySide">
|
||||
<aio-menu class="small"></aio-menu>
|
||||
<span class="fill-remaining-space"></span>
|
||||
</md-toolbar>
|
||||
|
||||
<aio-navitem *ngFor="let node of nodes | async" [node]="node" [selectedNode]="selectedNode"></aio-navitem>
|
||||
</md-sidenav>
|
||||
<section class="sidenav-content">
|
||||
<aio-doc-viewer [doc]="currentDoc | async"></aio-doc-viewer>
|
||||
</section>
|
||||
</md-sidenav-container>
|
|
@ -1,27 +0,0 @@
|
|||
.sidenav-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidenav-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.sidenav-content button {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
md-toolbar {
|
||||
display: none;
|
||||
padding-left: 10px !important;
|
||||
}
|
||||
md-toolbar.active {
|
||||
display: block;
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
/* tslint:disable:no-unused-variable */
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement, EventEmitter, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import 'rxjs/add/operator/delay';
|
||||
|
||||
import { Doc, DocMetadata, NavEngine, NavMapService, NavMap, NavNode } from '../nav-engine';
|
||||
import { SidenavComponent } from './sidenav.component';
|
||||
|
||||
//// Test Components ///
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'md-sidenav',
|
||||
template: ''
|
||||
})
|
||||
class FakeMdSideNavComponent {
|
||||
_isOpen = false;
|
||||
@Input() opened: boolean;
|
||||
@Input() mode: 'side' | 'over';
|
||||
toggle = jasmine.createSpy('toggle');
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aio-doc-viewer',
|
||||
template: ''
|
||||
})
|
||||
class FakeDocViewerComponent {
|
||||
@Input() doc: Doc;
|
||||
}
|
||||
|
||||
//// Tests /////
|
||||
describe('SidenavComponent', () => {
|
||||
let component: SidenavComponent;
|
||||
let fixture: ComponentFixture<SidenavComponent>;
|
||||
|
||||
let fakeDoc: Doc;
|
||||
let fakeNode: NavNode;
|
||||
let fakeNavMap: NavMap;
|
||||
let navEngine: NavEngine;
|
||||
let navMapService: NavMapService;
|
||||
let navigateSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(async(() => {
|
||||
fakeDoc = {
|
||||
metadata: {docId: 'fake'} as DocMetadata,
|
||||
content: 'Fake content'
|
||||
};
|
||||
|
||||
navEngine = {
|
||||
currentDoc: of(fakeDoc).delay(0).first(),
|
||||
navigate: (docId: string) => { }
|
||||
} as NavEngine;
|
||||
navigateSpy = spyOn(navEngine, 'navigate');
|
||||
|
||||
fakeNode = {
|
||||
id: 42,
|
||||
docId: fakeDoc.metadata.docId,
|
||||
navTitle: 'Fakery',
|
||||
docPath: 'content/fake.hmlt'
|
||||
} as NavNode;
|
||||
|
||||
fakeNavMap = {
|
||||
nodes: [fakeNode],
|
||||
docs: new Map<string, NavNode>([[fakeNode.docId, fakeNode]])
|
||||
};
|
||||
|
||||
navMapService = {
|
||||
navMap: of(fakeNavMap).delay(0).first()
|
||||
} as NavMapService;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
SidenavComponent,
|
||||
FakeMdSideNavComponent,
|
||||
FakeDocViewerComponent
|
||||
],
|
||||
providers: [
|
||||
{provide: NavEngine, useValue: navEngine },
|
||||
{provide: NavMapService, useValue: navMapService }
|
||||
],
|
||||
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SidenavComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('#currentDoc', () => {
|
||||
it('should have "currentDoc" after a tick', fakeAsync(() => {
|
||||
component.currentDoc.subscribe(doc => {
|
||||
expect(doc).toBe(fakeDoc);
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
it('should set "currentDocId" as side effect', fakeAsync(() => {
|
||||
component.currentDoc.subscribe(doc => {
|
||||
expect(component.currentDocId).toEqual(fakeDoc.metadata.docId);
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#nodes', () => {
|
||||
it('should have "nodes" after a tick', fakeAsync(() => {
|
||||
component.nodes.subscribe(nodes => {
|
||||
expect(nodes).toEqual(fakeNavMap.nodes);
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('#selectedNode', () => {
|
||||
// Simulate when user clicks a left nav link in a `NavItemComponent`
|
||||
// which calls `emit` on the selectedNode navigates
|
||||
// all of this synchronously
|
||||
it('should call navigate after emitting a node', () => {
|
||||
expect(navigateSpy.calls.count()).toBe(0, 'before emit');
|
||||
component.selectedNode.emit(fakeNode);
|
||||
expect(navigateSpy.calls.count()).toBe(1, 'after emit');
|
||||
});
|
||||
|
||||
it('should raise event when currentDoc changes', done => {
|
||||
component.selectedNode.subscribe((node: NavNode) => {
|
||||
expect(node.docId).toBe(fakeDoc.metadata.docId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onResize', () => {
|
||||
it('should go into side-by-side when width > 600', () => {
|
||||
component.onResize(601);
|
||||
expect(component.isSideBySide).toBe(true);
|
||||
});
|
||||
|
||||
it('should emit overlay mode when width > 600', () => {
|
||||
component.isOverlayMode.subscribe(isOverlay =>
|
||||
expect(isOverlay).toBe(false)
|
||||
);
|
||||
component.onResize(601);
|
||||
});
|
||||
it('should go into side-by-side when width == 600', () => {
|
||||
component.onResize(600);
|
||||
expect(component.isSideBySide).toBe(false);
|
||||
});
|
||||
|
||||
it('should emit overlay mode when width == 600', () => {
|
||||
component.isOverlayMode.subscribe(isOverlay =>
|
||||
expect(isOverlay).toBe(true)
|
||||
);
|
||||
component.onResize(600);
|
||||
});
|
||||
});
|
||||
|
||||
describe('-> MdSideNav', () => {
|
||||
|
||||
let mdSideNavComponent: FakeMdSideNavComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
mdSideNavComponent = fixture.debugElement
|
||||
.query(By.directive(FakeMdSideNavComponent))
|
||||
.componentInstance as FakeMdSideNavComponent;
|
||||
});
|
||||
|
||||
it('toggle should call through to MdSideNav toggle', () => {
|
||||
const calls = mdSideNavComponent.toggle.calls;
|
||||
expect(calls.count()).toBe(0, 'before toggle');
|
||||
component.toggle();
|
||||
expect(calls.count()).toBe(1, 'after toggle');
|
||||
});
|
||||
|
||||
it('should be opened when width > 600', () => {
|
||||
component.onResize(601);
|
||||
fixture.detectChanges();
|
||||
expect(mdSideNavComponent.opened).toBe(true);
|
||||
});
|
||||
|
||||
it('should be not open when width == 600', () => {
|
||||
component.onResize(600);
|
||||
fixture.detectChanges();
|
||||
expect(mdSideNavComponent.opened).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('-> DocViewer', () => {
|
||||
let docViewer: FakeDocViewerComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
docViewer = fixture.debugElement
|
||||
.query(By.directive(FakeDocViewerComponent))
|
||||
.componentInstance as FakeDocViewerComponent;
|
||||
});
|
||||
|
||||
it('should not have a document at the start', () => {
|
||||
expect(docViewer.doc).toBeNull();
|
||||
});
|
||||
|
||||
// Doesn't work with fakeAsync and async complains about the delay timer (setInterval);
|
||||
// it('should have a document after NavEngine has a current doc', (fakeAsync(() => {
|
||||
// tick();
|
||||
// fixture.detectChanges();
|
||||
// expect(docViewer.doc).toBe(fakeDoc);
|
||||
// })));
|
||||
|
||||
// Must go old school with setTimeout and `done`
|
||||
it('should have a document after NavEngine has a current doc', (done => {
|
||||
setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
expect(docViewer.doc).toBe(fakeDoc);
|
||||
done();
|
||||
}, 1);
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
import { Component, EventEmitter, Output, OnInit, OnChanges, ViewChild } from '@angular/core';
|
||||
import { MdSidenav } from '@angular/material/sidenav';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/take';
|
||||
|
||||
import { Doc, NavEngine, NavMap, NavMapService, NavNode } from '../nav-engine';
|
||||
|
||||
@Component({
|
||||
selector: 'aio-sidenav',
|
||||
templateUrl: './sidenav.component.html',
|
||||
styleUrls: ['./sidenav.component.scss'],
|
||||
animations: []
|
||||
})
|
||||
export class SidenavComponent implements OnInit {
|
||||
|
||||
@Output() isOverlayMode = new EventEmitter<boolean>();
|
||||
@ViewChild('sidenav') private sidenav: MdSidenav;
|
||||
|
||||
currentDoc: Observable<Doc>;
|
||||
currentDocId: string;
|
||||
isSideBySide = false;
|
||||
nodes: Observable<NavNode[]>;
|
||||
selectedNode = new EventEmitter<NavNode>();
|
||||
sideBySideWidth = 600;
|
||||
windowWidth = 0;
|
||||
|
||||
constructor(
|
||||
private navEngine: NavEngine,
|
||||
private navMapService: NavMapService ) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.onResize(window.innerWidth);
|
||||
|
||||
this.nodes = this.navMapService.navMap.map( navMap => navMap.nodes );
|
||||
|
||||
this.currentDoc = this.navEngine.currentDoc
|
||||
.do(doc => {
|
||||
// Side effect: when the current doc changes,
|
||||
// get its NavNode and alert the navigation panel
|
||||
this.currentDocId = doc.metadata.docId;
|
||||
this.navMapService.navMap.first() // take makes sure it completes!
|
||||
.map(navMap => navMap.docs.get(this.currentDocId))
|
||||
.subscribe( node => this.selectedNode.emit(node));
|
||||
});
|
||||
|
||||
this.selectedNode.subscribe((node: NavNode) => {
|
||||
// Navigate when the user selects a doc other than the current doc
|
||||
const docId = node && node.docId;
|
||||
if (docId && docId !== this.currentDocId) {
|
||||
this.navEngine.navigate(docId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onResize(width) {
|
||||
this.windowWidth = width;
|
||||
this.isSideBySide = width > this.sideBySideWidth;
|
||||
this.sidenav.mode = this.isSideBySide ? 'side' : 'over';
|
||||
this.isOverlayMode.emit(!this.isSideBySide);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
}
|
|
@ -1,402 +0,0 @@
|
|||
{ "nodes": [
|
||||
{
|
||||
"docId": "guide/quickstart",
|
||||
"navTitle": "Quickstart",
|
||||
"tooltip": "A quick look at an Angular app."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/cli-quickstart",
|
||||
"navTitle": "CLI Quickstart",
|
||||
"tooltip": "A quick look at an Angular app built with the Angular CLI."
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Tutorial",
|
||||
"tooltip": "The Tour of Heroes tutorial takes you through the steps of creating an Angular application in TypeScript.",
|
||||
"children": [
|
||||
{
|
||||
"docId": " tutorial/",
|
||||
"navTitle": "Introduction",
|
||||
"tooltip": "Introduction to the Tour of Heroes tutorial"
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-1",
|
||||
"navTitle": "The hero editor",
|
||||
"tooltip": "Build a simple hero editor"
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-2",
|
||||
"navTitle": "Master/detail",
|
||||
"tooltip": "Build a master/detail page with a list of heroes."
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-3",
|
||||
"navTitle": "Multiple components",
|
||||
"tooltip": "Refactor the master/detail view into separate components."
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-4",
|
||||
"navTitle": "Services",
|
||||
"tooltip": "Create a reusable service to manage hero data."
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-5",
|
||||
"navTitle": "Routing",
|
||||
"tooltip": "Add the Angular router and navigate among the views."
|
||||
},
|
||||
{
|
||||
"docId": "tutorial/toh-6",
|
||||
"navTitle": "HTTP",
|
||||
"tooltip": "Use HTTP to retrieve and save hero data."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Getting started",
|
||||
"tooltip": "A gentle introduction to Angular.",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/docs-overview",
|
||||
"navTitle": "Overview",
|
||||
"tooltip": "How to read and use this documentation."
|
||||
},
|
||||
{
|
||||
"docId": "guide/setup",
|
||||
"navTitle": "Setup",
|
||||
"tooltip": "Install the Angular QuickStart seed for faster, more efficient development on your machine."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/learning-angular",
|
||||
"navTitle": "Learning Angular",
|
||||
"tooltip": "A suggested path through the documentation for Angular newcomers."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/architecture",
|
||||
"navTitle": "Architecture",
|
||||
"tooltip": "The basic building blocks of Angular applications."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/appmodule",
|
||||
"navTitle": "The root AppModule",
|
||||
"tooltip": "Tell Angular how to construct and bootstrap the app in the root \"AppModule\"."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/displaying-data",
|
||||
"navTitle": "Displaying data",
|
||||
"tooltip": "Property binding helps show app data in the UI."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/user-input",
|
||||
"navTitle": "User Input",
|
||||
"tooltip": "User input triggers DOM events. We listen to those events with event bindings that funnel updated values back into our components and models."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/forms",
|
||||
"navTitle": "Forms",
|
||||
"tooltip": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/dependency-injection",
|
||||
"navTitle": "Dependency Injection",
|
||||
"tooltip": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\"."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/template-syntax",
|
||||
"navTitle": "Template Syntax",
|
||||
"tooltip": "Learn how to write templates that display data and consume user events with the help of data binding."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/cheatsheet",
|
||||
"navTitle": "Cheat Sheet",
|
||||
"tooltip": "A quick guide to common Angular coding techniques."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/style-guide",
|
||||
"navTitle": "Style guide",
|
||||
"tooltip": "Write Angular with style."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/glossary",
|
||||
"navTitle": "Glossary",
|
||||
"tooltip": "Brief definitions of the most important words in the Angular vocabulary."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/change-log",
|
||||
"navTitle": "Change Log",
|
||||
"tooltip": "An annotated history of recent documentation improvements."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"navTitle": "Core",
|
||||
"tooltip": "Learn the core capabilities of Angular",
|
||||
"children": [
|
||||
{
|
||||
"navTitle": "Angular Modules",
|
||||
"tooltip": "Learn how directives modify the layout and behavior of elements.",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/ngmodule",
|
||||
"navTitle": "NgModule",
|
||||
"tooltip": "Define application modules with @NgModule."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/ngmodule-faq",
|
||||
"navTitle": "Angular module FAQs",
|
||||
"tooltip": "Answers to frequently asked questions about @NgModule."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"docId": "guide/component-communication",
|
||||
"navTitle": "Component interaction",
|
||||
"tooltip": "Share information between different directives and components."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/component-relative-paths",
|
||||
"navTitle": "Component-relative paths",
|
||||
"tooltip": "Use relative URLs for component templates and styles."
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Dependency Injection",
|
||||
"tooltip": "More about Dependency Injection",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/cb-dependency-injection",
|
||||
"navTitle": "Dependency injection",
|
||||
"tooltip": "Techniques for Dependency Injection."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/hierarchical-dependency-injection",
|
||||
"navTitle": "Hierarchical injectors",
|
||||
"tooltip": "Angular's hierarchical dependency injection system supports nested injectors in parallel with the component tree."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"docId": "guide/dynamic-component-loader",
|
||||
"navTitle": "Dynamic components",
|
||||
"tooltip": "Load components dynamically."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/directives",
|
||||
"navTitle": "Directives",
|
||||
"tooltip": "Learn how directives modify the layout and behavior of elements.",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/attribute-directives",
|
||||
"navTitle": "Attribute directives",
|
||||
"tooltip": "Attribute directives attach behavior to elements."
|
||||
},
|
||||
{
|
||||
"docId": "guide/structural-directives",
|
||||
"navTitle": "Structural directives",
|
||||
"tooltip": "Structural directives manipulate the layout of the page."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Forms",
|
||||
"tooltip": "More about forms",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/dynamic-form",
|
||||
"navTitle": "Dynamic forms",
|
||||
"tooltip": "Render dynamic forms with FormGroup."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/form-validation",
|
||||
"navTitle": "Form validation",
|
||||
"tooltip": "Validate user's form entries."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/reactive-forms",
|
||||
"navTitle": "Reactive forms",
|
||||
"tooltip": "Create a reactive form using FormBuilder, groups, and arrays."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"docId": "guide/server-communication",
|
||||
"navTitle": "HTTP client",
|
||||
"tooltip": "Use an HTTP Client to talk to a remote server."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/lifecycle-hooks",
|
||||
"navTitle": "Lifecycle hooks",
|
||||
"tooltip": "Angular calls lifecycle hook methods on directives and components as it creates, changes, and destroys them."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/pipes",
|
||||
"navTitle": "Pipes",
|
||||
"tooltip": "Pipes transform displayed values within a template."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/router",
|
||||
"navTitle": "Routing & navigation",
|
||||
"tooltip": "Discover the basics of screen navigation with the Angular Router."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"navTitle": "Additional Techniques",
|
||||
"tooltip": "Other",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/aot-compiler",
|
||||
"navTitle": "Ahead-of-Time compilation",
|
||||
"tooltip": "Learn why and how to use the Ahead-of-Time (AOT) compiler."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/animations",
|
||||
"navTitle": "Animations",
|
||||
"tooltip": "A guide to Angular's animation system."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/ajs-quick-reference",
|
||||
"navTitle": "AngularJS to Angular",
|
||||
"tooltip": "Learn how AngularJS concepts and techniques map to Angular."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/component-styles",
|
||||
"navTitle": "Component styles",
|
||||
"tooltip": "Learn how to apply CSS styles to components."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/deployment",
|
||||
"navTitle": "Deployment",
|
||||
"tooltip": "Learn how to deploy your Angular app."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/i18n",
|
||||
"navTitle": "Internationalization (i18n)",
|
||||
"tooltip": "Translate the app's template text into multiple languages."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/security",
|
||||
"navTitle": "Security",
|
||||
"tooltip": "Developing for content security in Angular applications."
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Setup",
|
||||
"tooltip": "Details of the local development setup",
|
||||
"children": [
|
||||
{
|
||||
"docId": "guide/setup-systemjs-anatomy",
|
||||
"navTitle": "Setup Anatomy",
|
||||
"tooltip": "Inside the local development environment for SystemJS."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/browser-support",
|
||||
"navTitle": "Browser support",
|
||||
"tooltip": "Browser support and polyfills guide."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/npm-packages",
|
||||
"navTitle": "Npm packages",
|
||||
"tooltip": "Recommended npm packages, and how to specify package dependencies."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/typescript-configuration",
|
||||
"navTitle": "TypeScript configuration",
|
||||
"tooltip": "TypeScript configuration for Angular developers."
|
||||
}
|
||||
]},
|
||||
|
||||
{
|
||||
"docId": "guide/testing",
|
||||
"navTitle": "Testing",
|
||||
"tooltip": "Techniques and practices for testing an Angular app."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/upgrade",
|
||||
"navTitle": "Upgrading from AngularJS",
|
||||
"tooltip": "Incrementally upgrade an AngularJS application to Angular."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/ts-to-js",
|
||||
"navTitle": "TypeScript to JavaScript",
|
||||
"tooltip": "Convert Angular TypeScript examples into ES6 and ES5 JavaScript."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/visual-studio-2015",
|
||||
"navTitle": "Visual Studio 2015 QuickStart",
|
||||
"tooltip": "Use Visual Studio 2015 with the QuickStart files."
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "guide/webpack",
|
||||
"navTitle": "Webpack: an introduction",
|
||||
"tooltip": "Create Angular applications with a Webpack based tooling."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"docId": "resources/",
|
||||
"navTitle": "Resources",
|
||||
"children": [
|
||||
{
|
||||
"docId": "about",
|
||||
"navTitle": "About",
|
||||
"tooltip": "The people behind Angular."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"navTitle": "Help",
|
||||
"children": [
|
||||
{
|
||||
"url":
|
||||
"http://stackoverflow.com/questions/tagged/angular2",
|
||||
"urlNew": "http://stackoverflow.com/questions/tagged/angular2.html",
|
||||
"navTitle": "Stack Overflow",
|
||||
"tooltip": "Stack Overflow: where the community answers your technical Angular questions."
|
||||
},
|
||||
{
|
||||
"url": "https://gitter.im/angular/angular",
|
||||
"navTitle": "Gitter",
|
||||
"tooltip": "Chat about Angular with other birds of a feather."
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
Loading…
Reference in New Issue