diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 94e29dbedb..4b5ca7cb42 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -207,7 +207,7 @@ describe('AppComponent', () => { describe('pageId', () => { - it('should set the id of the doc viewer container based on the current url', () => { + it('should set the id of the doc viewer container based on the current doc', () => { const container = fixture.debugElement.query(By.css('section.sidenav-content')); locationService.go('guide/pipes'); @@ -226,7 +226,7 @@ describe('AppComponent', () => { expect(container.properties['id']).toEqual('home'); }); - it('should not be affected by changes to the query or hash', () => { + it('should not be affected by changes to the query', () => { const container = fixture.debugElement.query(By.css('section.sidenav-content')); locationService.go('guide/pipes'); @@ -236,11 +236,6 @@ describe('AppComponent', () => { fixture.detectChanges(); expect(component.pageId).toEqual('guide-other'); expect(container.properties['id']).toEqual('guide-other'); - - locationService.go('guide/http#anchor-1'); - fixture.detectChanges(); - expect(component.pageId).toEqual('guide-http'); - expect(container.properties['id']).toEqual('guide-http'); }); }); @@ -261,7 +256,7 @@ describe('AppComponent', () => { it('should display a marketing page', () => { locationService.go('features'); fixture.detectChanges(); - expect(docViewer.innerText).toMatch(/Test Doc/i); + expect(docViewer.innerText).toMatch(/Features Doc/i); }); it('should update the document title', () => { @@ -275,7 +270,7 @@ describe('AppComponent', () => { it('should update the document title, with a default value if the document has no title', () => { const titleService = TestBed.get(Title); spyOn(titleService, 'setTitle'); - locationService.go('file-not-found'); + locationService.go('no-title'); fixture.detectChanges(); expect(titleService.setTitle).toHaveBeenCalledWith('Angular'); }); @@ -431,35 +426,20 @@ class TestHttp { } }; - apiDoc = { - "title": "API", - "contents": "

API Doc

" - }; - - pipesDoc = { - "title": "Pipes", - "contents": "

Pipes Doc

" - }; - - testDoc = { - "title": "Test", - "contents": "

Test Doc

" - }; - - fileNotFoundDoc = { - "title": "", - "contents": "Page not found" - }; - - // get = jasmine.createSpy('get').and.callFake((url: string) => { ... }); get(url: string) { - const json = - /navigation.json/.test(url) ? this.navJson : - /api/.test(url) ? this.apiDoc : - /pipes/.test(url) ? this.pipesDoc : - /file-not-found/.test(url) ? this.fileNotFoundDoc : - this.testDoc; - return of({ json: () => json }); + let data; + if (/navigation\.json/.test(url)) { + data = this.navJson; + } else { + const match = /content\/docs\/(.+)\.json/.exec(url); + const id = match[1]; + const title = id.split('/').pop().replace(/^([a-z])/, (_, letter) => letter.toUpperCase()); + const contents = `

${title} Doc

`; + data = { id, title, contents }; + if (id === 'no-title') { + data.title = ''; + } + } + return of({ json: () => data }); } - } diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index f3c263c8e6..eda9502d5b 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -72,12 +72,7 @@ export class AppComponent implements OnInit { this.documentService.currentDocument.subscribe(doc => { this.currentDocument = doc; this.setDocumentTitle(doc.title); - this.pageId = this.currentDocument.url.replace('/', '-'); - - // Special case the home page - if (this.pageId === 'index') { - this.pageId = 'home'; - } + this.setPageId(doc.id); }); // scroll even if only the hash fragment changed @@ -151,11 +146,16 @@ export class AppComponent implements OnInit { this.sidenav.toggle(value); } - setDocumentTitle(title) { + setDocumentTitle(title: string) { if (title.trim()) { this.titleService.setTitle(`Angular - ${title}`); } else { this.titleService.setTitle('Angular'); } } + + setPageId(id: string) { + // Special case the home page + this.pageId = (id === 'index') ? 'home' : id.replace('/', '-'); + } } diff --git a/aio/src/app/documents/document-contents.ts b/aio/src/app/documents/document-contents.ts index 53471eca07..a4ac90e008 100644 --- a/aio/src/app/documents/document-contents.ts +++ b/aio/src/app/documents/document-contents.ts @@ -1,5 +1,8 @@ export interface DocumentContents { - url: string; + /** The unique identifier for this document */ + id: string; + /** The string to display in the browser tab when this document is being viewed */ title: string; + /** The HTML to display in the doc viewer */ contents: string; } diff --git a/aio/src/app/documents/document.service.spec.ts b/aio/src/app/documents/document.service.spec.ts index a7702b6d34..296a55878d 100644 --- a/aio/src/app/documents/document.service.spec.ts +++ b/aio/src/app/documents/document.service.spec.ts @@ -86,15 +86,14 @@ describe('DocumentService', () => { connections[0].mockError(new Response(new ResponseOptions({ status: 404, statusText: 'NOT FOUND'})) as any); expect(connections.length).toEqual(2); expect(connections[1].request.url).toEqual(CONTENT_URL_PREFIX + 'file-not-found.json'); - const fileNotFoundDoc = { title: 'File Not Found' }; + const fileNotFoundDoc = { id: 'file-not-found', title: 'Page Not Found', contents: '

Page Not Found

' }; connections[1].mockRespond(createResponse(fileNotFoundDoc)); - expect(currentDocument).toEqual(jasmine.objectContaining(fileNotFoundDoc)); - expect(currentDocument.url).toEqual('file-not-found'); + expect(currentDocument).toEqual(fileNotFoundDoc); }); it('should emit a hard-coded not-found document if the not-found document is not found on the server', () => { let currentDocument: DocumentContents; - const notFoundDoc: DocumentContents = { title: 'Not Found', contents: 'Document not found', url: 'file-not-found' }; + const notFoundDoc: DocumentContents = { title: 'Not Found', contents: 'Document not found', id: 'file-not-found' }; const nextDoc = { title: 'Next Doc', url: 'new/url' }; const { docService, backend, locationService } = getServices('file-not-found'); const connections = backend.connectionsArray; @@ -178,7 +177,7 @@ describe('DocumentService', () => { }); it('should map the "folder" locations to the correct document request', () => { - const { docService, backend, locationService} = getServices('guide/'); + const { docService, backend, locationService} = getServices('guide'); docService.currentDocument.subscribe(); expect(backend.connectionsArray[0].request.url).toEqual(CONTENT_URL_PREFIX + 'guide.json'); diff --git a/aio/src/app/documents/document.service.ts b/aio/src/app/documents/document.service.ts index 3b6882cb9d..94e72bdd50 100644 --- a/aio/src/app/documents/document.service.ts +++ b/aio/src/app/documents/document.service.ts @@ -15,6 +15,7 @@ import { LocationService } from 'app/shared/location.service'; import { Logger } from 'app/shared/logger.service'; const FILE_NOT_FOUND_URL = 'file-not-found'; +const FETCHING_ERROR_URL = 'fetching-error'; @Injectable() export class DocumentService { @@ -28,50 +29,52 @@ export class DocumentService { private http: Http, location: LocationService) { // Whenever the URL changes we try to get the appropriate doc - this.currentDocument = location.currentUrl.switchMap(url => this.getDocument(url)); + this.currentDocument = location.currentUrl.switchMap(() => this.getDocument(location.path())); } private getDocument(url: string) { - this.logger.log('getting document', url); - url = this.cleanUrl(url); - if ( !this.cache.has(url)) { - this.cache.set(url, this.fetchDocument(url)); + const id = url || 'index'; + this.logger.log('getting document', id); + if ( !this.cache.has(id)) { + this.cache.set(id, this.fetchDocument(id)); } - return this.cache.get(url); + return this.cache.get(id); } - private fetchDocument(url: string) { - this.logger.log('fetching document from', url); + private fetchDocument(id: string): Observable { + const requestPath = `content/docs/${id}.json`; + this.logger.log('fetching document from', requestPath); const subject = new AsyncSubject(); this.http - .get(`content/docs/${url}.json`) - // Add the document's url to the DocumentContents provided to the rest of the app - .map(res => Object.assign(res.json(), { url }) as DocumentContents) + .get(requestPath) + .map(response => response.json()) .catch((error: Response) => { - if (error.status === 404) { - if (url !== FILE_NOT_FOUND_URL) { - this.logger.error(`Document file not found at '${url}'`); - // using `getDocument` means that we can fetch the 404 doc contents from the server and cache it - return this.getDocument(FILE_NOT_FOUND_URL); - } else { - return of({ title: 'Not Found', contents: 'Document not found', url: FILE_NOT_FOUND_URL }); - } - } else { - this.logger.error('Error fetching document', error); - return Observable.of({ title: 'Error fetching document', contents: 'Sorry we were not able to fetch that document.', url}); - } + return error.status === 404 ? this.getFileNotFoundDoc(id) : this.getErrorDoc(error); }) .subscribe(subject); return subject.asObservable(); } - private cleanUrl(url: string) { - url = url.match(/[^#?]*/)[0]; // strip off fragment and query - url = url.replace(/\/$/, ''); // strip off trailing slash - if (url === '') { - // deal with root url - url = 'index'; + private getFileNotFoundDoc(url: string): Observable { + if (url !== FILE_NOT_FOUND_URL) { + this.logger.error(`Document file not found at '${url}'`); + // using `getDocument` means that we can fetch the 404 doc contents from the server and cache it + return this.getDocument(FILE_NOT_FOUND_URL); + } else { + return of({ + title: 'Not Found', + contents: 'Document not found', + id: FILE_NOT_FOUND_URL + }); } - return url; + } + + private getErrorDoc(error: Response): Observable { + this.logger.error('Error fetching document', error); + return Observable.of({ + title: 'Error fetching document', + contents: 'Sorry we were not able to fetch that document.', + id: FETCHING_ERROR_URL + }); } } diff --git a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts index 5111f5ee15..4e8bf3031f 100644 --- a/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts +++ b/aio/src/app/layout/doc-viewer/doc-viewer.component.spec.ts @@ -122,23 +122,23 @@ describe('DocViewerComponent', () => { }); it(('should display nothing when set currentDoc has no content'), () => { - component.currentDoc = { title: 'fake title', contents: '', url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents: '', id: 'a/b' }; fixture.detectChanges(); expect(docViewerEl.innerHTML).toBe(''); }); it(('should display simple static content doc'), () => { const contents = '

Howdy, doc viewer

'; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; fixture.detectChanges(); expect(docViewerEl.innerHTML).toEqual(contents); }); it(('should display nothing after reset static content doc'), () => { const contents = '

Howdy, doc viewer

'; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; fixture.detectChanges(); - component.currentDoc = { title: 'fake title', contents: '', url: 'a/c' }; + component.currentDoc = { title: 'fake title', contents: '', id: 'a/c' }; fixture.detectChanges(); expect(docViewerEl.innerHTML).toEqual(''); }); @@ -149,7 +149,7 @@ describe('DocViewerComponent', () => {

Below Foo

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; fixture.detectChanges(); const fooHtml = docViewerEl.querySelector('aio-foo').innerHTML; expect(fooHtml).toContain('Foo Component'); @@ -165,7 +165,7 @@ describe('DocViewerComponent', () => {

Below Foo

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; fixture.detectChanges(); const foos = docViewerEl.querySelectorAll('aio-foo'); expect(foos.length).toBe(2); @@ -177,7 +177,7 @@ describe('DocViewerComponent', () => {

Below Bar

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; fixture.detectChanges(); const barHtml = docViewerEl.querySelector('aio-bar').innerHTML; expect(barHtml).toContain('Bar Component'); @@ -189,7 +189,7 @@ describe('DocViewerComponent', () => { ###bar content###

Below Bar

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; // necessary to trigger projection within ngOnInit fixture.detectChanges(); @@ -207,7 +207,7 @@ describe('DocViewerComponent', () => {

Bottom

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; // necessary to trigger Bar's projection within ngOnInit fixture.detectChanges(); @@ -230,7 +230,7 @@ describe('DocViewerComponent', () => {

Bottom

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; // necessary to trigger Bar's projection within ngOnInit fixture.detectChanges(); @@ -254,7 +254,7 @@ describe('DocViewerComponent', () => {

Bottom

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; // necessary to trigger Bar's projection within ngOnInit fixture.detectChanges(); @@ -282,7 +282,7 @@ describe('DocViewerComponent', () => {

---More baz--

Bottom

`; - component.currentDoc = { title: 'fake title', contents, url: 'a/b' }; + component.currentDoc = { title: 'fake title', contents, id: 'a/b' }; // necessary to trigger Bar's projection within ngOnInit fixture.detectChanges();