Skip to content

Commit

Permalink
feature(json-formatter): enhancements and bugfixes (closes #247) (#250)
Browse files Browse the repository at this point in the history
* add collapse animation to json-formatter

* use mdTooltip instead of `title`

* feature(json-formatter): Improved efficiency by changing its change detection to OnPush.

* bugfix(json-formatter): recreate children array so it doesnt append the new children data.

* limit preview tooltip to 80 characters

* update docs and README.md

* json-formatter unit test scaffolding

* remove lint issue

* code-health(json-formatter): added initial unit tests for json-formatter
  • Loading branch information
emoralesb05 authored and kyleledbetter committed Jan 15, 2017
1 parent 8015a67 commit da4db7f
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export class JsonFormatterDemoComponent {
description: `Levels opened by default when JS object is formatted and rendered.`,
name: 'levelsOpen?',
type: 'number',
}, {
description: `Refreshes json-formatter and rerenders [data]`,
name: 'refresh',
type: `function()`,
}];

data: Object = {
Expand Down
3 changes: 2 additions & 1 deletion src/platform/core/json-formatter/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# td-json-formatter

`td-json-formatter` renders a javascript object in Json format the same way the chrome/firefox console would render it using `console.log()`.
`td-json-formatter` renders a javascript object in JSON format the same way the chrome/firefox console would render it using `console.log()`.

Hovering on nodes will bring out a preview tooltip of the first 5 objects/properties of the node.

Expand All @@ -15,6 +15,7 @@ Properties:
| `key?` | `string` | Tag to be displayed as root of formatted object.
| `data` | `any` | JS object to be formatted.
| `levelsOpen?` | `number` | Levels opened by default when JS object is formatted and rendered.
| `refresh?` | `function()` | Refreshes json-formatter and rerenders [data]

## Setup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<md-icon class="tc-grey-600" *ngIf="hasChildren()">{{open? 'keyboard_arrow_down' : 'keyboard_arrow_right'}}</md-icon>
<span *ngIf="key" class="key">{{key}}:</span>
<span class="value">
<span [class.td-empty]="!hasChildren()" *ngIf="isObject()" [title]="getPreview()">
<span [class.td-empty]="!hasChildren()" *ngIf="isObject()" [mdTooltip]="getPreview()" mdTooltipPosition="after" (mouseenter)="tooltipRefresh()" (mouseleave)="tooltipRefresh()" (click)="tooltipRefresh()">
<span>{{getObjectName()}}</span>
<span *ngIf="isArray()">[{{data.length}}]</span>
</span>
<span *ngIf="!isObject()" [class]="getType(data)">{{getValue(data)}}</span>
</span>
</a>
<div class="td-object-children" *ngIf="hasChildren() && open">
<div class="td-object-children" [@tdCollapse]="!(hasChildren() && open)">
<template let-key ngFor [ngForOf]="children">
<td-json-formatter [key]="key" [data]="data[key]" [levelsOpen]="levelsOpen - 1"></td-json-formatter>
</template>
Expand Down
194 changes: 194 additions & 0 deletions src/platform/core/json-formatter/json-formatter.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
TestBed,
inject,
async,
ComponentFixture,
} from '@angular/core/testing';
import { Component } from '@angular/core';
import { CovalentJsonFormatterModule, TdJsonFormatterComponent } from './json-formatter.module';
import { By } from '@angular/platform-browser';

describe('Component: JsonFormatter', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
TdJsonFormatterBasicTestComponent,
],
imports: [
CovalentJsonFormatterModule.forRoot(),
],
});
TestBed.compileComponents();
}));

it('should render component with undefined in it',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.undefined'))).toBeTruthy();
});
})));

it('should render component with key truncated and elipsis',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = {
loooooooooooooooooooooooooooooong: 'string',
};
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('.key'))[0].nativeElement.textContent.trim())
.toBe('looooooooooooooooooooooooooooo…:');
});
})));

it('should throw error if [levelsOpen] is not an integer',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.levelsOpen = undefined;
expect(function(): void {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
});
}).toThrowError();
})));

it('should render component with key and values depending on type',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = {
stringProperty: 'string',
dateProperty: new Date(),
numberProperty: 1,
functionProperty: function(): void {/* */},
/* tslint:disable-next-line */
nullProperty: null,
undefinedProperty: undefined,
arrayProperty: [],
};
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('.key'))[0].nativeElement.textContent.trim())
.toBe('stringProperty:');
expect(fixture.debugElement.query(By.css('.string'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[1].nativeElement.textContent.trim())
.toBe('dateProperty:');
expect(fixture.debugElement.query(By.css('.date'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[2].nativeElement.textContent.trim())
.toBe('numberProperty:');
expect(fixture.debugElement.query(By.css('.number'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[3].nativeElement.textContent.trim())
.toBe('functionProperty:');
expect(fixture.debugElement.query(By.css('.function'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[4].nativeElement.textContent.trim())
.toBe('nullProperty:');
expect(fixture.debugElement.query(By.css('.null'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[5].nativeElement.textContent.trim())
.toBe('undefinedProperty:');
expect(fixture.debugElement.query(By.css('.undefined'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.key'))[6].nativeElement.textContent.trim())
.toBe('arrayProperty:');
expect(fixture.debugElement.query(By.css('.td-empty'))).toBeTruthy();
});
})));

it('should render component with an array hidden',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = [1, 2, 3, 4, 5, 6];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.td-key-node'))).toBeTruthy();
expect(fixture.debugElement.queryAll(By.css('.td-key-leaf')).length).toBe(6);
/* tslint:disable-next-line */
expect(fixture.debugElement.query(By.css('.td-object-children')).styles['display']).toBe('none');
});
})));

it('should render component with an array hidden, click on node and then display array content',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = [1, 2, 3, 4, 5, 6];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.td-key-node'))).toBeTruthy();
/* tslint:disable-next-line */
expect(fixture.debugElement.query(By.css('.td-object-children')).styles['display']).toBe('none');
fixture.debugElement.query(By.css('.td-key-node')).triggerEventHandler('click', new Event('click'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
/* tslint:disable-next-line */
expect(fixture.debugElement.query(By.css('.td-object-children')).styles['display']).toBeNull();
});
});
})));

it('should render component with an array display by 1 level',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = [1, 2, 3, 4, 5, 6];
component.levelsOpen = 1;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
/* tslint:disable-next-line */
expect(fixture.debugElement.query(By.css('.td-object-children')).styles['display']).toBeNull();
});
})));

it('should render component and rerender data only when refresh method explicitly called',
async(inject([], () => {
let fixture: ComponentFixture<any> = TestBed.createComponent(TdJsonFormatterBasicTestComponent);
let component: TdJsonFormatterBasicTestComponent = fixture.debugElement.componentInstance;
component.data = {property: 'test'};
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
/* tslint:disable-next-line */
expect(fixture.debugElement.query(By.css('.string')).nativeElement.textContent.trim()).toBe('"test"');
component.data.property = 'test2';
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.string')).nativeElement.textContent.trim()).toBe('"test"');
fixture.debugElement.query(By.directive(TdJsonFormatterComponent)).componentInstance.refresh();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.string')).nativeElement.textContent.trim()).toBe('"test2"');
});
});
});
})));

});

@Component({
selector: 'td-json-formatter-basic-test',
template: `
<td-json-formatter [data]="data" [levelsOpen]="levelsOpen">
</td-json-formatter>
`,
})
class TdJsonFormatterBasicTestComponent {

data: any;
levelsOpen: number = 0;

}
48 changes: 40 additions & 8 deletions src/platform/core/json-formatter/json-formatter.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Component, Input } from '@angular/core';
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { TdCollapseAnimation } from '../common/common.module';

@Component({

changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'td-json-formatter',
styleUrls: ['./json-formatter.component.scss' ],
templateUrl: './json-formatter.component.html',
animations: [
TdCollapseAnimation(),
],
})
export class TdJsonFormatterComponent {

Expand All @@ -13,14 +17,19 @@ export class TdJsonFormatterComponent {
*/
private static KEY_MAX_LENGTH: number = 30;

/**
* Max length for preview string. Any names bigger than this get trunctated.
*/
private static PREVIEW_STRING_MAX_LENGTH: number = 80;

/**
* Max tooltip preview elements.
*/
private static PREVIEW_LIMIT: number = 5;

private _key: string;
private _data: any;
private _children: string[] = [];
private _children: string[];
private _open: boolean = false;
private _levelsOpen: number = 0;

Expand Down Expand Up @@ -74,6 +83,25 @@ export class TdJsonFormatterComponent {
return this._children;
}

constructor(private _changeDetectorRef: ChangeDetectorRef) {
}

/**
* Refreshes json-formatter and rerenders [data]
*/
refresh(): void {
this._changeDetectorRef.markForCheck();
}

/**
* Workaround for https://github.com/angular/material2/issues/1825
*/
tooltipRefresh(): void {
setTimeout(() => {
this.refresh();
}, 100);
}

/**
* Toggles collapse/expanded state of component.
*/
Expand All @@ -90,7 +118,7 @@ export class TdJsonFormatterComponent {
}

hasChildren(): boolean {
return this.children.length > 0;
return this._children && this._children.length > 0;
}

/**
Expand Down Expand Up @@ -155,8 +183,8 @@ export class TdJsonFormatterComponent {
*/
getPreview(): string {
let previewData: string[];
let startChar: string = '{';
let endChar: string = '}';
let startChar: string = '{ ';
let endChar: string = ' }';
if (this.isArray()) {
let previewArray: any[] = this._data.slice(0, TdJsonFormatterComponent.PREVIEW_LIMIT);
previewData = previewArray.map((obj: any) => {
Expand All @@ -170,12 +198,16 @@ export class TdJsonFormatterComponent {
return key + ': ' + this.getValue(this._data[key]);
});
}
let ellipsis: string = previewData.length >= TdJsonFormatterComponent.PREVIEW_LIMIT ? '…' : '';
return startChar + previewData.join(', ') + ellipsis + endChar;
let previewString: string = previewData.join(', ');
let ellipsis: string = previewData.length >= TdJsonFormatterComponent.PREVIEW_LIMIT ||
previewString.length > TdJsonFormatterComponent.PREVIEW_STRING_MAX_LENGTH ? '…' : '';
return startChar + previewString.substring(0, TdJsonFormatterComponent.PREVIEW_STRING_MAX_LENGTH) +
ellipsis + endChar;
}

private parseChildren(): void {
if (this.isObject()) {
this._children = [];
for (let key in this._data) {
this._children.push(key);
}
Expand Down
4 changes: 2 additions & 2 deletions src/platform/core/json-formatter/json-formatter.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';

import { MdIconModule } from '@angular/material';
import { MaterialModule } from '@angular/material';

import { TdJsonFormatterComponent } from './json-formatter.component';

Expand All @@ -10,7 +10,7 @@ export { TdJsonFormatterComponent } from './json-formatter.component';
@NgModule({
imports: [
CommonModule,
MdIconModule.forRoot(),
MaterialModule.forRoot(),
],
declarations: [
TdJsonFormatterComponent,
Expand Down

0 comments on commit da4db7f

Please sign in to comment.