From b3e800cb5342722a50a97292bc81ea8282d3659e Mon Sep 17 00:00:00 2001 From: Steven Ov Date: Sat, 6 May 2017 08:57:32 -0700 Subject: [PATCH] feat(highlight): added contentReady event binding. (closes #553) (#560) * feat(highlight): added contentReady event binding. (closes #553) * chore(): rename property to onContentReady but kept API as contentReady * chore(): ninja fix to unit test title in paging bar --- .../core/paging/paging-bar.component.spec.ts | 2 +- src/platform/highlight/README.md | 1 + .../highlight/highlight.component.spec.ts | 259 +++++++++++++----- src/platform/highlight/highlight.component.ts | 9 +- 4 files changed, 205 insertions(+), 66 deletions(-) diff --git a/src/platform/core/paging/paging-bar.component.spec.ts b/src/platform/core/paging/paging-bar.component.spec.ts index c8233cc832..1309bf3e16 100644 --- a/src/platform/core/paging/paging-bar.component.spec.ts +++ b/src/platform/core/paging/paging-bar.component.spec.ts @@ -12,7 +12,7 @@ import { CovalentPagingModule } from './paging.module'; import { NgModule, DebugElement } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -describe('Component: TdPagingBarComponent', () => { +describe('Component: PagingBar', () => { beforeEach(async(() => { TestBed.configureTestingModule({ diff --git a/src/platform/highlight/README.md b/src/platform/highlight/README.md index 49bcc63e99..9fefa27cf5 100644 --- a/src/platform/highlight/README.md +++ b/src/platform/highlight/README.md @@ -12,6 +12,7 @@ Properties: | --- | --- | --- | | `lang` | `[any common language supported in highlight.js]` | Language of the code content to be parsed as highlighted html. | `content` | `string` | Code content to be parsed as highlighted html. Used to load data dynamically. e.g. `.ts` content. +| `contentReady` | `function` | Event emitted after the highlight content rendering is finished. **Note:** This module uses the **DomSanitizer** service to ~sanitize~ the parsed `html` from the `highlight.js` lib to avoid **XSS** issues. diff --git a/src/platform/highlight/highlight.component.spec.ts b/src/platform/highlight/highlight.component.spec.ts index c5fbf562e1..3c1b5d3189 100644 --- a/src/platform/highlight/highlight.component.spec.ts +++ b/src/platform/highlight/highlight.component.spec.ts @@ -1,6 +1,5 @@ import { TestBed, - inject, async, ComponentFixture, } from '@angular/core/testing'; @@ -17,88 +16,176 @@ describe('Component: Highlight', () => { CovalentHighlightModule, ], declarations: [ - TdHighlightEmptyTestComponent, - TdHighlightStaticHtmlTestComponent, - TdHighlightDynamicCssTestComponent, - TdHighlightUndefinedLangTestComponent, + TdHighlightEmptyStaticTestRenderingComponent, + TdHighlightStaticHtmlTestRenderingComponent, + TdHighlightDynamicCssTestRenderingComponent, + TdHighlightUndefinedLangTestRenderingComponent, + + TdHighlightEmptyStaticTestEventsComponent, + TdHighlightStaticHtmlTestEventsComponent, + TdHighlightDynamicCssTestEventsComponent, + TdHighlightUndefinedLangTestEventsComponent, ], }); TestBed.compileComponents(); })); - it('should render empty', async(inject([], () => { + describe('Rendering: ', () => { - let fixture: ComponentFixture = TestBed.createComponent(TdHighlightEmptyTestComponent); - let component: TdHighlightEmptyTestComponent = fixture.debugElement.componentInstance; - let element: HTMLElement = fixture.nativeElement; + it('should render empty', async(() => { - expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) - .toBe(``); - expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightEmptyStaticTestRenderingComponent); + let component: TdHighlightEmptyStaticTestRenderingComponent = fixture.debugElement.componentInstance; + let element: HTMLElement = fixture.nativeElement; + + expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) + .toBe(``); expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); - expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()).toBe(''); - }); - }))); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); + expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()).toBe(''); + }); + })); - it('should render code from static content', async(inject([], () => { + it('should render code from static content', async(() => { - let fixture: ComponentFixture = TestBed.createComponent(TdHighlightStaticHtmlTestComponent); - let component: TdHighlightStaticHtmlTestComponent = fixture.debugElement.componentInstance; - let element: HTMLElement = fixture.nativeElement; + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightStaticHtmlTestRenderingComponent); + let component: TdHighlightStaticHtmlTestRenderingComponent = fixture.debugElement.componentInstance; + let element: HTMLElement = fixture.nativeElement; - expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) - .toContain(`{ {property} }`.trim()); - expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeTruthy(); - expect(element.querySelector('td-highlight pre code').textContent.trim()).toContain(`{{property}}`); - expect(element.querySelectorAll('.hljs-tag').length).toBe(6); - }); - }))); - - it('should render code from dynamic content', async(inject([], () => { - - let fixture: ComponentFixture = TestBed.createComponent(TdHighlightDynamicCssTestComponent); - let component: TdHighlightDynamicCssTestComponent = fixture.debugElement.componentInstance; - component.content = ` - pre { - background: #002451; - border-radius: 2px; - }`; - let element: HTMLElement = fixture.nativeElement; - - expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) - .toBe(''); - expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); - fixture.detectChanges(); - fixture.whenStable().then(() => { + expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) + .toContain(`{ {property} }`.trim()); + expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeTruthy(); - expect(element.querySelectorAll('.hljs-number').length).toBe(2); - }); - }))); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeTruthy(); + expect(element.querySelector('td-highlight pre code').textContent.trim()).toContain(`{{property}}`); + expect(element.querySelectorAll('.hljs-tag').length).toBe(6); + }); + })); + + it('should render code from dynamic content', async(() => { - it('should throw error for undefined language', async(inject([], () => { - let fixture: ComponentFixture = TestBed.createComponent(TdHighlightUndefinedLangTestComponent); - let component: TdHighlightUndefinedLangTestComponent = fixture.debugElement.componentInstance; - expect(function(): void { + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightDynamicCssTestRenderingComponent); + let component: TdHighlightDynamicCssTestRenderingComponent = fixture.debugElement.componentInstance; + component.content = ` + pre { + background: #002451; + border-radius: 2px; + }`; + let element: HTMLElement = fixture.nativeElement; + + expect(fixture.debugElement.query(By.css('td-highlight')).nativeElement.textContent.trim()) + .toBe(''); + expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeFalsy(); fixture.detectChanges(); - }).toThrowError(); - }))); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('td-highlight pre code'))).toBeTruthy(); + expect(element.querySelectorAll('.hljs-number').length).toBe(2); + }); + })); + + it('should throw error for undefined language', async(() => { + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightUndefinedLangTestRenderingComponent); + let component: TdHighlightUndefinedLangTestRenderingComponent = fixture.debugElement.componentInstance; + expect(function(): void { + fixture.detectChanges(); + }).toThrowError(); + })); + }); + + describe('Event bindings: ', () => { + + describe('contentReady event: ', () => { + + it('should be fired only once after display renders empty static content', async(() => { + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightEmptyStaticTestEventsComponent); + let component: TdHighlightEmptyStaticTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdHighlightContentIsReady'); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it('should be fired only once after display renders highlight from static html', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightStaticHtmlTestEventsComponent); + let component: TdHighlightStaticHtmlTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdHighlightContentIsReady'); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it('should be fired only once after display renders inital highlight from dynamic css content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightDynamicCssTestEventsComponent); + let component: TdHighlightDynamicCssTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdHighlightContentIsReady'); + + // Inital dynamic css content + component.content = ` + pre { + background: #002451; + border-radius: 2px; + }`; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(1); + }); + })); + + it('should be fired twice after changing the inital rendered highlight dynamic css content', async(() => { + + let fixture: ComponentFixture = TestBed.createComponent(TdHighlightDynamicCssTestEventsComponent); + let component: TdHighlightDynamicCssTestEventsComponent = fixture.debugElement.componentInstance; + let eventSpy: jasmine.Spy = spyOn(component, 'tdHighlightContentIsReady'); + + component.content = ` + pre { + background: #002451; + border-radius: 2px; + }`; + + fixture.detectChanges(); + + component.content = ` + pre { + color: red; + background: #000000; + border-radius: 1em; + }`; + + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(eventSpy.calls.count()).toBe(2); + }); + })); + }); + }); }); +// Use the 4 components below to test the rendering requirements of the TdHighlight component. @Component({ template: ` `, }) -class TdHighlightEmptyTestComponent { +class TdHighlightEmptyStaticTestRenderingComponent { } @Component({ @@ -111,7 +198,7 @@ class TdHighlightEmptyTestComponent { ]]> `, }) -class TdHighlightStaticHtmlTestComponent { +class TdHighlightStaticHtmlTestRenderingComponent { } @Component({ @@ -119,7 +206,7 @@ class TdHighlightStaticHtmlTestComponent { `, }) -class TdHighlightDynamicCssTestComponent { +class TdHighlightDynamicCssTestRenderingComponent { content: string; } @@ -128,6 +215,50 @@ class TdHighlightDynamicCssTestComponent { `, }) -class TdHighlightUndefinedLangTestComponent { +class TdHighlightUndefinedLangTestRenderingComponent { + lang: string; +} + +// Use the 4 components below to test event binding requirements of the TdHighlight component. +@Component({ + template: ` + + `, +}) +class TdHighlightEmptyStaticTestEventsComponent { + tdHighlightContentIsReady(): void { /* Stub */ } +} + +@Component({ + template: ` + +

hello world!

+ { {property} } +
+ ]]> + `, +}) +class TdHighlightStaticHtmlTestEventsComponent { + tdHighlightContentIsReady(): void { /* Stub */ } +} + +@Component({ + template: ` + + `, +}) +class TdHighlightDynamicCssTestEventsComponent { + content: string; + tdHighlightContentIsReady(): void { /* Stub */ } +} + +@Component({ + template: ` + + `, +}) +class TdHighlightUndefinedLangTestEventsComponent { lang: string; + tdHighlightContentIsReady(): void { /* Stub */ } } diff --git a/src/platform/highlight/highlight.component.ts b/src/platform/highlight/highlight.component.ts index 070bb20e51..178962b638 100644 --- a/src/platform/highlight/highlight.component.ts +++ b/src/platform/highlight/highlight.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'; /* tslint:disable-next-line */ let hljs: any = require('highlight.js/lib'); @@ -36,6 +36,12 @@ export class TdHighlightComponent implements AfterViewInit { */ @Input('lang') language: string = 'typescript'; + /** + * contentReady?: function + * Event emitted after the highlight content rendering is finished. + */ + @Output('contentReady') onContentReady: EventEmitter = new EventEmitter(); + constructor(private _renderer: Renderer2, private _elementRef: ElementRef, private _domSanitizer: DomSanitizer) {} @@ -58,6 +64,7 @@ export class TdHighlightComponent implements AfterViewInit { // Parse html string into actual HTML elements. let preElement: HTMLPreElement = this._elementFromString(this._render(code)); } + this.onContentReady.emit(); } private _elementFromString(codeStr: string): HTMLPreElement {