Skip to content

Commit

Permalink
feat(markdown): added contentReady event binding. (closes #563) (#536)
Browse files Browse the repository at this point in the history
feat(markdown): added contentReady event binding. (closes #563) (#536)
  • Loading branch information
stevenov7 authored and emoralesb05 committed May 5, 2017
1 parent 11c3d15 commit cdf6cad
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 93 deletions.
1 change: 1 addition & 0 deletions src/platform/markdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
311 changes: 219 additions & 92 deletions src/platform/markdown/markdown.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
TestBed,
inject,
async,
ComponentFixture,
} from '@angular/core/testing';
Expand All @@ -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<any> = 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<any> = 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<any> = TestBed.createComponent(TdMarkdownBasicTestComponent);
let component: TdMarkdownBasicTestComponent = fixture.debugElement.componentInstance;
let element: HTMLElement = fixture.nativeElement;
let fixture: ComponentFixture<any> = 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<any> = 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<any> = 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<any> = 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<any> = 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<any> = 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<any> = 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<any> = 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<any> = 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: `
<td-markdown>
</td-markdown>`,
})
class TdMarkdownEmptyStaticContentTestRenderingComponent { }

@Component({
template: `
<td-markdown>
# title
* list item
</td-markdown>`,
})
class TdMarkdownStaticContentTestRenderingComponent { }

@Component({
template: `
<td-markdown [content]="content">
</td-markdown>`,
})
class TdMarkdownDymanicContentTestRenderingComponent {
content: string;
}

// Use the 3 components below to test event binding requirements of the TdMarkdown component.
@Component({
template: `
<td-markdown>
</td-markdown>`,
<td-markdown (contentReady)="tdMarkdownContentIsReady()">
</td-markdown>`,
})
class TdMarkdownEmptyTestComponent {
class TdMarkdownEmptyStaticContentTestEventsComponent {
tdMarkdownContentIsReady(): void { /* Stub */ }
}

@Component({
template: `
<td-markdown>
# title
<td-markdown (contentReady)="tdMarkdownContentIsReady()">
# title
* list item
</td-markdown>`,
* list item
</td-markdown>`,
})
class TdMarkdownBasicTestComponent {
class TdMarkdownStaticContentTestEventsComponent {
tdMarkdownContentIsReady(): void { /* Stub */ }
}

@Component({
template: `
<td-markdown [content]="content">
</td-markdown>`,
<td-markdown [content]="content" (contentReady)="tdMarkdownContentIsReady()">
</td-markdown>`,
})
class TdMarkdownContentTestComponent {
class TdMarkdownDynamicContentTestEventsComponent {
content: string;
tdMarkdownContentIsReady(): void { /* Stub */ }
}
9 changes: 8 additions & 1 deletion src/platform/markdown/markdown.component.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<undefined> = new EventEmitter<undefined>();

constructor(private _renderer: Renderer2,
private _elementRef: ElementRef,
private _domSanitizer: DomSanitizer) {}
Expand All @@ -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 {
Expand Down

0 comments on commit cdf6cad

Please sign in to comment.