Skip to content

Commit 9e35488

Browse files
authored
fix: stop click propagation by default (#47)
This is typically the expected behavior. Expanding/collapsing nodes should not trigger event handling in parent components in most cases. Overridable if needed.
1 parent 181aeb5 commit 9e35488

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Component } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { By } from '@angular/platform-browser';
4+
import { StopClickPropagationDirective } from './stop-click-propagation.directive';
5+
6+
@Component({
7+
standalone: true,
8+
imports: [StopClickPropagationDirective],
9+
template: `
10+
<div (click)="parentClicked()">
11+
<div [ngxJtStopClickPropagation]="enabled">
12+
<span (click)="childClicked()">Click Me</span>
13+
</div>
14+
</div>
15+
`,
16+
})
17+
class TestComponent {
18+
enabled = true;
19+
parentClicked() {}
20+
childClicked() {}
21+
}
22+
23+
describe('StopClickPropagationDirective', () => {
24+
let fixture: ComponentFixture<TestComponent>;
25+
let component: TestComponent;
26+
27+
beforeEach(async () => {
28+
await TestBed.configureTestingModule({
29+
imports: [TestComponent],
30+
}).compileComponents();
31+
32+
fixture = TestBed.createComponent(TestComponent);
33+
component = fixture.componentInstance;
34+
fixture.detectChanges();
35+
});
36+
37+
it('should stop click propagation when enabled', () => {
38+
const parentSpy = spyOn(component, 'parentClicked');
39+
const childSpy = spyOn(component, 'childClicked');
40+
41+
const clickableSpan = fixture.debugElement.query(By.css('span'));
42+
clickableSpan.nativeElement.click();
43+
44+
expect(childSpy).toHaveBeenCalled();
45+
expect(parentSpy).not.toHaveBeenCalled();
46+
});
47+
48+
it('should not stop click propagation when disabled', () => {
49+
component.enabled = false;
50+
fixture.detectChanges();
51+
52+
const parentSpy = spyOn(component, 'parentClicked');
53+
const childSpy = spyOn(component, 'childClicked');
54+
55+
const clickableSpan = fixture.debugElement.query(By.css('span'));
56+
clickableSpan.nativeElement.click();
57+
58+
expect(childSpy).toHaveBeenCalled();
59+
expect(parentSpy).toHaveBeenCalled();
60+
});
61+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Directive, HostListener, input } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[ngxJtStopClickPropagation]',
5+
standalone: true,
6+
})
7+
export class StopClickPropagationDirective {
8+
enabled = input<boolean>(true, { alias: 'ngxJtStopClickPropagation' });
9+
10+
@HostListener('click', ['$event'])
11+
onClick(event: Event): void {
12+
if (this.enabled()) {
13+
event.stopPropagation();
14+
}
15+
}
16+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class="segment-primitive"
99
[class.clickable]="isClickablePrimitive()"
1010
(click)="onPrimitiveClick($event)"
11+
[ngxJtStopClickPropagation]="stopClickPropagation()"
1112
[disabled]="!isClickablePrimitive()">
1213
{{ asString() }}
1314
</button>
@@ -45,6 +46,7 @@
4546
[class.expandable]="expandable"
4647
[class.expanded]="segment.expanded"
4748
(click)="toggle(segment)"
49+
[ngxJtStopClickPropagation]="stopClickPropagation()"
4850
[disabled]="!expandable">
4951
@if (expandable) {
5052
<div class="toggler"></div>
@@ -72,6 +74,7 @@
7274
[class.segment-value]="!expandable"
7375
[class.clickable]="clickableValue"
7476
(click)="onValueClickHandler(segment, $event)"
77+
[ngxJtStopClickPropagation]="stopClickPropagation()"
7578
[disabled]="!clickableValue">
7679
{{ segment.description }}
7780
</button>
@@ -91,6 +94,7 @@
9194
[enableClickableValues]="enableClickableValues()"
9295
[valueClickHandlers]="valueClickHandlers()"
9396
[isClickableValue]="isClickableValue()"
97+
[stopClickPropagation]="stopClickPropagation()"
9498
(onValueClick)="onValueClickHandler($event)"
9599
[_parent]="segment"
96100
[_currentDepth]="_currentDepth() + 1" />

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component, computed, inject, input, output } from '@angular/core';
2+
import { StopClickPropagationDirective } from '../directives/stop-click-propagation.directive';
23
import { VALUE_CLICK_HANDLERS } from '../handlers';
34
import { ID_GENERATOR } from '../services/id-generator';
45
import { IsClickableValueFn, Segment, ValueClickHandler } from '../types';
@@ -10,7 +11,7 @@ import { decycle, previewString } from '../util';
1011
*/
1112
@Component({
1213
selector: 'ngx-json-treeview',
13-
imports: [],
14+
imports: [StopClickPropagationDirective],
1415
templateUrl: './ngx-json-treeview.component.html',
1516
styleUrls: ['./ngx-json-treeview.component.scss'],
1617
})
@@ -52,6 +53,19 @@ export class NgxJsonTreeviewComponent {
5253
*/
5354
enableClickableValues = input<boolean>(false);
5455

56+
/**
57+
* A flag to control whether click events on nodes propagate up the DOM tree.
58+
*
59+
* By default, click events are stopped from propagating. This is useful when
60+
* the tree view is embedded within other clickable elements to avoid
61+
* unintended side effects.
62+
*
63+
* Set to `false` to allow events to propagate.
64+
*
65+
* @default true
66+
*/
67+
stopClickPropagation = input<boolean>(true);
68+
5569
/**
5670
* @deprecated Use `valueClickHandlers` instead. This input will be removed
5771
* in a future version.

0 commit comments

Comments
 (0)