Skip to content

Commit 98b0ca8

Browse files
authored
Follow links (#34)
Refactors the click handling, and adds built in functionality to follow http(s) links. Users will be able to extend with their own click handlers.
1 parent b7e4612 commit 98b0ca8

File tree

7 files changed

+168
-35
lines changed

7 files changed

+168
-35
lines changed

projects/demo/src/app/app.component.html

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ <h2>ngx-json-treeview</h2>
22

33
<h3>Collapsed</h3>
44
<div class="json-container">
5-
<ngx-json-treeview [json]="json" [expanded]="false" />
5+
<ngx-json-treeview
6+
[json]="json"
7+
[expanded]="false"
8+
[enableClickableValues]="true" />
69
</div>
710

811
<h3>Max Depth (1)</h3>
912
<div class="json-container">
10-
<ngx-json-treeview [json]="json" [depth]="1" />
13+
<ngx-json-treeview [json]="json" [depth]="1" [enableClickableValues]="true" />
1114
</div>
1215

1316
<h3>Fully Expanded</h3>
1417
<div class="json-container">
15-
<ngx-json-treeview [json]="json" />
18+
<ngx-json-treeview [json]="json" [enableClickableValues]="true" />
1619
</div>
1720

1821
<h3>Clickable Nodes</h3>
@@ -22,16 +25,14 @@ <h4>Object</h4>
2225
<ngx-json-treeview
2326
[json]="json"
2427
[expanded]="false"
25-
[isClickableValue]="isClickablePrimitiveValue"
26-
[enableClickableValues]="true"
27-
(onValueClick)="onValueClick($event)" />
28+
[valueClickHandlers]="clickHandlers"
29+
[enableClickableValues]="true" />
2830
<h4>Primitives</h4>
2931
@for (primitive of primitives; track $index) {
3032
<ngx-json-treeview
3133
[json]="primitive"
32-
[isClickableValue]="isClickablePrimitiveValue"
33-
[enableClickableValues]="true"
34-
(onValueClick)="onValueClick($event)" />
34+
[valueClickHandlers]="primitiveClickHandlers"
35+
[enableClickableValues]="true" />
3536
}
3637
</div>
3738
@let segment = currentSegment();

projects/demo/src/app/app.component.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Component, signal } from '@angular/core';
2-
import { NgxJsonTreeviewComponent, Segment } from 'ngx-json-treeview';
2+
import {
3+
NgxJsonTreeviewComponent,
4+
Segment,
5+
ValueClickHandler,
6+
} from 'ngx-json-treeview';
37

48
@Component({
59
selector: 'app-root',
@@ -45,17 +49,27 @@ export class AppComponent {
4549
},
4650
};
4751

48-
isClickableValue(segment: Segment) {
49-
return ['object', 'array', 'string'].includes(segment.type ?? '');
50-
}
51-
52-
isClickablePrimitiveValue(segment: Segment) {
53-
return ['string'].includes(segment.type ?? '');
54-
}
52+
clickHandlers: ValueClickHandler[] = [
53+
{
54+
canHandle: (segment: Segment) => {
55+
return ['object', 'array', 'string'].includes(segment.type ?? '');
56+
},
57+
handler: (segment: Segment) => {
58+
this.currentSegment.set(segment);
59+
},
60+
},
61+
];
5562

56-
onValueClick(segment: Segment) {
57-
this.currentSegment.set(segment);
58-
}
63+
primitiveClickHandlers: ValueClickHandler[] = [
64+
{
65+
canHandle: (segment: Segment) => {
66+
return ['string'].includes(segment.type ?? '');
67+
},
68+
handler: (segment: Segment) => {
69+
this.currentSegment.set(segment);
70+
},
71+
},
72+
];
5973

6074
stringify(obj: any) {
6175
if (typeof obj === 'function') {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ValueClickHandler } from './types';
2+
3+
/**
4+
* A handler that checks if a segment's value is a string that looks like an
5+
* HTTP/HTTPS link. If it is, it opens the link in a new tab.
6+
*/
7+
export const followLinkHandler: ValueClickHandler = {
8+
canHandle: (segment) => {
9+
if (typeof segment.value === 'string' && segment.value.startsWith('http')) {
10+
try {
11+
const url = new URL(segment.value); // Validate the URL.
12+
return url.protocol === 'http:' || url.protocol === 'https:';
13+
} catch (e) {
14+
// Invalid URL.
15+
}
16+
}
17+
return false;
18+
},
19+
handler: (segment) => {
20+
window.open(segment.value, '_blank', 'noopener,noreferrer');
21+
},
22+
};
23+
24+
/**
25+
* A collection of built-in value click handlers.
26+
* This array can be used to easily apply all default handlers.
27+
*/
28+
export const VALUE_CLICK_HANDLERS: readonly ValueClickHandler[] = [
29+
followLinkHandler,
30+
];
31+
32+
/**
33+
* A namespace for individual value click handlers.
34+
* This allows for easy discovery and individual import of handlers.
35+
*/
36+
export const ValueClickHandlers = {
37+
followLinkHandler,
38+
};

projects/ngx-json-treeview/src/lib/ngx-json-treeview/ngx-json-treeview.component.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,9 @@
8888
[json]="segment.value"
8989
[expanded]="expanded()"
9090
[depth]="depth()"
91-
[isClickableValue]="isClickableValue()"
9291
[enableClickableValues]="enableClickableValues()"
9392
[_parent]="segment"
94-
[_currentDepth]="_currentDepth() + 1"
95-
(onValueClick)="onValueClickHandler($event)" />
93+
[_currentDepth]="_currentDepth() + 1" />
9694
<span class="punctuation">
9795
{{ closingBrace }}{{ needsComma ? ',' : '' }}
9896
</span>

projects/ngx-json-treeview/src/lib/ngx-json-treeview/ngx-json-treeview.component.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component, computed, inject, input, output } from '@angular/core';
2+
import { VALUE_CLICK_HANDLERS } from '../handlers';
23
import { ID_GENERATOR } from '../services/id-generator';
3-
import { IsClickableValueFn, Segment } from '../types';
4+
import { IsClickableValueFn, Segment, ValueClickHandler } from '../types';
45
import { decycle, previewString } from '../util';
56

67
/**
@@ -40,16 +41,21 @@ export class NgxJsonTreeviewComponent {
4041
depth = input<number>(-1);
4142

4243
/**
43-
* If `true`, value nodes will emit an `onValueClick` event when clicked. This
44-
* allows for some interesting use cases, such as:
45-
* - Rendering preformatted text, html, markdown, etc in another component.
44+
* If `true`, values are clickable when there is a corresponding handler
45+
* in the `valueClickHandlers` array that can process it.
46+
*
47+
* This allows for use cases such as:
48+
* - Following hyperlinks.
4649
* - Copying a value to the clipboard.
47-
* - Following hyperlinks, etc
50+
* - Triggering custom actions based on the value's content or type.
4851
* @default false
4952
*/
5053
enableClickableValues = input<boolean>(false);
5154

5255
/**
56+
* @deprecated Use `valueClickHandlers` instead. This input will be removed
57+
* in a future version.
58+
*
5359
* A function that determines if a specific value node should be considered
5460
* clickable. This provides more granular control than the global
5561
* `enableClickableValues` flag.
@@ -61,18 +67,29 @@ export class NgxJsonTreeviewComponent {
6167
* @param segment - The segment being evaluated.
6268
* @returns `true` if the segment's value should be clickable, `false`
6369
* otherwise.
64-
* @default () => true - By default, all values are considered clickable if
65-
* `enableClickableValues` is true.
6670
*/
67-
isClickableValue = input<IsClickableValueFn>(() => true);
71+
isClickableValue = input<IsClickableValueFn>();
6872

6973
/**
74+
* @deprecated Use `valueClickHandlers` instead. This output will be removed
75+
* in a future version.
76+
*
7077
* If `enableClickableValues` is set to `true`, emits a `Segment` object when
7178
* a value node is clicked. The emitted `Segment` contains details about the
7279
* clicked node (key, value, type, path, etc.).
7380
*/
7481
onValueClick = output<Segment>();
7582

83+
/**
84+
* An array of handler functions to be executed when a value node is clicked.
85+
* Only the first handler in the array for which `isClickable` returns `true`
86+
* will be executed.
87+
*
88+
* If `enableClickableValues` is set to true, but `valueClickHandlers` is
89+
* omitted, the built-in `VALUE_CLICK_HANDLERS` will be used as the default.
90+
*/
91+
valueClickHandlers = input<ValueClickHandler[]>();
92+
7693
/**
7794
* *Internal* input representing the parent segment in the tree hierarchy.
7895
* Primrily used for calculating paths.
@@ -87,6 +104,21 @@ export class NgxJsonTreeviewComponent {
87104
*/
88105
_currentDepth = input<number>(0);
89106

107+
private internalValueClickHandlers = computed<ValueClickHandler[]>(() => {
108+
const handlers: ValueClickHandler[] = [];
109+
const legacyIsClickableFn = this.isClickableValue();
110+
111+
if (legacyIsClickableFn) {
112+
handlers.push({
113+
canHandle: legacyIsClickableFn,
114+
handler: (segment) => this.onValueClick.emit(segment),
115+
});
116+
}
117+
118+
handlers.push(...(this.valueClickHandlers() ?? VALUE_CLICK_HANDLERS));
119+
return handlers;
120+
});
121+
90122
rootType = computed<string>(() => {
91123
if (this.json() === null) {
92124
return 'null';
@@ -160,8 +192,18 @@ export class NgxJsonTreeviewComponent {
160192
);
161193
}
162194

163-
isClickable(segment: Segment) {
164-
return this.enableClickableValues() && this.isClickableValue()(segment);
195+
isClickable(segment: Segment): boolean {
196+
if (!this.enableClickableValues()) {
197+
return false;
198+
}
199+
200+
return this.internalValueClickHandlers().some((handler) => {
201+
try {
202+
return handler.canHandle(segment);
203+
} catch (e) {
204+
return false;
205+
}
206+
});
165207
}
166208

167209
toggle(segment: Segment) {
@@ -178,8 +220,19 @@ export class NgxJsonTreeviewComponent {
178220
}
179221

180222
onValueClickHandler(segment: Segment) {
181-
if (this.isClickable(segment)) {
182-
this.onValueClick.emit(segment);
223+
for (const handler of this.internalValueClickHandlers()) {
224+
try {
225+
if (handler.canHandle(segment)) {
226+
try {
227+
handler.handler(segment);
228+
} catch (e) {
229+
console.error('Error executing click handler:', e);
230+
}
231+
return; // Stop after the first handler is executed.
232+
}
233+
} catch (e) {
234+
// in case of any error, continue to the next handler
235+
}
183236
}
184237
}
185238

projects/ngx-json-treeview/src/lib/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,27 @@ export interface Segment {
3030
* @returns `true` if the value is clickable, `false` otherwise.
3131
*/
3232
export type IsClickableValueFn = (segment: Segment) => boolean;
33+
34+
/**
35+
* Represents a handler for value click events, containing both the logic to
36+
* determine if a value is clickable and the handler function itself.
37+
*
38+
* This approach allows for a more modular and self-contained way to define
39+
* click behaviors. Each handler can specify its own criteria for being active
40+
* and the action to take, making it easier to manage and extend different
41+
* click functionalities.
42+
*/
43+
export interface ValueClickHandler {
44+
/**
45+
* A function that determines whether this handler should be active for a
46+
* given segment.
47+
* @param segment The segment to evaluate.
48+
* @returns `true` if the handler is applicable, `false` otherwise.
49+
*/
50+
canHandle: IsClickableValueFn;
51+
/**
52+
* The function to execute when a clickable value is clicked.
53+
* @param segment The segment that was clicked.
54+
*/
55+
handler: (segment: Segment) => void;
56+
}

projects/ngx-json-treeview/src/public-api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
* Public API Surface of ngx-json-treeview
33
*/
44

5+
export {
6+
VALUE_CLICK_HANDLERS,
7+
ValueClickHandlers,
8+
followLinkHandler,
9+
} from './lib/handlers';
510
export * from './lib/ngx-json-treeview/ngx-json-treeview.component';
611
export * from './lib/types';

0 commit comments

Comments
 (0)