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

atinc / ngx-tethys / ea6fe6ba-7187-47bf-b0db-9f0360e15466

06 Jun 2025 02:10AM UTC coverage: 90.308% (-0.007%) from 90.315%
ea6fe6ba-7187-47bf-b0db-9f0360e15466

Pull #3462

circleci

minlovehua
refactor(property): use computed instead of effect
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 2 files now uncovered.

13736 of 14537 relevant lines covered (94.49%)

902.98 hits per line

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

93.83
/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
    SimpleChanges,
16
    TemplateRef,
17
    inject,
18
    input,
1✔
19
    computed,
20
    effect,
53✔
21
    output,
53✔
22
    contentChild,
53✔
23
    viewChild,
53✔
24
    signal,
53✔
25
    DestroyRef
53✔
26
} from '@angular/core';
53✔
27

53✔
28
import { ThyProperties } from './properties.component';
53✔
29
import { coerceBooleanProperty, ThyBooleanInput } from 'ngx-tethys/util';
53✔
30
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
53✔
31

53✔
32
export type ThyPropertyItemOperationTrigger = 'hover' | 'always';
53✔
33

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

60
    /**
61
     * 属性名称
62
     * @type sting
63
     */
42✔
64
    readonly thyLabelText = input<string>();
42✔
65

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

71
    /**
72
     * 设置跨列的数量
73
     */
74
    readonly thySpan = input(1, { transform: numberAttribute });
×
75

76
    /**
77
     * 设置编辑状态触发方法
8✔
78
     * @type 'hover' | 'click'
79
     */
80
    readonly thyEditTrigger = input<'hover' | 'click'>();
25!
81

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

88
    readonly thyEditingChange = output<boolean>();
89

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

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

102
    /**
103
     * 操作区模板
1✔
104
     * @type TemplateRef
105
     */
106
    readonly operation = contentChild<TemplateRef<void>>('operation');
107

108
    /**
7✔
109
     * @private
110
     */
111
    readonly content = viewChild<TemplateRef<void>>('contentTemplate');
2✔
112

113
    /**
114
     * @private
2✔
115
     */
116
    readonly itemContent = viewChild<ElementRef<HTMLElement>>('item');
117

118
    editing = signal(false);
8✔
119

8✔
120
    changes$ = new Subject<SimpleChanges>();
1✔
121

122
    private eventDestroy$ = new Subject<void>();
123

7✔
124
    private originOverlays: OverlayRef[] = [];
125

126
    private clickEventSubscription: Subscription;
127

128
    protected readonly gridColumn = computed(() => {
53✔
129
        return `span ${Math.min(this.thySpan(), this.parent?.thyColumn())}`;
53✔
130
    });
131

1✔
132
    readonly isVertical = computed(() => {
1✔
133
        return this.parent?.layout() === 'vertical';
134
    });
135

136
    constructor() {
137
        this.originOverlays = [...this.overlayOutsideClickDispatcher._attachedOverlays] as OverlayRef[];
138

139
        effect(() => {
140
            if (this.thyEditable()) {
141
                this.subscribeClick();
142
            } else {
143
                this.setEditing(false);
144
                this.eventDestroy$.next();
145
                this.eventDestroy$.complete();
146

1✔
147
                if (this.clickEventSubscription) {
148
                    this.clickEventSubscription.unsubscribe();
149
                    this.clickEventSubscription = null;
150
                }
151
            }
152
        });
153
    }
154

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

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

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

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

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

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

222
    private bindEditorBlurEvent(editorElement: HTMLElement) {
223
        timer(0).subscribe(() => {
224
            if (this.hasOverlay()) {
225
                this.subscribeOverlayDetach();
226
            } else {
227
                this.subscribeDocumentClick(editorElement);
228
            }
229
        });
230
    }
231

232
    ngOnDestroy(): void {
233
        this.eventDestroy$.next();
234
        this.eventDestroy$.complete();
235
    }
236
}
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