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

atinc / ngx-tethys / b23929fa-6159-4035-9970-81fd86a79cf5

06 Jun 2025 03:28AM UTC coverage: 90.312% (-0.003%) from 90.315%
b23929fa-6159-4035-9970-81fd86a79cf5

Pull #3462

circleci

minlovehua
refactor(property): remove useless changes$
Pull Request #3462: refactor(property): use computed instead of effect

5552 of 6821 branches covered (81.4%)

Branch coverage included in aggregate %.

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

2 existing lines in 1 file now uncovered.

13736 of 14536 relevant lines covered (94.5%)

903.05 hits per line

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

93.75
/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
import { OverlayOutsideClickDispatcher, OverlayRef } from '@angular/cdk/overlay';
6
import { NgTemplateOutlet } from '@angular/common';
7
import {
8
    ChangeDetectionStrategy,
9
    Component,
10
    ElementRef,
11
    NgZone,
12
    numberAttribute,
13
    OnDestroy,
14
    TemplateRef,
15
    inject,
16
    input,
17
    computed,
18
    effect,
1✔
19
    output,
20
    contentChild,
53✔
21
    viewChild,
53✔
22
    signal,
53✔
23
    DestroyRef
53✔
24
} from '@angular/core';
53✔
25
import { ThyProperties } from './properties.component';
53✔
26
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
53✔
27
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
53✔
28

53✔
29
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
53✔
30

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

1✔
57
    /**
58
     * 属性名称
59
     * @type sting
60
     */
61
    readonly thyLabelText = input<string>();
62

42✔
63
    /**
42✔
64
     * 设置属性是否是可编辑的
13✔
65
     */
66
    readonly thyEditable = input<boolean, ThyBooleanInput>(false, { transform: coerceBooleanProperty });
42✔
67

68
    /**
69
     * 设置跨列的数量
70
     */
71
    readonly thySpan = input(1, { transform: numberAttribute });
72

UNCOV
73
    /**
×
74
     * 设置编辑状态触发方法
75
     * @type 'hover' | 'click'
76
     */
8✔
77
    readonly thyEditTrigger = input<'hover' | 'click'>();
78

79
    /**
25!
80
     * 设置属性操作现实触发方式,默认 always 一直显示
25✔
81
     * @type 'hover' | 'always'
25!
UNCOV
82
     */
×
83
    readonly thyOperationTrigger = input<ThyPropertyItemOperationTrigger>('always');
84

25✔
85
    readonly thyEditingChange = output<boolean>();
25✔
86

87
    /**
88
     * 属性名称自定义模板
8✔
89
     * @type TemplateRef
8✔
90
     */
8✔
91
    readonly label = contentChild<TemplateRef<void>>('label');
92

93
    /**
94
     * 属性内容编辑模板,只有在 thyEditable 为 true 时生效
95
     * @type TemplateRef
96
     */
1✔
97
    readonly editor = contentChild<TemplateRef<void>>('editor');
1✔
98

1!
99
    /**
1✔
100
     * 操作区模板
101
     * @type TemplateRef
102
     */
1✔
103
    readonly operation = contentChild<TemplateRef<void>>('operation');
104

105
    /**
106
     * @private
107
     */
7✔
108
    readonly content = viewChild<TemplateRef<void>>('contentTemplate');
109

110
    /**
2✔
111
     * @private
112
     */
113
    readonly itemContent = viewChild<ElementRef<HTMLElement>>('item');
2✔
114

115
    editing = signal(false);
116

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

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

121
    private clickEventSubscription: Subscription;
122

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

127
    readonly isVertical = computed(() => {
53✔
128
        return this.parent?.layout() === 'vertical';
53✔
129
    });
130

1✔
131
    constructor() {
1✔
132
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
133

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

142
                if (this.clickEventSubscription) {
143
                    this.clickEventSubscription.unsubscribe();
144
                    this.clickEventSubscription = null;
145
                }
1✔
146
            }
147
        });
148
    }
149

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

159
    /**
160
     * @deprecated please use setEditing(editing: boolean)
161
     */
162
    setKeepEditing(keep: boolean) {
163
        this.setEditing(keep);
164
    }
165

166
    private hasOverlay() {
167
        return !!this.overlayOutsideClickDispatcher._attachedOverlays.filter(overlay => !this.originOverlays.includes(overlay)).length;
168
    }
169

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

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

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

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

227
    ngOnDestroy(): void {
228
        this.eventDestroy$.next();
229
        this.eventDestroy$.complete();
230
    }
231
}
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