diff --git a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.html b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.html new file mode 100644 index 0000000000..bc011fe9a2 --- /dev/null +++ b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.html @@ -0,0 +1 @@ + diff --git a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.scss b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.ts b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.ts new file mode 100644 index 0000000000..53fa4d3cf0 --- /dev/null +++ b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { IMarkdownNavigatorItem } from '@covalent/markdown-navigator'; + +@Component({ + selector: 'markdown-navigator-demo-children-url', + styleUrls: ['./markdown-navigator-demo-children-url.component.scss'], + templateUrl: './markdown-navigator-demo-children-url.component.html', +}) +export class MarkdownNavigatorDemoChildrenUrlComponent { + items: IMarkdownNavigatorItem[] = [ + { + title: '🔥', + childrenUrl: 'https://api.myjson.com/bins/aqnfk', + }, + ]; +} diff --git a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.component.html b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.component.html index 581c76a8e1..8b92c3829e 100644 --- a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.component.html +++ b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.component.html @@ -9,3 +9,7 @@ + + + + diff --git a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.module.ts b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.module.ts index e1d6573240..9079d56592 100644 --- a/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.module.ts +++ b/src/app/content/components/component-demos/markdown-navigator/demos/markdown-navigator-demo.module.ts @@ -13,6 +13,7 @@ import { } from './markdown-navigator-demo-footer/markdown-navigator-demo-footer.component'; import { MatListModule } from '@angular/material/list'; import { MarkdownNavigatorDemoEventsComponent } from './markdown-navigator-demo-events/markdown-navigator-demo-events.component'; +import { MarkdownNavigatorDemoChildrenUrlComponent } from './markdown-navigator-demo-children-url/markdown-navigator-demo-children-url.component'; @NgModule({ declarations: [ @@ -22,6 +23,7 @@ import { MarkdownNavigatorDemoEventsComponent } from './markdown-navigator-demo- MarkdownNavigatorDemoFooterExampleAComponent, MarkdownNavigatorDemoFooterExampleBComponent, MarkdownNavigatorDemoEventsComponent, + MarkdownNavigatorDemoChildrenUrlComponent, ], imports: [ DemoModule, diff --git a/src/platform/markdown-navigator/README.md b/src/platform/markdown-navigator/README.md index da41a813ea..495afc0eeb 100644 --- a/src/platform/markdown-navigator/README.md +++ b/src/platform/markdown-navigator/README.md @@ -33,6 +33,7 @@ interface IMarkdownNavigatorItem { markdownString?: string; // raw markdown anchor?: string; children?: IMarkdownNavigatorItem[]; + childrenUrl?: string; description?: string; icon?: string; footer?: Type; diff --git a/src/platform/markdown-navigator/markdown-navigator.component.spec.ts b/src/platform/markdown-navigator/markdown-navigator.component.spec.ts index acf1c4ffef..095c5340df 100644 --- a/src/platform/markdown-navigator/markdown-navigator.component.spec.ts +++ b/src/platform/markdown-navigator/markdown-navigator.component.spec.ts @@ -10,6 +10,8 @@ import { By } from '@angular/platform-browser'; import { Component, DebugElement, Type } from '@angular/core'; import { CovalentMarkdownNavigatorModule } from './markdown-navigator.module'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; const RAW_MARKDOWN_HEADING: string = 'Heading'; const RAW_MARKDOWN: string = `# ${RAW_MARKDOWN_HEADING}`; @@ -79,6 +81,15 @@ const ITEMS_WITH_FOOTERS: IMarkdownNavigatorItem[] = [ }, ]; +const CHILDREN_URL: string = 'https://samplechildrenurl.com'; + +const ITEMS_WITH_CHILDREN_URL: IMarkdownNavigatorItem[] = [ + { + title: 'Children url', + childrenUrl: CHILDREN_URL, + }, +]; + export const DEEPLY_NESTED_TREE: IMarkdownNavigatorItem[] = [ { title: 'A', @@ -288,11 +299,17 @@ class TdMarkdownNavigatorTestComponent { } describe('MarkdownNavigatorComponent', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [TdMarkdownNavigatorTestComponent], - imports: [NoopAnimationsModule, CovalentMarkdownNavigatorModule], + imports: [NoopAnimationsModule, CovalentMarkdownNavigatorModule, HttpClientTestingModule], }).compileComponents(); + + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); })); it('should render empty state when an empty array is passed into items', async( @@ -361,6 +378,42 @@ describe('MarkdownNavigatorComponent', () => { }), )); + it('should fetch childrenUrl and render', async( + inject([], async () => { + const itemATitle: string = 'fetched item A'; + const itemBTitle: string = 'fetched item B'; + + const testData: IMarkdownNavigatorItem[] = [{ title: itemATitle }, { title: itemBTitle }]; + const fixture: ComponentFixture = TestBed.createComponent( + TdMarkdownNavigatorTestComponent, + ); + + fixture.componentInstance.items = ITEMS_WITH_CHILDREN_URL; + await wait(fixture); + + const markdownNavigator: TdMarkdownNavigatorComponent = fixture.debugElement.query( + By.directive(TdMarkdownNavigatorComponent), + ).componentInstance; + + expect(markdownNavigator.showMenu).toBeTruthy(); + expect(markdownNavigator.showTdMarkdown).toBeFalsy(); + + getItem(fixture, 0).click(); + const req: TestRequest = httpTestingController.expectOne(CHILDREN_URL); + req.flush(testData); + + await wait(fixture); + await wait(fixture); + + expect(markdownNavigator.showMenu).toBeTruthy(); + expect(markdownNavigator.showTdMarkdown).toBeFalsy(); + expect(getItem(fixture, 0).textContent).toContain(itemATitle); + expect(getItem(fixture, 1).textContent).toContain(itemBTitle); + + httpTestingController.verify(); + }), + )); + it('should render one url item from GitHub', async( inject([], async () => { const fixture: ComponentFixture = TestBed.createComponent( diff --git a/src/platform/markdown-navigator/markdown-navigator.component.ts b/src/platform/markdown-navigator/markdown-navigator.component.ts index 235c2c8269..649965e23a 100644 --- a/src/platform/markdown-navigator/markdown-navigator.component.ts +++ b/src/platform/markdown-navigator/markdown-navigator.component.ts @@ -11,9 +11,12 @@ import { Type, Output, EventEmitter, + SecurityContext, } from '@angular/core'; import { removeLeadingHash, isAnchorLink, TdMarkdownLoaderService } from '@covalent/markdown'; import { ITdFlavoredMarkdownButtonClickEvent } from '@covalent/flavored-markdown'; +import { DomSanitizer } from '@angular/platform-browser'; +import { HttpClient } from '@angular/common/http'; export interface IMarkdownNavigatorItem { title?: string; @@ -22,6 +25,7 @@ export interface IMarkdownNavigatorItem { markdownString?: string; // raw markdown anchor?: string; children?: IMarkdownNavigatorItem[]; + childrenUrl?: string; description?: string; icon?: string; footer?: Type; @@ -145,6 +149,8 @@ export class TdMarkdownNavigatorComponent implements OnChanges { constructor( private _markdownUrlLoaderService: TdMarkdownLoaderService, private _changeDetectorRef: ChangeDetectorRef, + private _sanitizer: DomSanitizer, + private _http: HttpClient, ) {} @HostListener('click', ['$event']) @@ -245,9 +251,14 @@ export class TdMarkdownNavigatorComponent implements OnChanges { } } + hasChildrenOrChildrenUrl(item: IMarkdownNavigatorItem): boolean { + return (item.children && item.children.length > 0) || !!item.childrenUrl; + } + reset(): void { + this.loading = false; // if single item and no children - if (this.items && this.items.length === 1 && (!this.items[0].children || this.items[0].children.length === 0)) { + if (this.items && this.items.length === 1 && !this.hasChildrenOrChildrenUrl(this.items[0])) { this.currentMenuItems = []; this.currentMarkdownItem = this.items[0]; } else { @@ -259,10 +270,11 @@ export class TdMarkdownNavigatorComponent implements OnChanges { } goBack(): void { + this.loading = false; if (this.historyStack.length > 1) { const parent: IMarkdownNavigatorItem = this.historyStack[this.historyStack.length - 2]; this.currentMarkdownItem = parent; - this.currentMenuItems = parent.children; + this.setChildrenAsCurrentMenuItems(parent); this.historyStack = this.historyStack.slice(0, -1); } else { // one level down just go to root @@ -273,11 +285,42 @@ export class TdMarkdownNavigatorComponent implements OnChanges { handleItemSelected(item: IMarkdownNavigatorItem): void { this.historyStack = [...this.historyStack, item]; - this.currentMenuItems = item.children; + this.setChildrenAsCurrentMenuItems(item); this.currentMarkdownItem = item; this._changeDetectorRef.markForCheck(); } + async setChildrenAsCurrentMenuItems(item: IMarkdownNavigatorItem): Promise { + this.currentMenuItems = []; + this.loading = true; + this._changeDetectorRef.markForCheck(); + + const itemToVerifyWith: IMarkdownNavigatorItem = this.historyStack[0]; + let children: IMarkdownNavigatorItem[] = []; + if (item.children) { + children = item.children; + } else if (item.childrenUrl) { + children = await this.loadChildrenUrl(item); + } + if (this.historyStack[0] === itemToVerifyWith) { + this.currentMenuItems = children; + } + + this.loading = false; + this._changeDetectorRef.markForCheck(); + } + + async loadChildrenUrl(item: IMarkdownNavigatorItem): Promise { + const sanitizedUrl: string = this._sanitizer.sanitize(SecurityContext.URL, item.childrenUrl); + try { + return await this._http + .get(sanitizedUrl, { ...item.httpOptions }) + .toPromise(); + } catch { + return []; + } + } + getTitle(item: IMarkdownNavigatorItem): string { if (item) { return (