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

atinc / ngx-tethys / 89e9464a-5a7f-4212-8ceb-9a496a513532

14 May 2025 04:33AM UTC coverage: 90.277% (+0.005%) from 90.272%
89e9464a-5a7f-4212-8ceb-9a496a513532

push

circleci

web-flow
feat(action): migrate to signal for action @wumeimin (#3414)

5616 of 6881 branches covered (81.62%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

13381 of 14162 relevant lines covered (94.49%)

921.72 hits per line

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

94.59
/src/action/action.component.ts
1
import {
2
    AfterViewInit,
3
    ChangeDetectionStrategy,
4
    ChangeDetectorRef,
5
    Component,
6
    ElementRef,
7
    OnChanges,
8
    OnDestroy,
9
    OnInit,
1✔
10
    Renderer2,
11
    Signal,
12
    SimpleChanges,
13
    computed,
14
    inject,
15
    input
16
} from '@angular/core';
17
import { useHostRenderer } from '@tethys/cdk/dom';
18
import { ThyIcon } from 'ngx-tethys/icon';
19

20
import { coerceBooleanProperty } from 'ngx-tethys/util';
21
import { Subscription, timer } from 'rxjs';
22

23
export type ThyActionType = 'primary' | 'success' | 'danger' | 'warning';
24

25
export type ThyActionFeedback = 'success' | 'error';
1✔
26

27
export interface ThyActionFeedbackOptions {
460✔
28
    icon?: string;
460✔
29
    class?: string;
460✔
30
    duration?: number;
460✔
31
}
460✔
32

460✔
33
const defaultFeedbackOptions: Record<ThyActionFeedback, ThyActionFeedbackOptions> = {
460!
34
    success: {
460✔
35
        icon: 'check-circle-fill',
460✔
36
        class: 'text-success',
460✔
37
        duration: 3000
460✔
38
    },
460✔
39
    error: {
460✔
40
        icon: 'close-circle-fill',
460✔
41
        class: 'text-danger',
460✔
42
        duration: 3000
460✔
43
    }
44
};
45
/**
460✔
46
 * 立即操作组件
47
 * @name thy-action,[thyAction]
48
 */
460✔
49
@Component({
50
    selector: 'thy-action, [thyAction]',
51
    templateUrl: './action.component.html',
460!
UNCOV
52
    changeDetection: ChangeDetectionStrategy.OnPush,
×
53
    host: {
54
        class: 'thy-action',
55
        '[class.active]': 'active()',
56
        '[class.thy-action-hover-icon]': 'thyHoverIcon()',
149✔
57
        '[class.thy-action-has-feedback]': '!!feedback',
58
        '[class.disabled]': 'thyDisabled()'
59
    },
60
    imports: [ThyIcon]
61
})
62
export class ThyAction implements OnInit, AfterViewInit, OnChanges, OnDestroy {
2✔
63
    private elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
64
    private renderer = inject(Renderer2);
65
    private cdr = inject(ChangeDetectorRef);
66

67
    public icon: Signal<string> = computed(() => this.thyActionIcon() || this.thyIcon());
68

1✔
69
    feedback: ThyActionFeedback = null;
70

71
    feedbackOptions: ThyActionFeedbackOptions;
3✔
72

1✔
73
    active: Signal<boolean> = computed(() => this.thyActionActive() || this.thyActive());
74

2✔
75
    private type: Signal<string> = computed(() => this.thyType() || 'primary');
2✔
76

2✔
77
    private hostRenderer = useHostRenderer();
2✔
78

2!
79
    private feedbackTimer: Subscription;
2✔
80

1✔
81
    /**
82
     * 操作图标的类型
2✔
83
     * @type primary | success | danger | warning
1✔
84
     */
1✔
85
    readonly thyType = input<ThyActionType>('primary');
1✔
86

87
    /**
88
     * 操作图标,支持传参同时也支持在投影中写 thy-icon 组件
89
     */
90
    readonly thyIcon = input<string>('');
460✔
91

1,867✔
92
    /**
9✔
93
     * 操作图标,当 thyIcon 和其他指令参数名有冲突时使用 thyActionIcon
9✔
94
     */
95
    readonly thyActionIcon = input<string>('');
9✔
96

9✔
97
    /**
98
     * 操作的图标 Active 状态,设置为 true 时会在 Item 上添加 active class
99
     */
100
    readonly thyActive = input(false, { transform: coerceBooleanProperty });
101

460✔
102
    /**
460✔
103
     * 操作的图标 Active 状态,当 thyActive 和其他指令参数名有冲突时使用 thyActionActive
460✔
104
     */
29✔
105
    readonly thyActionActive = input(false, { transform: coerceBooleanProperty });
106

460✔
107
    /**
108
     * 操作图标的主题
109
     * @type fill(背景色填充) | lite(简单文本颜色变化)
460✔
110
     */
111
    readonly thyTheme = input<'fill' | 'lite'>('fill');
1✔
112

113
    /**
114
     * Hover 展示的图标
115
     */
116
    readonly thyHoverIcon = input<string>();
117

118
    /**
119
     * 是否处于禁用状态
120
     */
121
    readonly thyDisabled = input<boolean, boolean | string | number>(false, { transform: coerceBooleanProperty });
122

1✔
123
    ngOnInit(): void {
124
        this.updateClasses();
125
    }
126

127
    ngAfterViewInit() {
128
        this.wrapSpanForText(this.elementRef.nativeElement.childNodes);
129
    }
130

131
    ngOnChanges(changes: SimpleChanges): void {
132
        if ((changes.thyType && !changes.thyType.firstChange) || (changes.thyTheme && !changes.thyTheme.firstChange)) {
133
            this.updateClasses();
134
        }
135
    }
136

137
    setMarginRight(marginRight: string) {
138
        this.elementRef.nativeElement.style.marginRight = marginRight;
139
    }
140

141
    /**
142
     * 触发成功反馈操作
143
     */
144
    success(options?: ThyActionFeedbackOptions) {
145
        this.setFeedback('success', options);
146
    }
147

148
    /**
149
     * 触发失败反馈操作
150
     */
151
    error(options?: ThyActionFeedbackOptions) {
152
        this.setFeedback('error', options);
153
    }
154

155
    private setFeedback(feedback: ThyActionFeedback, options: ThyActionFeedbackOptions) {
156
        if (this.thyDisabled()) {
157
            return;
158
        }
159
        options = Object.assign({}, defaultFeedbackOptions[feedback], options);
160
        this.feedback = feedback;
161
        this.feedbackOptions = options;
162
        this.cdr.markForCheck();
163
        if (options.duration) {
164
            if (this.feedbackTimer) {
165
                this.feedbackTimer.unsubscribe();
166
            }
167
            this.feedbackTimer = timer(options.duration).subscribe(() => {
168
                this.feedback = null;
169
                this.feedbackOptions = null;
170
                this.cdr.markForCheck();
171
            });
172
        }
173
    }
174

175
    private wrapSpanForText(nodes: NodeList): void {
176
        nodes.forEach(node => {
177
            if (node.nodeName === '#text') {
178
                const span = this.renderer.createElement('span');
179
                const parent = this.renderer.parentNode(node);
180
                // this.renderer.addClass(span, 'thy-action-wrap-span');
181
                this.renderer.insertBefore(parent, span, node);
182
                this.renderer.appendChild(span, node);
183
            }
184
        });
185
    }
186

187
    private updateClasses() {
188
        let classNames: string[] = [];
189
        classNames.push(`action-${this.type()}`);
190
        if (this.thyTheme() === 'lite') {
191
            classNames.push('thy-action-lite');
192
        }
193
        this.hostRenderer.updateClass(classNames);
194
    }
195

196
    ngOnDestroy(): void {
197
        this.feedbackTimer?.unsubscribe();
198
    }
199
}
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

© 2026 Coveralls, Inc