• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / 90f0f0ed-223e-4754-9642-97b0660ce0dc

22 May 2025 02:22AM UTC coverage: 90.245% (-0.009%) from 90.254%
90f0f0ed-223e-4754-9642-97b0660ce0dc

Pull #3451

circleci

invalid-email-address
fix(property): remove oninit to subscribe click #TINFR-1760
Pull Request #3451: fix(property): remove oninit to subscribe click #TINFR-1760

5555 of 6826 branches covered (81.38%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

13696 of 14506 relevant lines covered (94.42%)

900.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.9
/src/property/property-item.component.ts
1
import { ThyClickDispatcher } from 'ngx-tethys/core';
2
import { ThyFlexibleText } from 'ngx-tethys/flexible-text';
3
import { combineLatest, fromEvent, Subject, Subscription, timer } from 'rxjs';
4
import { delay, filter, take, takeUntil } from 'rxjs/operators';
5

6
import { OverlayOutsideClickDispatcher, OverlayRef } from '@angular/cdk/overlay';
7
import { NgTemplateOutlet } from '@angular/common';
8
import {
9
    ChangeDetectionStrategy,
10
    Component,
11
    ElementRef,
12
    NgZone,
13
    numberAttribute,
14
    OnDestroy,
15
    OnInit,
16
    SimpleChanges,
17
    TemplateRef,
1✔
18
    inject,
19
    input,
47✔
20
    computed,
47✔
21
    effect,
47✔
22
    output,
47✔
23
    contentChild,
47✔
24
    viewChild,
47✔
25
    signal
47✔
26
} from '@angular/core';
47✔
27

47✔
28
import { ThyProperties } from './properties.component';
47✔
29
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
47✔
30

47✔
31
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
47✔
32

47✔
33
/**
47✔
34
 * 属性组件
47✔
35
 * @name thy-property-item
47✔
36
 */
47✔
37
@Component({
47✔
38
    selector: 'thy-property-item',
47✔
39
    templateUrl: './property-item.component.html',
54✔
40
    host: {
41
        class: 'thy-property-item',
47✔
42
        '[class.thy-property-item-operational]': '!!operation()',
47✔
43
        '[class.thy-property-item-operational-hover]': "thyOperationTrigger() === 'hover'",
47✔
44
        '[style.grid-column]': 'gridColumn()'
49✔
45
    },
22✔
46
    changeDetection: ChangeDetectionStrategy.OnPush,
47
    imports: [ThyFlexibleText, NgTemplateOutlet]
48
})
27✔
49
export class ThyPropertyItem implements OnDestroy {
27✔
50
    private clickDispatcher = inject(ThyClickDispatcher);
27✔
51
    private ngZone = inject(NgZone);
27✔
52
    private overlayOutsideClickDispatcher = inject(OverlayOutsideClickDispatcher);
1✔
53
    private parent = inject(ThyProperties);
1✔
54

55
    /**
56
     * 属性名称
57
     * @type sting
47✔
58
     * @default thyLabelText
51✔
59
     */
51✔
60
    readonly thyLabelText = input<string>();
61

62
    /**
63
     * 设置属性是否是可编辑的
37✔
64
     * @type sting
37✔
65
     * @default false
11✔
66
     */
67
    readonly thyEditable = input<boolean, ThyBooleanInput>(false, { transform: coerceBooleanProperty });
37✔
68

69
    /**
70
     * 设置跨列的数量
71
     * @type number
72
     */
73
    readonly thySpan = input(1, { transform: numberAttribute });
UNCOV
74

×
75
    /**
76
     * 设置属性操作现实触发方式,默认 always 一直显示
77
     * @type 'hover' | 'always'
6✔
78
     */
79
    readonly thyOperationTrigger = input<ThyPropertyItemOperationTrigger>('always');
80

22!
81
    readonly thyEditingChange = output<boolean>();
22✔
82

22!
UNCOV
83
    /**
×
84
     * 属性名称自定义模板
85
     * @type TemplateRef
22✔
86
     */
87
    readonly label = contentChild<TemplateRef<void>>('label');
88

6✔
89
    /**
6✔
90
     * 属性内容编辑模板,只有在 thyEditable 为 true 时生效
91
     * @type TemplateRef
92
     */
93
    readonly editor = contentChild<TemplateRef<void>>('editor');
94

95
    /**
1✔
96
     * 操作区模板
1✔
97
     * @type TemplateRef
1!
98
     */
1✔
99
    readonly operation = contentChild<TemplateRef<void>>('operation');
100

101
    /**
1✔
102
     * @private
103
     */
104
    readonly content = viewChild<TemplateRef<void>>('contentTemplate');
105

106
    /**
5✔
107
     * @private
108
     */
109
    readonly itemContent = viewChild<ElementRef<HTMLElement>>('item');
2✔
110

111
    editing = signal(false);
112

2✔
113
    changes$ = new Subject<SimpleChanges>();
114

115
    private destroy$ = new Subject<void>();
116

6✔
117
    private eventDestroy$ = new Subject<void>();
6✔
118

1✔
119
    private originOverlays: OverlayRef[] = [];
120

121
    private clickEventSubscription: Subscription;
5✔
122

123
    protected readonly gridColumn = computed(() => {
124
        return `span ${Math.min(this.thySpan(), this.parent.thyColumn())}`;
125
    });
126

47✔
127
    isVertical = signal(false);
47✔
128

47✔
129
    constructor() {
47✔
130
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
131

1✔
132
        effect(() => {
1✔
133
            if (this.thyEditable()) {
134
                this.subscribeClick();
135
            } else {
136
                this.setEditing(false);
137
                this.eventDestroy$.next();
138
                this.eventDestroy$.complete();
139

140
                if (this.clickEventSubscription) {
141
                    this.clickEventSubscription.unsubscribe();
142
                    this.clickEventSubscription = null;
143
                }
144
            }
145
        });
1✔
146

147
        effect(() => {
148
            const layout = this.parent.layout();
149
            this.isVertical.set(layout === 'vertical');
150
        });
151
    }
152

153
    setEditing(editing: boolean) {
154
        this.ngZone.run(() => {
155
            if (!!this.editing() !== !!editing) {
156
                this.thyEditingChange.emit(editing);
157
            }
158
            this.editing.set(editing);
159
        });
160
    }
161

162
    /**
163
     * @deprecated please use setEditing(editing: boolean)
164
     */
165
    setKeepEditing(keep: boolean) {
166
        this.setEditing(keep);
167
    }
168

169
    private hasOverlay() {
170
        return !!this.overlayOutsideClickDispatcher._attachedOverlays.filter(overlay => !this.originOverlays.includes(overlay)).length;
171
    }
172

173
    private subscribeClick() {
174
        if (this.thyEditable() === true) {
175
            this.ngZone.runOutsideAngular(() => {
176
                if (this.clickEventSubscription) {
177
                    return;
178
                }
179
                this.clickEventSubscription = fromEvent(this.itemContent().nativeElement, 'click')
180
                    .pipe(takeUntil(this.eventDestroy$))
181
                    .subscribe(() => {
182
                        this.setEditing(true);
183
                        this.bindEditorBlurEvent(this.itemContent().nativeElement);
184
                    });
185
            });
186
        }
187
    }
188

189
    private subscribeOverlayDetach() {
190
        const openedOverlays = this.overlayOutsideClickDispatcher._attachedOverlays.filter(
191
            overlay => !this.originOverlays.includes(overlay)
192
        );
193
        const overlaysDetachments$ = openedOverlays.map(overlay => overlay.detachments());
194
        if (overlaysDetachments$.length) {
195
            combineLatest(overlaysDetachments$)
196
                .pipe(delay(50), take(1), takeUntil(this.destroy$))
197
                .subscribe(() => {
198
                    this.setEditing(false);
199
                });
200
        }
201
    }
202

203
    private subscribeDocumentClick(editorElement: HTMLElement) {
204
        this.clickDispatcher
205
            .clicked(0)
206
            .pipe(
207
                filter(event => {
208
                    return !editorElement.contains(event.target as HTMLElement);
209
                }),
210
                take(1),
211
                takeUntil(this.destroy$)
212
            )
213
            .subscribe(() => {
214
                this.setEditing(false);
215
            });
216
    }
217

218
    private bindEditorBlurEvent(editorElement: HTMLElement) {
219
        timer(0).subscribe(() => {
220
            if (this.hasOverlay()) {
221
                this.subscribeOverlayDetach();
222
            } else {
223
                this.subscribeDocumentClick(editorElement);
224
            }
225
        });
226
    }
227

228
    ngOnDestroy(): void {
229
        this.destroy$.next();
230
        this.destroy$.complete();
231

232
        this.eventDestroy$.next();
233
        this.eventDestroy$.complete();
234
    }
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc