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

atinc / ngx-tethys / f737954f-a31f-4e8c-b739-ad697474bc49

30 May 2025 08:21AM UTC coverage: 90.315% (-0.001%) from 90.316%
f737954f-a31f-4e8c-b739-ad697474bc49

push

circleci

web-flow
refactor(icon): migrate signal input #TINFR-1476 (#3460)

5552 of 6821 branches covered (81.4%)

Branch coverage included in aggregate %.

12 of 14 new or added lines in 1 file covered. (85.71%)

1 existing line in 1 file now uncovered.

13741 of 14541 relevant lines covered (94.5%)

902.53 hits per line

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

91.51
/src/icon/icon.component.ts
1
import { take } from 'rxjs/operators';
2
import { useHostRenderer } from '@tethys/cdk/dom';
3

4
import {
5
    ChangeDetectionStrategy,
6
    Component,
7
    ElementRef,
8
    Renderer2,
9
    ViewEncapsulation,
1✔
10
    numberAttribute,
11
    inject,
12
    input,
13
    effect
14
} from '@angular/core';
15

16
import { getWhetherPrintErrorWhenIconNotFound } from './config';
17
import { ThyIconRegistry } from './icon-registry';
18
import { coerceBooleanProperty } from 'ngx-tethys/util';
1✔
19

20
const iconSuffixMap = {
6,728✔
21
    fill: 'fill',
6,728✔
22
    twotone: 'tt'
6,728✔
23
};
6,728✔
24

6,728✔
25
/**
6,728✔
26
 * 图标组件
6,728✔
27
 * @name thy-icon,[thy-icon]
6,728✔
28
 * @order 10
6,728✔
29
 */
6,728✔
30
@Component({
31
    selector: 'thy-icon, [thy-icon]',
32
    template: '<ng-content></ng-content>',
6,728✔
33
    changeDetection: ChangeDetectionStrategy.OnPush,
6,728✔
34
    encapsulation: ViewEncapsulation.None,
6,816✔
35
    host: {
36
        class: 'thy-icon',
6,728✔
37
        '[class.thy-icon-legging]': 'thyIconLegging()'
6,728✔
38
    }
39
})
40
export class ThyIcon {
41
    private render = inject(Renderer2);
6,816✔
42
    private elementRef = inject(ElementRef);
6,816✔
43
    private iconRegistry = inject(ThyIconRegistry);
6,662✔
44

6,660✔
45
    /**
46
     * 图标的类型
47
     * @type outline | fill | twotone
48
     */
1,362✔
49
    readonly thyIconType = input<'outline' | 'fill' | 'twotone'>('outline');
50

5,066!
NEW
51
    readonly thyTwotoneColor = input<string>();
×
52

53
    /**
54
     * 图标的名字
6,660!
55
     */
56
    readonly thyIconName = input.required<string>();
57

2✔
58
    /**
59
     * 图标的旋转角度
60
     * @default 0
2✔
61
     */
62
    readonly thyIconRotate = input<number, unknown>(undefined, { transform: numberAttribute });
63

64
    readonly thyIconSet = input<string>();
65

8,090✔
66
    /**
67
     * 图标打底色,镂空的图标,会透过颜色来
205✔
68
     * @default false
205✔
69
     */
10✔
70
    readonly thyIconLegging = input<boolean, undefined>(undefined, { transform: coerceBooleanProperty });
71

195✔
72
    readonly thyIconLinearGradient = input<boolean, unknown>(undefined, {
73
        transform: coerceBooleanProperty
74
    });
75

76
    private hostRenderer = useHostRenderer();
1,362✔
77

78
    constructor() {
79
        effect(() => {
80
            this.updateClasses();
1,362✔
81
        });
1,362✔
82
        effect(() => {
16✔
83
            this.setStyleRotate();
84
        });
1,362✔
85
    }
1✔
86

1!
87
    private updateClasses() {
1✔
88
        const [namespace, iconName] = this.iconRegistry.splitIconName(this.thyIconName());
2✔
89
        if (iconName) {
1✔
90
            if (this.iconRegistry.iconMode === 'svg') {
91
                this.iconRegistry
92
                    .getSvgIcon(this.buildIconNameByType(iconName), namespace)
93
                    .pipe(take(1))
94
                    .subscribe(
95
                        svg => {
96
                            this.setSvgElement(svg);
97
                        },
98
                        (error: Error) => {
99
                            if (getWhetherPrintErrorWhenIconNotFound()) {
100
                                console.error(`Error retrieving icon: ${error.message}`);
101
                            }
102
                        }
1,362✔
103
                    );
1✔
104
                this.hostRenderer.updateClass([`thy-icon${namespace ? `-${namespace}` : ``}-${this.buildIconNameByType(iconName)}`]);
1✔
105
            } else {
106
                const fontSetClass = this.thyIconSet()
1,362✔
107
                    ? this.iconRegistry.getFontSetClassByAlias(this.thyIconSet())
1,362✔
108
                    : this.iconRegistry.getDefaultFontSetClass();
109
                this.hostRenderer.updateClass([fontSetClass, `${fontSetClass}-${this.thyIconName()}`]);
110
            }
1,362✔
111
        }
1,362✔
112
    }
113

114
    private setStyleRotate() {
115
        if (this.thyIconRotate() !== undefined) {
116
            // 基于 effect 无法保证在 setSvgElement 之前执行,所以这里增加判断
117
            const svg = this.elementRef.nativeElement.querySelector('svg');
1,362✔
118
            if (!svg) {
17✔
119
                return;
120
            }
121
            this.render.setStyle(svg, 'transform', `rotate(${this.thyIconRotate()}deg)`);
17!
122
        }
17✔
123
    }
124

125
    //#region svg element
126

127
    private setSvgElement(svg: SVGElement) {
128
        this.clearSvgElement();
13,320✔
129

6✔
130
        // Workaround for IE11 and Edge ignoring `style` tags inside dynamically-created SVGs.
6✔
131
        // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10898469/
132
        // Do this before inserting the element into the DOM, in order to avoid a style recalculation.
133
        const styleTags = svg.querySelectorAll('style') as NodeListOf<HTMLStyleElement>;
13,314✔
134

135
        for (let i = 0; i < styleTags.length; i++) {
136
            styleTags[i].textContent += ' ';
137
        }
138

139
        if (this.thyIconType() === 'twotone') {
140
            const allPaths = svg.querySelectorAll('path');
141
            if (allPaths.length > 1) {
1✔
142
                allPaths.forEach((child, index: number) => {
1✔
143
                    if (child.getAttribute('id').includes('secondary-color')) {
1!
NEW
144
                        child.setAttribute('fill', this.thyTwotoneColor());
×
145
                    }
146
                });
1!
UNCOV
147
            }
×
148
        }
149

150
        // Note: we do this fix here, rather than the icon registry, because the
151
        // references have to point to the URL at the time that the icon was created.
152
        // if (this._location) {
1✔
153
        //     const path = this._location.getPathname();
1✔
154
        //     this._previousPath = path;
155
        //     this._cacheChildrenWithExternalReferences(svg);
1✔
156
        //     this._prependPathToReferences(path);
1✔
157
        // }
158
        if (this.thyIconLinearGradient()) {
159
            this.setBaseUrl(svg);
160
            this.clearTitleElement(svg);
161
        }
162

163
        this.elementRef.nativeElement.appendChild(svg);
164
        this.setStyleRotate();
165
    }
166

1✔
167
    private clearSvgElement() {
168
        const layoutElement: HTMLElement = this.elementRef.nativeElement;
169
        let childCount = layoutElement.childNodes.length;
170

171
        // if (this._elementsWithExternalReferences) {
172
        //     this._elementsWithExternalReferences.clear();
173
        // }
174

175
        // Remove existing non-element child nodes and SVGs, and add the new SVG element. Note that
176
        // we can't use innerHTML, because IE will throw if the element has a data binding.
177
        while (childCount--) {
178
            const child = layoutElement.childNodes[childCount];
179

180
            // 1 corresponds to Node.ELEMENT_NODE. We remove all non-element nodes in order to get rid
181
            // of any loose text nodes, as well as any SVG elements in order to remove any old icons.
182
            if (child.nodeType !== 1 || child.nodeName.toLowerCase() === 'svg') {
183
                layoutElement.removeChild(child);
184
            }
185
        }
186
    }
187

188
    //#endregion
189

190
    private buildIconNameByType(iconName: string) {
191
        if (this.thyIconType() && ['fill', 'twotone'].indexOf(this.thyIconType()) >= 0) {
192
            const suffix = iconSuffixMap[this.thyIconType() as keyof typeof iconSuffixMap];
193
            return iconName.includes(`-${suffix}`) ? iconName : `${iconName}-${suffix}`;
194
        } else {
195
            return iconName;
196
        }
197
    }
198

199
    /**
200
     * Support Safari SVG LinearGradient.
201
     * @param svg
202
     */
203
    private setBaseUrl(svg: SVGElement) {
204
        const styleElements = svg.querySelectorAll('style');
205
        styleElements.forEach((n: HTMLElement) => {
206
            if (n.style.cssText.includes('url')) {
207
                n.style.fill = n.style.fill.replace('url("', 'url("' + location.pathname);
208
            }
209
            if (n.style.cssText.includes('clip-path')) {
210
                n.style.clipPath = n.style.clipPath.replace('url("', 'url("' + location.pathname);
211
            }
212
        });
213
    }
214

215
    private clearTitleElement(svg: SVGElement) {
216
        const titleElement = svg.querySelector('title');
217
        titleElement && titleElement.remove();
218
    }
219
}
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