diff --git a/src/platform/markdown/README.md b/src/platform/markdown/README.md index 4423d3cc83..9aebae0902 100644 --- a/src/platform/markdown/README.md +++ b/src/platform/markdown/README.md @@ -9,6 +9,7 @@ Methods: | Name | Type | Description | | --- | --- | --- | | `content` | `string` | Markdown format content to be parsed as html markup. Used to load data dynamically. e.g. `README.md` content. +| `contentReady` | `function` | Event emitted after the markdown content rendering is finished. **Note:** This module uses the **DomSanitizer** service to ~sanitize~ the parsed `html` from the `showdown` lib to avoid **XSS** issues. diff --git a/src/platform/markdown/markdown.component.spec.ts b/src/platform/markdown/markdown.component.spec.ts index 6b01c37a79..cf006eb022 100644 --- a/src/platform/markdown/markdown.component.spec.ts +++ b/src/platform/markdown/markdown.component.spec.ts @@ -1,6 +1,5 @@ import { TestBed, - inject, async, ComponentFixture, } from '@angular/core/testing'; @@ -17,128 +16,256 @@ describe('Component: Markdown', () => { CovalentMarkdownModule, ], declarations: [ - TdMarkdownEmptyTestComponent, - TdMarkdownBasicTestComponent, - TdMarkdownContentTestComponent, + TdMarkdownEmptyStaticContentTestRenderingComponent, + TdMarkdownStaticContentTestRenderingComponent, + TdMarkdownDymanicContentTestRenderingComponent, + + TdMarkdownEmptyStaticContentTestEventsComponent, + TdMarkdownStaticContentTestEventsComponent, + TdMarkdownDynamicContentTestEventsComponent, ], }); TestBed.compileComponents(); })); - it('should render empty', async(inject([], () => { + describe('Rendering: ', () => { - let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownEmptyTestComponent); - let component: TdMarkdownEmptyTestComponent = fixture.debugElement.componentInstance; - let element: HTMLElement = fixture.nativeElement; + it('should render empty static content', async(() => { - expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) - .toBe(``); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownEmptyStaticContentTestRenderingComponent); + let component: TdMarkdownEmptyStaticContentTestRenderingComponent = fixture.debugElement.componentInstance; + let element: HTMLElement = fixture.nativeElement; + + expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) + .toBe(``); expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); - expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()).toBe(''); - }); - }))); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); + expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()).toBe(''); + }); + })); - it('should render markup from static content', async(inject([], () => { + it('should render markup from static content', async(() => { - let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownBasicTestComponent); - let component: TdMarkdownBasicTestComponent = fixture.debugElement.componentInstance; - let element: HTMLElement = fixture.nativeElement; + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownStaticContentTestRenderingComponent); + let component: TdMarkdownStaticContentTestRenderingComponent = fixture.debugElement.componentInstance; + let element: HTMLElement = fixture.nativeElement; - expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) - .toBe(` - # title + expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) + .toBe(` + # title - * list item`.trim()); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { + * list item`.trim()); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); - expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('title'); - expect(element.querySelector('td-markdown div ul li').textContent.trim()).toBe('list item'); - }); - }))); - - it('should render markup from dynamic content', async(inject([], () => { - - let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownContentTestComponent); - let component: TdMarkdownContentTestComponent = fixture.debugElement.componentInstance; - component.content = ` - # another title - - ## subtitle - - \`\`\` - pseudo code - \`\`\``; - let element: HTMLElement = fixture.nativeElement; - - expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) - .toBe(''); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); + expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('title'); + expect(element.querySelector('td-markdown div ul li').textContent.trim()).toBe('list item'); + }); + })); + + it('should render markup from dynamic content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownDymanicContentTestRenderingComponent); + let component: TdMarkdownDymanicContentTestRenderingComponent = fixture.debugElement.componentInstance; + component.content = ` + # another title + + ## subtitle + + \`\`\` + pseudo code + \`\`\``; + let element: HTMLElement = fixture.nativeElement; + + expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) + .toBe(''); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); - expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('another title'); - expect(element.querySelector('td-markdown div h2').textContent.trim()).toBe('subtitle'); - expect(element.querySelector('td-markdown div code').textContent.trim()).toBe('pseudo code'); - }); - }))); - - it('should render markup from dynamic content incorrectly', async(inject([], () => { - - let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownContentTestComponent); - let component: TdMarkdownContentTestComponent = fixture.debugElement.componentInstance; - component.content = ` - # another title - - ## subtitle`; - let element: HTMLElement = fixture.nativeElement; - - expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) - .toBe(''); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); + expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('another title'); + expect(element.querySelector('td-markdown div h2').textContent.trim()).toBe('subtitle'); + expect(element.querySelector('td-markdown div code').textContent.trim()).toBe('pseudo code'); + }); + })); + + it('should render markup from dynamic content incorrectly', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownDymanicContentTestRenderingComponent); + let component: TdMarkdownDymanicContentTestRenderingComponent = fixture.debugElement.componentInstance; + component.content = ` + # another title + + ## subtitle`; + let element: HTMLElement = fixture.nativeElement; + + expect(fixture.debugElement.query(By.css('td-markdown')).nativeElement.textContent.trim()) + .toBe(''); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeFalsy(); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); - expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('another title'); - expect(element.querySelector('td-markdown div h2')).toBeFalsy(); - expect(element.querySelector('td-markdown div').textContent.trim()).toContain('## subtitle'); - }); - }))); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-markdown div'))).toBeTruthy(); + expect(element.querySelector('td-markdown div h1').textContent.trim()).toBe('another title'); + expect(element.querySelector('td-markdown div h2')).toBeFalsy(); + expect(element.querySelector('td-markdown div').textContent.trim()).toContain('## subtitle'); + }); + })); + }); + + describe('Event bindings: ', () => { + + describe('contentReady event: ', () => { + + it('should be fired only once after display renders empty static content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownEmptyStaticContentTestEventsComponent); + let component: TdMarkdownEmptyStaticContentTestEventsComponent = fixture.debugElement.componentInstance; + + let eventSpy: jasmine.Spy = spyOn(component, 'tdMarkdownContentIsReady'); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it('should be fired only once after display renders markup from static content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownStaticContentTestEventsComponent); + let component: TdMarkdownStaticContentTestEventsComponent = fixture.debugElement.componentInstance; + + let eventSpy: jasmine.Spy = spyOn(component, 'tdMarkdownContentIsReady'); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it('should be fired only once after display renders inital markup from dynamic content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownDynamicContentTestEventsComponent); + let component: TdMarkdownDynamicContentTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdMarkdownContentIsReady'); + + // Inital dynamic content + component.content = ` + # another title + + ## subtitle + + \`\`\` + pseudo code + \`\`\``; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it(`should be fired twice after changing the inital rendered markup dynamic content`, async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdMarkdownDynamicContentTestEventsComponent); + let component: TdMarkdownDynamicContentTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdMarkdownContentIsReady'); + + component.content = ` + # another title + + ## subtitle + + \`\`\` + pseudo code + \`\`\``; + + fixture.detectChanges(); + + component.content = ` + # changed title + + ## changed subtitle + + \`\`\` + changed pseudo code + \`\`\``; + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(2); + }); + })); + }); + }); }); +// Use the 3 components below to test the rendering requirements of the TdMarkdown component. +@Component({ + template: ` + + `, +}) +class TdMarkdownEmptyStaticContentTestRenderingComponent { } + +@Component({ + template: ` + + # title + + * list item + `, +}) +class TdMarkdownStaticContentTestRenderingComponent { } + +@Component({ + template: ` + + `, +}) +class TdMarkdownDymanicContentTestRenderingComponent { + content: string; +} + +// Use the 3 components below to test event binding requirements of the TdMarkdown component. @Component({ template: ` - - `, + + `, }) -class TdMarkdownEmptyTestComponent { +class TdMarkdownEmptyStaticContentTestEventsComponent { + tdMarkdownContentIsReady(): void { /* Stub */ } } @Component({ template: ` - - # title + + # title - * list item - `, + * list item + `, }) -class TdMarkdownBasicTestComponent { +class TdMarkdownStaticContentTestEventsComponent { + tdMarkdownContentIsReady(): void { /* Stub */ } } @Component({ template: ` - - `, + + `, }) -class TdMarkdownContentTestComponent { +class TdMarkdownDynamicContentTestEventsComponent { content: string; + tdMarkdownContentIsReady(): void { /* Stub */ } } diff --git a/src/platform/markdown/markdown.component.ts b/src/platform/markdown/markdown.component.ts index 5bfd56cfd0..c5079a538b 100644 --- a/src/platform/markdown/markdown.component.ts +++ b/src/platform/markdown/markdown.component.ts @@ -1,4 +1,4 @@ -import { Component, AfterViewInit, ElementRef, Input, Renderer2, SecurityContext } from '@angular/core'; +import { Component, AfterViewInit, ElementRef, Input, Output, EventEmitter, Renderer2, SecurityContext } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; declare var showdown: any; @@ -26,6 +26,12 @@ export class TdMarkdownComponent implements AfterViewInit { this._loadContent(this._content); } + /** + * contentReady?: function + * Event emitted after the markdown content rendering is finished. + */ + @Output('contentReady') onContentReady: EventEmitter = new EventEmitter(); + constructor(private _renderer: Renderer2, private _elementRef: ElementRef, private _domSanitizer: DomSanitizer) {} @@ -46,6 +52,7 @@ export class TdMarkdownComponent implements AfterViewInit { // Parse html string into actual HTML elements. let divElement: HTMLDivElement = this._elementFromString(this._render(markdown)); } + this.onContentReady.emit(); } private _elementFromString(markupStr: string): HTMLDivElement {