Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ export class AppComponent {
...structuredClone(this.baseObj),
nested: {
...structuredClone(this.baseObj),
function: () => {
return 'foo';
},
deeplyNested: {
...structuredClone(this.baseObj),
function: () => {
return 'bar';
},
},
},
function: () => {
return 'foo';
return 'baz';
},
};
}
52 changes: 5 additions & 47 deletions projects/ngx-json-treeview/src/lib/ngx-json-treeview.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, computed, input } from '@angular/core';
import { decycle, previewString } from './util';

export interface Segment {
key: string;
Expand All @@ -24,7 +25,7 @@ export class NgxJsonTreeviewComponent {

// computed values
segments = computed<Segment[]>(() => {
const json = this.decycle(this.json());
const json = decycle(this.json());
const arr = [];
if (typeof json === 'object') {
Object.keys(json).forEach((key) => {
Expand Down Expand Up @@ -87,14 +88,13 @@ export class NgxJsonTreeviewComponent {
segment.description = 'null';
} else if (Array.isArray(segment.value)) {
segment.type = 'array';
const len = segment.value.length;
segment.description = `Array[${len}] ${JSON.stringify(segment.value)}`;
segment.description = previewString(segment.value);
} else if (segment.value instanceof Date) {
segment.type = 'date';
segment.description = segment.value.toISOString();
segment.description = `"${segment.value.toISOString()}"`;
} else {
segment.type = 'object';
segment.description = `Object ${JSON.stringify(segment.value)}`;
segment.description = previewString(segment.value);
}
break;
default:
Expand All @@ -103,46 +103,4 @@ export class NgxJsonTreeviewComponent {

return segment;
}

// https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
private decycle(object: any) {
const objects = new WeakMap();
return (function derez(value, path) {
let old_path;
let nu: any;

if (
typeof value === 'object' &&
value !== null &&
!(value instanceof Boolean) &&
!(value instanceof Date) &&
!(value instanceof Number) &&
!(value instanceof RegExp) &&
!(value instanceof String)
) {
old_path = objects.get(value);
if (old_path !== undefined) {
return { $ref: old_path };
}
objects.set(value, path);

if (Array.isArray(value)) {
nu = [];
value.forEach(function (element, i) {
nu[i] = derez(element, path + '[' + i + ']');
});
} else {
nu = {};
Object.keys(value).forEach(function (name) {
nu[name] = derez(
value[name],
path + '[' + JSON.stringify(name) + ']'
);
});
}
return nu;
}
return value;
})(object, '$');
}
}
131 changes: 131 additions & 0 deletions projects/ngx-json-treeview/src/lib/tests/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { decycle, previewString } from '../util';

describe('Util', () => {
describe('previewString', () => {
it('should handle null values', () => {
expect(previewString(null)).toEqual('null');
});

it('should handle undefined values', () => {
expect(previewString(undefined)).toEqual('undefined');
});

it('should handle string values', () => {
expect(previewString('hello')).toEqual('"hello"');
});

it('should handle boolean values', () => {
expect(previewString(true)).toEqual('true');
});

it('should handle number (integer) values', () => {
expect(previewString(42)).toEqual('42');
});

it('should handle number (decimal) values', () => {
expect(previewString(5.6)).toEqual('5.6');
});

it('should handle date objects', () => {
const date = new Date();
expect(previewString(date)).toEqual(`"${date.toISOString()}"`);
});

it('should handle arrays', () => {
expect(previewString([1, 2, 3])).toEqual('Array[3] [1,2,3]');
});

it('should handle regular objects', () => {
const obj = { a: 1, b: 'hello' };
expect(previewString(obj)).toEqual('Object {"a":1,"b":"hello"}');
});

it('should handle function values', () => {
expect(previewString(() => {})).toEqual('Function');
});

it('should truncate when limit is exceeded', () => {
const obj = { a: 1, b: 'hello'.repeat(50) };
expect(previewString(obj, 20)).toEqual('Object {"a":1,"b":"h…');
});

it('should truncate string values in objects when stringsLimit is exceeded', () => {
const obj = { a: 1, b: 'hello'.repeat(50) };
expect(previewString(obj, 200, 10)).toEqual(
'Object {"a":1,"b":"hellohello…"}'
);
});

it('should truncate string values in arrays when stringsLimit is exceeded', () => {
expect(previewString(['longstring'.repeat(10)], 200, 10)).toEqual(
'Array[1] ["longstring…"]'
);
});

describe('parity with JSON.stringify()', () => {
it('should handle null values', () => {
expect(previewString(null)).toEqual(JSON.stringify(null));
});

it('should handle undefined values', () => {
expect(previewString(undefined)).toEqual(
JSON.stringify(undefined) + ''
);
});

it('should handle string values', () => {
expect(previewString('hello')).toEqual(JSON.stringify('hello'));
});

it('should handle number (integer) values', () => {
expect(previewString(42)).toEqual(JSON.stringify(42));
});

it('should handle number (decimal) values', () => {
expect(previewString(5.6)).toEqual(JSON.stringify(5.6));
});

it('should handle boolean values', () => {
expect(previewString(true)).toEqual(JSON.stringify(true));
});

it('should handle date objects', () => {
const date = new Date();
expect(previewString(date)).toEqual(JSON.stringify(date));
});

it('should handle regular objects', () => {
const obj = { a: 1, b: 'hello' };
expect(previewString(obj)).toEqual('Object ' + JSON.stringify(obj));
});

it('should handle arrays', () => {
const arr = [1, 2, 'hello'];
expect(previewString(arr)).toEqual('Array[3] ' + JSON.stringify(arr));
});

// functions have intentional differences
});
});

describe('decycle', () => {
it('should replace circular references with $ref properties', () => {
const obj = { a: 1 } as any;
obj.b = obj;
expect(decycle(obj)).toEqual({ a: 1, b: { $ref: '$' } });
});

it('should handle arrays with circular references', () => {
const arr: any[] = [1];
arr[1] = arr;
expect(decycle(arr)).toEqual([1, { $ref: '$' }]);
});

it('should handle nested objects with circular references', () => {
const obj1 = { a: 1 } as any;
const obj2 = { b: 2, c: obj1 } as any;
obj1.d = obj2;
expect(decycle(obj1)).toEqual({ a: 1, d: { b: 2, c: { $ref: '$' } } });
});
});
});
120 changes: 120 additions & 0 deletions projects/ngx-json-treeview/src/lib/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Generates a preview string representation of an object.
*
* @param obj The object to preview.
* @param limit The maximum length of the preview string. Defaults to 200.
* @param stringsLimit The maximum length of a string to display before
* truncating. Defaults to 10.
* @returns A preview string representation of the object.
*/
export function previewString(obj: any, limit = 200, stringsLimit = 10) {
let result = '';

if (obj === null) {
result += 'null';
} else if (obj === undefined) {
result += 'undefined';
} else if (typeof obj === 'string') {
if (obj.length > stringsLimit) {
result += `"${obj.substring(0, stringsLimit)}…"`;
} else {
result += `"${obj}"`;
}
} else if (typeof obj === 'boolean') {
result += `${obj ? 'true' : 'false'}`;
} else if (typeof obj === 'number') {
result += `${obj}`;
} else if (typeof obj === 'object') {
if (obj instanceof Date) {
result += `"${obj.toISOString()}"`;
} else if (Array.isArray(obj)) {
result += `Array[${obj.length}] [`;
for (const key in obj) {
if (result.length >= limit) {
break;
}
result += previewString(obj[key], limit - result.length);
result += ',';
}
if (result.endsWith(',')) {
result = result.slice(0, -1);
}
result += ']';
} else {
result += 'Object {';
for (const key in obj) {
if (result.length >= limit) {
break;
}
if (obj[key] !== undefined) {
result += `"${key}":`;
result += previewString(obj[key], limit - result.length);
result += ',';
}
}
if (result.endsWith(',')) {
result = result.slice(0, -1);
}
result += '}';
}
} else if (typeof obj === 'function') {
result += 'Function';
}

if (result.length >= limit) {
return result.substring(0, limit) + '…';
}

return result;
}

/**
* Decycles a JavaScript object by replacing circular references with `$ref`
* properties. This is useful for serializing objects that contain circular
* references, preventing infinite loops.
*
* Original: https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
*
* @param object The object to decycle.
* @returns A decycled version of the object.
*/
export function decycle(object: any): any {
const objects = new WeakMap();
return (function derez(value, path) {
let old_path;
let nu: any;

if (
typeof value === 'object' &&
value !== null &&
!(value instanceof Boolean) &&
!(value instanceof Date) &&
!(value instanceof Number) &&
!(value instanceof RegExp) &&
!(value instanceof String)
) {
old_path = objects.get(value);
if (old_path !== undefined) {
return { $ref: old_path };
}
objects.set(value, path);

if (Array.isArray(value)) {
nu = [];
value.forEach(function (element, i) {
nu[i] = derez(element, path + '[' + i + ']');
});
} else {
nu = {};
Object.keys(value).forEach(function (name) {
nu[name] = derez(
value[name],
path + '[' + JSON.stringify(name) + ']'
);
});
}
return nu;
}
return value;
})(object, '$');
}