feat(aio): add sidenav driven by navigation data (#14429)
This commit is contained in:
parent
56f232cdd7
commit
9a6f3d637f
|
@ -3,7 +3,7 @@ import { browser, element, by } from 'protractor';
|
||||||
export class SitePage {
|
export class SitePage {
|
||||||
|
|
||||||
links = element.all(by.css('md-toolbar a'));
|
links = element.all(by.css('md-toolbar a'));
|
||||||
datePipeLink = element(by.css('md-toolbar a[aioNavLink="docs/api/common/DatePipe"]'));
|
datePipeLink = element(by.css('md-toolbar a[aioNavLink="api/common/date-pipe"]'));
|
||||||
docViewer = element(by.css('aio-doc-viewer'));
|
docViewer = element(by.css('aio-doc-viewer'));
|
||||||
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
||||||
featureLink = element(by.css('md-toolbar a[aioNavLink="features"]'));
|
featureLink = element(by.css('md-toolbar a[aioNavLink="features"]'));
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<md-toolbar color="primary" class="app-toolbar">
|
<md-toolbar color="primary" class="app-toolbar">
|
||||||
<span>Angular</span>
|
<button *ngIf="isHamburgerVisible" class="hamburger" md-button (click)="toggleSideNav()"><md-icon>menu</md-icon></button>
|
||||||
<span><a class="nav-link" aioNavLink="home"> Home </a></span>
|
<aio-menu></aio-menu>
|
||||||
<span><a class="nav-link" aioNavLink="news"> News</a></span>
|
<md-input-container >
|
||||||
<span><a class="nav-link" aioNavLink="features"> Features</a></span>
|
<input #search md-input placeholder="Search">
|
||||||
<span><a class="nav-link" aioNavLink="docs/api/common/DatePipe"> DatePipe</a></span>
|
</md-input-container>
|
||||||
<span class="fill-remaining-space"></span>
|
<span class="fill-remaining-space"></span>
|
||||||
</md-toolbar>
|
</md-toolbar>
|
||||||
<section class="app-content">
|
|
||||||
<aio-doc-viewer [doc]="navEngine.currentDoc"></aio-doc-viewer>
|
<aio-sidenav></aio-sidenav>
|
||||||
</section>
|
|
||||||
|
|
|
@ -1,8 +1,22 @@
|
||||||
.fill-remaining-space {
|
.fill-remaining-space {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.nav-link {
|
|
||||||
margin-right: 10px;
|
md-input-container {
|
||||||
margin-left: 20px;
|
margin-left: 10px;
|
||||||
cursor: pointer;
|
input {
|
||||||
|
min-width:200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.md-input-element {
|
||||||
|
font-size: 70%;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
aio-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
import { NavEngine } from './nav-engine/nav-engine.service';
|
import { SidenavComponent } from './sidenav/sidenav.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-shell',
|
selector: 'aio-shell',
|
||||||
|
@ -8,5 +8,9 @@ import { NavEngine } from './nav-engine/nav-engine.service';
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
constructor(public navEngine: NavEngine) {}
|
isHamburgerVisible = true; // always ... for now
|
||||||
|
|
||||||
|
@ViewChild(SidenavComponent) sidenav: SidenavComponent;
|
||||||
|
|
||||||
|
toggleSideNav() { this.sidenav.toggle(); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,51 @@
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
|
|
||||||
import { MdToolbarModule } from '@angular/material/toolbar';
|
import { MdToolbarModule } from '@angular/material/toolbar';
|
||||||
import { MdButtonModule} from '@angular/material/button';
|
import { MdButtonModule} from '@angular/material/button';
|
||||||
|
import { MdIconModule} from '@angular/material/icon';
|
||||||
|
import { MdInputModule } from '@angular/material/input';
|
||||||
|
import { MdSidenavModule } from '@angular/material/sidenav';
|
||||||
|
import { Platform } from '@angular/material/core';
|
||||||
|
|
||||||
|
// Temporary fix for MdSidenavModule issue:
|
||||||
|
// crashes with "missing first" operator when SideNav.mode is "over"
|
||||||
|
import 'rxjs/add/operator/first';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { DocViewerComponent } from './doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from './doc-viewer/doc-viewer.component';
|
||||||
import { embeddedComponents, EmbeddedComponents } from './embedded';
|
import { embeddedComponents, EmbeddedComponents } from './embedded';
|
||||||
import { Logger } from './logger.service';
|
import { Logger } from './logger.service';
|
||||||
import { navDirectives, navProviders } from './nav-engine';
|
import { navDirectives, navProviders } from './nav-engine';
|
||||||
|
import { SidenavComponent } from './sidenav/sidenav.component';
|
||||||
|
import { NavItemComponent } from './sidenav/nav-item.component';
|
||||||
|
import { MenuComponent } from './sidenav/menu.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
|
MdButtonModule.forRoot(),
|
||||||
|
MdIconModule.forRoot(),
|
||||||
|
MdInputModule.forRoot(),
|
||||||
MdToolbarModule.forRoot(),
|
MdToolbarModule.forRoot(),
|
||||||
MdButtonModule.forRoot()
|
MdSidenavModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
embeddedComponents,
|
embeddedComponents,
|
||||||
DocViewerComponent,
|
DocViewerComponent,
|
||||||
|
MenuComponent,
|
||||||
navDirectives,
|
navDirectives,
|
||||||
|
NavItemComponent,
|
||||||
|
SidenavComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
EmbeddedComponents,
|
EmbeddedComponents,
|
||||||
Logger,
|
Logger,
|
||||||
navProviders
|
navProviders,
|
||||||
|
Platform
|
||||||
],
|
],
|
||||||
entryComponents: [ embeddedComponents ],
|
entryComponents: [ embeddedComponents ],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|
|
@ -18,9 +18,7 @@ import { embeddedComponents, EmbeddedComponents } from '../embedded';
|
||||||
selector: 'aio-foo',
|
selector: 'aio-foo',
|
||||||
template: `Foo Component`
|
template: `Foo Component`
|
||||||
})
|
})
|
||||||
class FooComponent {
|
class FooComponent { }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
///// BarComponent /////
|
///// BarComponent /////
|
||||||
|
|
||||||
|
@ -101,7 +99,7 @@ class TestComponent {
|
||||||
//////// Tests //////////////
|
//////// Tests //////////////
|
||||||
|
|
||||||
describe('DocViewerComponent', () => {
|
describe('DocViewerComponent', () => {
|
||||||
const mockDocMetadata: DocMetadata = { id: 'mock', title: 'Mock Doc', url: '' };
|
const fakeDocMetadata: DocMetadata = { docId: 'fake', title: 'fake Doc' };
|
||||||
let component: TestComponent;
|
let component: TestComponent;
|
||||||
let docViewerDE: DebugElement;
|
let docViewerDE: DebugElement;
|
||||||
let docViewerEl: HTMLElement;
|
let docViewerEl: HTMLElement;
|
||||||
|
@ -135,21 +133,21 @@ describe('DocViewerComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(('should display nothing when set DocViewer.doc to doc w/o content'), () => {
|
it(('should display nothing when set DocViewer.doc to doc w/o content'), () => {
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content: '' };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content: '' };
|
||||||
expect(docViewerEl.innerHTML).toBe('');
|
expect(docViewerEl.innerHTML).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(('should display simple static content doc'), () => {
|
it(('should display simple static content doc'), () => {
|
||||||
const content = '<p>Howdy, doc viewer</p>';
|
const content = '<p>Howdy, doc viewer</p>';
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
expect(docViewerEl.innerHTML).toEqual(content);
|
expect(docViewerEl.innerHTML).toEqual(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(('should display nothing after reset static content doc'), () => {
|
it(('should display nothing after reset static content doc'), () => {
|
||||||
const content = '<p>Howdy, doc viewer</p>';
|
const content = '<p>Howdy, doc viewer</p>';
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content: '' };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content: '' };
|
||||||
expect(docViewerEl.innerHTML).toEqual('');
|
expect(docViewerEl.innerHTML).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -159,7 +157,7 @@ describe('DocViewerComponent', () => {
|
||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Below Foo</p>
|
<p>Below Foo</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML;
|
||||||
expect(fooHtml).toContain('Foo Component');
|
expect(fooHtml).toContain('Foo Component');
|
||||||
});
|
});
|
||||||
|
@ -174,7 +172,7 @@ describe('DocViewerComponent', () => {
|
||||||
</div>
|
</div>
|
||||||
<p>Below Foo</p>
|
<p>Below Foo</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
const foos = docViewerEl.querySelectorAll('aio-foo');
|
const foos = docViewerEl.querySelectorAll('aio-foo');
|
||||||
expect(foos.length).toBe(2);
|
expect(foos.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
@ -185,7 +183,7 @@ describe('DocViewerComponent', () => {
|
||||||
<aio-bar></aio-bar>
|
<aio-bar></aio-bar>
|
||||||
<p>Below Bar</p>
|
<p>Below Bar</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
const barHtml = docViewerEl.querySelector('aio-bar').innerHTML;
|
||||||
expect(barHtml).toContain('Bar Component');
|
expect(barHtml).toContain('Bar Component');
|
||||||
});
|
});
|
||||||
|
@ -196,7 +194,7 @@ describe('DocViewerComponent', () => {
|
||||||
<aio-bar>###bar content###</aio-bar>
|
<aio-bar>###bar content###</aio-bar>
|
||||||
<p>Below Bar</p>
|
<p>Below Bar</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
|
|
||||||
// necessary to trigger projection within ngOnInit
|
// necessary to trigger projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -214,7 +212,7 @@ describe('DocViewerComponent', () => {
|
||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -237,7 +235,7 @@ describe('DocViewerComponent', () => {
|
||||||
<p><aio-foo></aio-foo><p>
|
<p><aio-foo></aio-foo><p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -261,7 +259,7 @@ describe('DocViewerComponent', () => {
|
||||||
<p><aio-foo></aio-foo></p>
|
<p><aio-foo></aio-foo></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@ -289,7 +287,7 @@ describe('DocViewerComponent', () => {
|
||||||
<p><aio-baz>---More baz--</aio-baz></p>
|
<p><aio-baz>---More baz--</aio-baz></p>
|
||||||
<p>Bottom</p>
|
<p>Bottom</p>
|
||||||
`;
|
`;
|
||||||
component.docViewer.doc = { metadata: mockDocMetadata, content };
|
component.docViewer.doc = { metadata: fakeDocMetadata, content };
|
||||||
|
|
||||||
// necessary to trigger Bar's projection within ngOnInit
|
// necessary to trigger Bar's projection within ngOnInit
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation
|
DoCheck, ElementRef, Injector, Input, OnDestroy, ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { Doc, DocMetadata } from '../nav-engine';
|
import { Doc, DocMetadata, DocMetadataService, NavNode } from '../nav-engine';
|
||||||
import { EmbeddedComponents } from '../embedded';
|
import { EmbeddedComponents } from '../embedded';
|
||||||
|
|
||||||
interface EmbeddedComponentFactory {
|
interface EmbeddedComponentFactory {
|
||||||
|
@ -17,7 +17,14 @@ const initialDocViewerContent = initialDocViewerElement ? initialDocViewerElemen
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-doc-viewer',
|
selector: 'aio-doc-viewer',
|
||||||
template: ''
|
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 (?!)
|
// TODO(robwormald): shadow DOM and emulated don't work here (?!)
|
||||||
// encapsulation: ViewEncapsulation.Native
|
// encapsulation: ViewEncapsulation.Native
|
||||||
})
|
})
|
||||||
|
@ -31,7 +38,8 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||||
componentFactoryResolver: ComponentFactoryResolver,
|
componentFactoryResolver: ComponentFactoryResolver,
|
||||||
elementRef: ElementRef,
|
elementRef: ElementRef,
|
||||||
embeddedComponents: EmbeddedComponents,
|
embeddedComponents: EmbeddedComponents,
|
||||||
private injector: Injector,
|
private docMetadataService: DocMetadataService,
|
||||||
|
private injector: Injector
|
||||||
) {
|
) {
|
||||||
this.hostElement = elementRef.nativeElement;
|
this.hostElement = elementRef.nativeElement;
|
||||||
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
// Security: the initialDocViewerContent comes from the prerendered DOM and is considered to be secure
|
||||||
|
@ -49,6 +57,7 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||||
set doc(newDoc: Doc) {
|
set doc(newDoc: Doc) {
|
||||||
this.ngOnDestroy();
|
this.ngOnDestroy();
|
||||||
if (newDoc) {
|
if (newDoc) {
|
||||||
|
this.docMetadataService.metadata = newDoc.metadata;
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.build(newDoc);
|
this.build(newDoc);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* tslint:disable component-selector */
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { DocMetadataService } from '../nav-engine';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'doc-title',
|
||||||
|
template: '<h1 class="docs-primary-header">{{title}}</h1>'
|
||||||
|
})
|
||||||
|
export class DocTitleComponent {
|
||||||
|
title: string;
|
||||||
|
constructor(metadataService: DocMetadataService) {
|
||||||
|
this.title = metadataService.metadata.title;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { CodeExampleComponent } from './code-example.component';
|
import { CodeExampleComponent } from './code-example.component';
|
||||||
|
import { DocTitleComponent } from './doc-title.component';
|
||||||
|
|
||||||
/** Components that can be embedded in docs such as CodeExampleComponent, LiveExampleComponent,... */
|
/** Components that can be embedded in docs such as CodeExampleComponent, LiveExampleComponent,... */
|
||||||
export const embeddedComponents = [
|
export const embeddedComponents: any[] = [
|
||||||
CodeExampleComponent
|
CodeExampleComponent, DocTitleComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Injectable class w/ property returning components that can be embedded in docs */
|
/** Injectable class w/ property returning components that can be embedded in docs */
|
||||||
|
|
|
@ -7,39 +7,74 @@ import 'rxjs/add/operator/catch';
|
||||||
import 'rxjs/add/operator/do';
|
import 'rxjs/add/operator/do';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
|
|
||||||
|
import { Doc, DocMetadata } from './doc.model';
|
||||||
import { Logger } from '../logger.service';
|
import { Logger } from '../logger.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocFetchingService {
|
export class DocFetchingService {
|
||||||
|
|
||||||
constructor(private http: Http, private logger: Logger) { }
|
private base = 'content/';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: Http,
|
||||||
|
private logger: Logger) { }
|
||||||
|
|
||||||
|
getPath(docId: string) {
|
||||||
|
return this.base + addPathExtension(docId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch document from server.
|
* Fetch document from server.
|
||||||
* NB: pass 404 response to caller as empty string content
|
* NB: pass 404 response to caller as Doc with empty string content
|
||||||
* Other errors and non-OK status responses are thrown errors.
|
* Other errors and non-OK status responses are thrown errors.
|
||||||
* TODO: add timeout and retry for lost connection
|
* TODO: add timeout and retry for lost connection
|
||||||
*/
|
*/
|
||||||
getFile(url: string): Observable<string> {
|
getDocFile(docId: string): Observable<Doc> {
|
||||||
|
|
||||||
if (!url) {
|
if (!docId) {
|
||||||
const emsg = 'getFile: no URL';
|
const emsg = 'getFile: no document id';
|
||||||
this.logger.error(emsg);
|
this.logger.error(emsg);
|
||||||
throw new Error(emsg);
|
throw new Error(emsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('fetching document file at ', url);
|
// 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)
|
return this.http.get(url)
|
||||||
.map(res => res.text())
|
.map(res => <Doc> { metadata, content: res.text() }) // TODO: It will come as JSON soon
|
||||||
.do(content => this.logger.log('fetched document file at ', url) )
|
.do(content => this.logger.log(`Fetched document file at '${url}'`) )
|
||||||
.catch((error: Response) => {
|
.catch((error: Response) => {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
this.logger.error(`Document file not found at '$(url)'`);
|
this.logger.error(`Document file not found at '${url}'`);
|
||||||
return of('');
|
return of({metadata, content: ''} as Doc);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
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;
|
||||||
|
// }
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DocMetadata } from './doc.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DocMetadataService {
|
||||||
|
metadata: DocMetadata;
|
||||||
|
}
|
|
@ -1,10 +1,54 @@
|
||||||
export interface DocMetadata {
|
export interface DocMetadata {
|
||||||
id: string; // 'home'
|
docId: string;
|
||||||
title: string; // 'Home'
|
title: string;
|
||||||
url: string; // 'assets/documents/home.html'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Doc {
|
export interface Doc {
|
||||||
metadata: DocMetadata;
|
metadata: DocMetadata;
|
||||||
content: string;
|
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,71 +1,80 @@
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
import { DocService } from './doc.service';
|
import { DocService } from './doc.service';
|
||||||
import { Doc, DocMetadata } from './doc.model';
|
import { Doc, DocMetadata, NavNode } from './doc.model';
|
||||||
import { DocFetchingService } from './doc-fetching.service';
|
import { DocFetchingService } from './doc-fetching.service';
|
||||||
import { SiteMapService } from './sitemap.service';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { of } from 'rxjs/observable/of';
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import 'rxjs/add/observable/throw';
|
||||||
import 'rxjs/add/operator/catch';
|
import 'rxjs/add/operator/catch';
|
||||||
import 'rxjs/add/operator/delay';
|
import 'rxjs/add/operator/delay';
|
||||||
|
import 'rxjs/add/operator/take';
|
||||||
|
|
||||||
describe('DocService', () => {
|
describe('DocService', () => {
|
||||||
let docFetchingService: DocFetchingService;
|
let docFetchingService: DocFetchingService;
|
||||||
let getFileSpy: jasmine.Spy;
|
let getFileSpy: jasmine.Spy;
|
||||||
let loggerSpy: any;
|
let loggerSpy: any;
|
||||||
let siteMapService: SiteMapService;
|
|
||||||
let docService: DocService;
|
let docService: DocService;
|
||||||
|
let testDoc: Doc;
|
||||||
|
let testDocId: string;
|
||||||
|
let testContent: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
testDocId = 'fake';
|
||||||
this.content = 'fake file contents';
|
testContent = 'fake file contents';
|
||||||
this.metadata = {
|
testDoc = {
|
||||||
id: 'fake',
|
metadata: {docId: testDocId, title: 'Fake Title'} as DocMetadata,
|
||||||
title: 'All about the fake',
|
content: testContent
|
||||||
url: 'assets/documents/fake.html'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
||||||
siteMapService = new SiteMapService();
|
|
||||||
spyOn(siteMapService, 'getDocMetadata').and
|
|
||||||
.callFake((id: string) => of(this.metadata).delay(0));
|
|
||||||
|
|
||||||
docFetchingService = new DocFetchingService(null, loggerSpy);
|
docFetchingService = new DocFetchingService(null, loggerSpy);
|
||||||
getFileSpy = spyOn(docFetchingService, 'getFile').and
|
getFileSpy = spyOn(docFetchingService, 'getDocFile').and
|
||||||
.callFake((url: string) => of(this.content).delay(0));
|
.returnValue(of(testDoc).delay(0).take(1)); // take(1) -> completes
|
||||||
|
|
||||||
docService = new DocService(docFetchingService, loggerSpy, siteMapService);
|
docService = new DocService(docFetchingService, loggerSpy);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return fake doc for fake id', fakeAsync(() => {
|
it('should return fake doc for fake id', fakeAsync(() => {
|
||||||
docService.getDoc('fake').subscribe(doc =>
|
docService.getDoc(testDocId).subscribe(doc =>
|
||||||
expect(doc.content).toBe(this.content)
|
expect(doc.content).toBe(testContent)
|
||||||
);
|
);
|
||||||
tick();
|
tick();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should retrieve file once for first file request', fakeAsync(() => {
|
it('should retrieve file once for first file request', fakeAsync(() => {
|
||||||
docService.getDoc('fake').subscribe();
|
let doc: Doc;
|
||||||
expect(getFileSpy.calls.count()).toBe(0, 'no call before tick');
|
expect(getFileSpy.calls.count()).toBe(0, 'no call before tick');
|
||||||
|
docService.getDoc(testDocId).subscribe(d => doc = d);
|
||||||
tick();
|
tick();
|
||||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after 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(() => {
|
it('should retrieve file from cache the second time', fakeAsync(() => {
|
||||||
docService.getDoc('fake').subscribe();
|
docService.getDoc(testDocId).subscribe(doc =>
|
||||||
|
expect(doc).toBe(testDoc, 'expected doc from server')
|
||||||
|
);
|
||||||
tick();
|
tick();
|
||||||
expect(getFileSpy.calls.count()).toBe(1, 'one call after 1st request');
|
expect(getFileSpy.calls.count()).toBe(1, 'one call after 1st request');
|
||||||
|
|
||||||
docService.getDoc('fake').subscribe();
|
docService.getDoc(testDocId).subscribe(doc =>
|
||||||
|
expect(doc).toBe(testDoc, 'expected doc from cache')
|
||||||
|
);
|
||||||
tick();
|
tick();
|
||||||
expect(getFileSpy.calls.count()).toBe(1, 'still only one call after 2nd request');
|
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(() => {
|
it('should pass along file error through its getDoc observable result', fakeAsync(() => {
|
||||||
|
|
||||||
const err = 'deliberate file error';
|
const err = 'deliberate file error';
|
||||||
getFileSpy.and.throwError(err);
|
getFileSpy.and.returnValue(
|
||||||
docService.getDoc('fake').subscribe(
|
// 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'),
|
doc => expect(false).toBe(true, 'should have failed'),
|
||||||
error => expect(error.message).toBe(err)
|
error => expect(error.message).toBe(err)
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,59 +2,56 @@ import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { of } from 'rxjs/observable/of';
|
import { of } from 'rxjs/observable/of';
|
||||||
|
import 'rxjs/add/operator/do';
|
||||||
import 'rxjs/add/operator/map';
|
import 'rxjs/add/operator/map';
|
||||||
import 'rxjs/add/operator/switchMap';
|
import 'rxjs/add/operator/switchMap';
|
||||||
|
|
||||||
import { Doc, DocMetadata } from './doc.model';
|
import { Doc, NavNode } from './doc.model';
|
||||||
import { DocFetchingService } from './doc-fetching.service';
|
import { DocFetchingService } from './doc-fetching.service';
|
||||||
import { Logger } from '../logger.service';
|
import { Logger } from '../logger.service';
|
||||||
|
|
||||||
import { SiteMapService } from './sitemap.service';
|
import { NavMapService } from './nav-map.service';
|
||||||
|
|
||||||
interface DocCache {
|
|
||||||
[index: string]: Doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocService {
|
export class DocService {
|
||||||
private cache: DocCache = {};
|
private cache = new Map<string, Doc>();
|
||||||
|
private notFoundContent: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fileService: DocFetchingService,
|
private fileService: DocFetchingService,
|
||||||
private logger: Logger,
|
private logger: Logger
|
||||||
private siteMapService: SiteMapService
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get document for documentId, from cache if found else server.
|
* Get document for id, from cache if found else server.
|
||||||
* Pass server errors along to caller
|
* Pass server errors along to caller
|
||||||
* Caller should interpret empty string content as "404 - file not found"
|
* Constructs and caches a "Not Found" doc when fileservice returns a doc with no content.
|
||||||
*/
|
*/
|
||||||
getDoc(documentId: string): Observable<Doc> {
|
getDoc(docId: string): Observable<Doc> {
|
||||||
let doc = this.cache[documentId];
|
const cached = this.cache.get(docId);
|
||||||
if (doc) {
|
if (cached) {
|
||||||
this.logger.log('returned cached content for ', doc.metadata);
|
this.logger.log(`Returned cached document for '${docId}'`);
|
||||||
return of(cloneDoc(doc));
|
return of(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.siteMapService
|
return this.fileService.getDocFile(docId)
|
||||||
.getDocMetadata(documentId)
|
.switchMap(doc => {
|
||||||
.switchMap(metadata => {
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
return this.fileService.getFile(metadata.url)
|
getNotFound(): Observable<string> {
|
||||||
.map(content => {
|
if (this.notFoundContent) { return of(this.notFoundContent); }
|
||||||
this.logger.log('fetched content for', metadata);
|
const nfDocId = 'not-found';
|
||||||
doc = { metadata, content };
|
return this.fileService.getDocFile(nfDocId)
|
||||||
this.cache[metadata.id] = doc;
|
.map(doc => {
|
||||||
return cloneDoc(doc);
|
this.logger.log(`Fetched "not found" document for '${nfDocId}'`);
|
||||||
});
|
this.notFoundContent = doc.content;
|
||||||
|
return doc.content;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneDoc(doc: Doc) {
|
|
||||||
return {
|
|
||||||
metadata: Object.assign({}, doc.metadata),
|
|
||||||
content: doc.content
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,9 +2,12 @@ import { DocService } from './doc.service';
|
||||||
import { DocFetchingService } from './doc-fetching.service';
|
import { DocFetchingService } from './doc-fetching.service';
|
||||||
import { NavEngine } from './nav-engine.service';
|
import { NavEngine } from './nav-engine.service';
|
||||||
import { NavLinkDirective } from './nav-link.directive';
|
import { NavLinkDirective } from './nav-link.directive';
|
||||||
import { SiteMapService } from './sitemap.service';
|
import { NavMapService } from './nav-map.service';
|
||||||
|
|
||||||
export { Doc, DocMetadata } from './doc.model';
|
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 = [
|
export const navDirectives = [
|
||||||
NavLinkDirective
|
NavLinkDirective
|
||||||
|
@ -14,5 +17,5 @@ export const navProviders = [
|
||||||
DocService,
|
DocService,
|
||||||
DocFetchingService,
|
DocFetchingService,
|
||||||
NavEngine,
|
NavEngine,
|
||||||
SiteMapService,
|
NavMapService,
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,42 +5,35 @@ import { of } from 'rxjs/observable/of';
|
||||||
import 'rxjs/add/operator/delay';
|
import 'rxjs/add/operator/delay';
|
||||||
|
|
||||||
import { DocService } from './doc.service';
|
import { DocService } from './doc.service';
|
||||||
import { Doc, DocMetadata } from './doc.model';
|
import { Doc, DocMetadata, NavNode } from './doc.model';
|
||||||
|
|
||||||
import { NavEngine } from './nav-engine.service';
|
import { NavEngine } from './nav-engine.service';
|
||||||
|
|
||||||
const fakeDoc: Doc = {
|
|
||||||
metadata: {
|
|
||||||
id: 'fake',
|
|
||||||
title: 'All about the fake',
|
|
||||||
url: 'assets/documents/fake.html'
|
|
||||||
},
|
|
||||||
content: 'fake content'
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('NavEngine', () => {
|
describe('NavEngine', () => {
|
||||||
|
|
||||||
|
let fakeDoc: Doc;
|
||||||
let navEngine: NavEngine;
|
let navEngine: NavEngine;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
this.fakeDoc = {
|
fakeDoc = {
|
||||||
metadata: {
|
metadata: {
|
||||||
id: 'fake',
|
docId: 'fake',
|
||||||
title: 'All about the fake',
|
title: 'All about the fake'
|
||||||
url: 'assets/documents/fake.html'
|
|
||||||
},
|
},
|
||||||
content: 'fake content'
|
content: 'fake content'
|
||||||
};
|
};
|
||||||
|
|
||||||
const docService: any = jasmine.createSpyObj('docService', ['getDoc']);
|
const docService: any = jasmine.createSpyObj('docService', ['getDoc']);
|
||||||
docService.getDoc.and.callFake((id: string) => of(this.fakeDoc).delay(0));
|
docService.getDoc.and.returnValue(of(fakeDoc).delay(0));
|
||||||
|
|
||||||
navEngine = new NavEngine(docService);
|
navEngine = new NavEngine(docService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set currentDoc to fake doc when navigate to fake id', fakeAsync(() => {
|
it('should set currentDoc to fake doc when navigate to fake id', fakeAsync(() => {
|
||||||
navEngine.navigate('fake');
|
navEngine.navigate('fake');
|
||||||
|
navEngine.currentDoc.subscribe(doc =>
|
||||||
|
expect(doc.content).toBe(fakeDoc.content)
|
||||||
|
);
|
||||||
tick();
|
tick();
|
||||||
expect(navEngine.currentDoc.content).toBe(this.fakeDoc.content);
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,24 +1,33 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { ReplaySubject } from 'rxjs/ReplaySubject';
|
||||||
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
|
||||||
import { Doc } from './doc.model';
|
import { Doc } from './doc.model';
|
||||||
import { DocService } from './doc.service';
|
import { DocService } from './doc.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NavEngine {
|
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();
|
||||||
|
|
||||||
/** Document result of most recent `navigate` call */
|
|
||||||
currentDoc: Doc;
|
|
||||||
constructor(private docService: DocService) {}
|
constructor(private docService: DocService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate sets `currentDoc` to the document for `documentId`.
|
* Navigate pushes new doc for the given `id` into the `currentDoc` observable.
|
||||||
* TODO: handle 'Document not found', signaled by empty string content
|
|
||||||
* TODO: handle document retrieval error
|
* TODO: handle document retrieval error
|
||||||
*/
|
*/
|
||||||
navigate(documentId: string) {
|
navigate(docId: string) {
|
||||||
this.docService.getDoc(documentId).subscribe(
|
this.ngOnDestroy();
|
||||||
doc => this.currentDoc = doc
|
this.subscription = this.docService.getDoc(docId).subscribe(doc => this.docSubject.next(doc));
|
||||||
);
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.subscription) { this.subscription.unsubscribe(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
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).take(1)); // take(1) -> completes
|
||||||
|
loggerSpy = jasmine.createSpyObj('logger', ['log', 'warn', 'error']);
|
||||||
|
|
||||||
|
navMapService = new NavMapService(new DocFetchingService(null, null), httpSpy, loggerSpy);
|
||||||
|
|
||||||
|
navMapService.navMap.take(1).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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,88 @@
|
||||||
|
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,32 +0,0 @@
|
||||||
import { fakeAsync, tick } from '@angular/core/testing';
|
|
||||||
import { DocMetadata } from './doc.model';
|
|
||||||
import { SiteMapService } from './sitemap.service';
|
|
||||||
|
|
||||||
describe('SiteMapService', () => {
|
|
||||||
let siteMapService: SiteMapService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
siteMapService = new SiteMapService();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get News metadata', fakeAsync(() => {
|
|
||||||
siteMapService.getDocMetadata('news').subscribe(
|
|
||||||
metadata => expect(metadata.url).toBe('content/news.html')
|
|
||||||
);
|
|
||||||
tick();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should calculate expected doc url for unknown id', fakeAsync(() => {
|
|
||||||
siteMapService.getDocMetadata('fizbuz').subscribe(
|
|
||||||
metadata => expect(metadata.url).toBe('content/fizbuz.html')
|
|
||||||
);
|
|
||||||
tick();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should calculate expected index doc url for unknown id ending in /', fakeAsync(() => {
|
|
||||||
siteMapService.getDocMetadata('fizbuz/').subscribe(
|
|
||||||
metadata => expect(metadata.url).toBe('content/fizbuz/index.html')
|
|
||||||
);
|
|
||||||
tick();
|
|
||||||
}));
|
|
||||||
});
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { of } from 'rxjs/observable/of';
|
|
||||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
|
|
||||||
import { DocMetadata } from './doc.model';
|
|
||||||
|
|
||||||
const siteMap: DocMetadata[] = [
|
|
||||||
{ 'title': 'Home', 'url': 'content/home.html', id: 'home'},
|
|
||||||
{ 'title': 'Features', 'url': 'content/features.html', id: 'features'},
|
|
||||||
{ 'title': 'News', 'url': 'content/news.html', id: 'news'}
|
|
||||||
];
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SiteMapService {
|
|
||||||
private siteMap = new BehaviorSubject(siteMap);
|
|
||||||
|
|
||||||
getDocMetadata(id: string) {
|
|
||||||
const missing = () => this.getMissingMetadata(id);
|
|
||||||
return this.siteMap
|
|
||||||
.map(map =>
|
|
||||||
map.find(d => d.id === id) || missing());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alternative way to calculate metadata. Will it be used?
|
|
||||||
private getMissingMetadata(id: string) {
|
|
||||||
|
|
||||||
const filename = id.startsWith('/') ? id.substring(1) : id; // strip leading '/'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title: id,
|
|
||||||
url: `content/${filename}${filename.endsWith('/') ? 'index' : ''}.html`
|
|
||||||
} as DocMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
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 {
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,27 @@
|
||||||
|
.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;
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
/* 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: ''
|
||||||
|
})
|
||||||
|
export class FakeMdSideNavComponent {
|
||||||
|
_isOpen = false;
|
||||||
|
@Input() opened: boolean;
|
||||||
|
@Input() mode: 'side' | 'over';
|
||||||
|
toggle = jasmine.createSpy('toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-doc-viewer',
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
export 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).take(1),
|
||||||
|
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).take(1)
|
||||||
|
} 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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,68 @@
|
||||||
|
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.take(1) // 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
<header class="hero background-sky"><h1 class="hero-title is-standard-case">DatePipe</h1><span
|
||||||
|
class="badge is-stable">Stable</span>
|
||||||
|
<div class="clear"></div>
|
||||||
|
<h2 class="hero-subtitle">Pipe</h2></header>
|
||||||
|
<article class="l-content-small grid-fluid docs-content">
|
||||||
|
<div layout="row" layout-xs="column" class="row-margin">
|
||||||
|
<div flex="20" flex-xs="100"><h2 class="h2-api-docs">What it does</h2></div>
|
||||||
|
<div flex="80" flex-xs="100"><p>Formats a date according to locale rules.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div layout="row" layout-xs="column" class="row-margin">
|
||||||
|
<div flex="20" flex-xs="100"><h2 class="h2-api-docs">How to use</h2></div>
|
||||||
|
<div flex="80" flex-xs="100"><p><code>date_expression | date[:format]</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div layout="row" layout-xs="column" class="row-margin">
|
||||||
|
<div flex="20" flex-xs="100"><h2 class="h2-api-docs">NgModule</h2></div>
|
||||||
|
<div flex="80" flex-xs="100" class="code-links">CommonModule
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div layout="row" layout-xs="column" class="row-margin">
|
||||||
|
<div flex="20" flex-xs="100"><h2 class="h2-api-docs">Description</h2></div>
|
||||||
|
<div flex="80" flex-xs="100" class="code-links"><p>Where:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>expression</code> is a date object or a number (milliseconds since UTC epoch) or
|
||||||
|
an ISO string
|
||||||
|
(<a href="https://www.w3.org/TR/NOTE-datetime">https://www.w3.org/TR/NOTE-datetime</a>).
|
||||||
|
</li>
|
||||||
|
<li><code>format</code> indicates which date/time components to include. The format can be
|
||||||
|
predifined as
|
||||||
|
shown below or custom as shown in the table.
|
||||||
|
<ul>
|
||||||
|
<li><code>'medium'</code>: equivalent to <code>'yMMMdjms'</code> (e.g.
|
||||||
|
<code>Sep 3, 2010, 12:05:08 PM</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'short'</code>: equivalent to <code>'yMdjm'</code> (e.g.
|
||||||
|
<code>9/3/2010, 12:05 PM</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'fullDate'</code>: equivalent to <code>'yMMMMEEEEd'</code>
|
||||||
|
(e.g. <code>Friday, September 3, 2010</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'longDate'</code>: equivalent to <code>'yMMMMd'</code> (e.g.
|
||||||
|
<code>September 3, 2010</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'mediumDate'</code>: equivalent to <code>'yMMMd'</code> (e.g.
|
||||||
|
<code>Sep 3, 2010</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'shortDate'</code>: equivalent to <code>'yMd'</code> (e.g.
|
||||||
|
<code>9/3/2010</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'mediumTime'</code>: equivalent to <code>'jms'</code> (e.g.
|
||||||
|
<code>12:05:08 PM</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
<li><code>'shortTime'</code>: equivalent to <code>'jm'</code> (e.g.
|
||||||
|
<code>12:05 PM</code> for <code>en-US</code>)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th style="text-align:center">Symbol</th>
|
||||||
|
<th>Narrow</th>
|
||||||
|
<th>Short Form</th>
|
||||||
|
<th>Long Form</th>
|
||||||
|
<th>Numeric</th>
|
||||||
|
<th>2-digit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>era</td>
|
||||||
|
<td style="text-align:center">G</td>
|
||||||
|
<td>G (A)</td>
|
||||||
|
<td>GGG (AD)</td>
|
||||||
|
<td>GGGG (Anno Domini)</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>year</td>
|
||||||
|
<td style="text-align:center">y</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>y (2015)</td>
|
||||||
|
<td>yy (15)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>month</td>
|
||||||
|
<td style="text-align:center">M</td>
|
||||||
|
<td>L (S)</td>
|
||||||
|
<td>MMM (Sep)</td>
|
||||||
|
<td>MMMM (September)</td>
|
||||||
|
<td>M (9)</td>
|
||||||
|
<td>MM (09)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>day</td>
|
||||||
|
<td style="text-align:center">d</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>d (3)</td>
|
||||||
|
<td>dd (03)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>weekday</td>
|
||||||
|
<td style="text-align:center">E</td>
|
||||||
|
<td>E (S)</td>
|
||||||
|
<td>EEE (Sun)</td>
|
||||||
|
<td>EEEE (Sunday)</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>hour</td>
|
||||||
|
<td style="text-align:center">j</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>j (13)</td>
|
||||||
|
<td>jj (13)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>hour12</td>
|
||||||
|
<td style="text-align:center">h</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>h (1 PM)</td>
|
||||||
|
<td>hh (01 PM)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>hour24</td>
|
||||||
|
<td style="text-align:center">H</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>H (13)</td>
|
||||||
|
<td>HH (13)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>minute</td>
|
||||||
|
<td style="text-align:center">m</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>m (5)</td>
|
||||||
|
<td>mm (05)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>second</td>
|
||||||
|
<td style="text-align:center">s</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>s (9)</td>
|
||||||
|
<td>ss (09)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>timezone</td>
|
||||||
|
<td style="text-align:center">z</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>z (Pacific Standard Time)</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>timezone</td>
|
||||||
|
<td style="text-align:center">Z</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>Z (GMT-8:00)</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>timezone</td>
|
||||||
|
<td style="text-align:center">a</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>a (PM)</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>-</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>In javascript, only the components specified will be respected (not the ordering,
|
||||||
|
punctuations, ...) and details of the formatting will be dependent on the locale.</p>
|
||||||
|
<p>Timezone of the formatted text will be the local system timezone of the end-user's
|
||||||
|
machine.</p>
|
||||||
|
<p>WARNINGS:</p>
|
||||||
|
<ul>
|
||||||
|
<li>this pipe is marked as pure hence it will not be re-evaluated when the input is mutated.
|
||||||
|
Instead users should treat the date as an immutable object and change the reference when
|
||||||
|
the
|
||||||
|
pipe needs to re-run (this is to avoid reformatting the date on every change detection run
|
||||||
|
which would be an expensive operation).
|
||||||
|
</li>
|
||||||
|
<li>this pipe uses the Internationalization API. Therefore it is only reliable in Chrome and
|
||||||
|
Opera
|
||||||
|
browsers.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="examples">Examples</h3>
|
||||||
|
<p>Assuming <code>dateObj</code> is (year: 2015, month: 6, day: 15, hour: 21, minute: 43,
|
||||||
|
second: 11)
|
||||||
|
in the <em>local</em> time and locale is 'en-US':</p>
|
||||||
|
<code-example format="linenums" language="js">{{ dateObj | date }} // output is 'Jun 15,
|
||||||
|
2015'
|
||||||
|
{{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
|
||||||
|
{{ dateObj | date:'shortTime' }} // output is '9:43 PM'
|
||||||
|
{{ dateObj | date:'mmss' }} // output is '43:11'
|
||||||
|
</code-example>
|
||||||
|
<div class="code-example">
|
||||||
|
<code-example language="ts" format="linenums">@Component({
|
||||||
|
selector: 'date-pipe',
|
||||||
|
template: `<div>
|
||||||
|
<p>Today is {{today | date}}</p>
|
||||||
|
<p>Or if you prefer, {{today | date:'fullDate'}}</p>
|
||||||
|
<p>The time is {{today | date:'jmZ'}}</p>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
export class DatePipeComponent {
|
||||||
|
today: number = Date.now();
|
||||||
|
}
|
||||||
|
</code-example>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="location-badge">exported from <a href="index.html">@angular/common/index</a>
|
||||||
|
defined in <a
|
||||||
|
href="https://github.com/angular/angular/tree/2.2.0-beta.1/modules/@angular/common/src/pipes/date_pipe.ts#L12-L116">@angular/common/src/pipes/date_pipe.ts</a>
|
||||||
|
</p></article>
|
|
@ -0,0 +1,110 @@
|
||||||
|
{ "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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"navTitle": "Core",
|
||||||
|
"tooltip": "Learn the core capabilities of Angular",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"docId": "guide/ngmodule",
|
||||||
|
"navTitle": "Angular Modules (NgModule)",
|
||||||
|
"tooltip": "Define application modules with @NgModule."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docId": "guide/pipes",
|
||||||
|
"navTitle": "Pipes",
|
||||||
|
"tooltip": "Pipes transform displayed values within a template."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<doc-title class="not-found"></doc-title>
|
||||||
|
<h3>Document not found</h3>
|
|
@ -19,6 +19,9 @@
|
||||||
sizes="192x192">
|
sizes="192x192">
|
||||||
<link rel="icon" type="image/png" href="assets/images/favicons/favicon-16x16.png"
|
<link rel="icon" type="image/png" href="assets/images/favicons/favicon-16x16.png"
|
||||||
sizes="16x16">
|
sizes="16x16">
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -25,16 +25,16 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-primary-header {
|
.docs-primary-header {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1.docs-primary-header, h1 .docs-primary-header{
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 50px;
|
padding: 30px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Response } from '@angular/http';
|
||||||
|
|
||||||
|
// tslint:disable:quotemark
|
||||||
|
export function getTestNavMapResponse(): Response {
|
||||||
|
|
||||||
|
const navMapJson = { "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.",
|
||||||
|
"hide": true // <----- SHOULD BE FILTERED OUT
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"navTitle": "Tutorial",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"navTitle": "Core",
|
||||||
|
"tooltip": "Learn the core capabilities of Angular",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"docId": "guide/NgModule",
|
||||||
|
"navTitle": "Angular Modules (NgModule)",
|
||||||
|
"tooltip": "Define application modules with @NgModule."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "Empty Heading",
|
||||||
|
"children": [ ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"navTitle": "External",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"url": "https://gitter.im/angular/angular",
|
||||||
|
"navTitle": "Gitter",
|
||||||
|
"tooltip": "Chat about Angular with other birds of a feather"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]};
|
||||||
|
|
||||||
|
// tslint:enable:quotemark
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
json: () => navMapJson
|
||||||
|
} as Response;
|
||||||
|
}
|
|
@ -190,4 +190,4 @@ function createDoc(content, relativePath, docType) {
|
||||||
docType: docType || 'example-file',
|
docType: docType || 'example-file',
|
||||||
startingLine: 1
|
startingLine: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue