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

atinc / ngx-tethys / ae34f7c4-f62a-4f83-afe6-1444d18c4794

06 Jun 2025 03:43AM UTC coverage: 90.306% (-0.002%) from 90.308%
ae34f7c4-f62a-4f83-afe6-1444d18c4794

push

circleci

web-flow
refactor: use readonly to decorate Signal, remove ngOnChanges (#3463)

5552 of 6821 branches covered (81.4%)

Branch coverage included in aggregate %.

6 of 8 new or added lines in 3 files covered. (75.0%)

8 existing lines in 2 files now uncovered.

13731 of 14532 relevant lines covered (94.49%)

903.28 hits per line

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

89.39
/src/grid/thy-grid.component.ts
1
import { ViewportRuler } from '@angular/cdk/scrolling';
2
import {
3
    ChangeDetectionStrategy,
4
    Component,
5
    Directive,
6
    ElementRef,
7
    NgZone,
8
    OnInit,
9
    inject,
10
    input,
11
    contentChildren,
12
    effect
13
} from '@angular/core';
14
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
15
import { Observable, Subject } from 'rxjs';
16
import { throttleTime } from 'rxjs/operators';
17
import { ThyGridToken, THY_GRID_COMPONENT } from './grid.token';
1✔
18
import { ThyGridItem } from './thy-grid-item.component';
19
import { useHostRenderer } from '@tethys/cdk/dom';
19✔
20
import {
19✔
21
    ThyGridResponsiveMode,
19✔
22
    ThyGridResponsiveDescription,
19✔
23
    THY_GRID_DEFAULT_COLUMNS,
19✔
24
    THY_GRID_ITEM_DEFAULT_SPAN,
19✔
25
    screenBreakpointsMap
19✔
26
} from './grid.type';
19✔
27

19✔
28
/**
19✔
29
 * 栅格组件
19✔
30
 * @name thy-grid, [thyGrid]
19✔
31
 * @order 10
19✔
32
 */
19✔
33
@Directive({
28✔
34
    selector: '[thyGrid]',
35
    providers: [
36
        {
37
            provide: THY_GRID_COMPONENT,
32✔
38
            useExisting: ThyGrid
32✔
39
        }
28✔
40
    ],
41
    host: {
42
        class: 'thy-grid'
43
    }
52✔
44
})
52✔
45
// eslint-disable-next-line @angular-eslint/directive-class-suffix
52✔
46
export class ThyGrid implements ThyGridToken, OnInit {
52✔
47
    private elementRef = inject(ElementRef);
52✔
48
    private viewportRuler = inject(ViewportRuler);
49✔
49
    private ngZone = inject(NgZone);
49✔
50

51
    /**
52
     * @internal
3✔
53
     */
3✔
54
    readonly gridItems = contentChildren(ThyGridItem);
55

52✔
56
    /**
52✔
57
     * 栅格的列数
52✔
58
     * @default 24
59
     */
60
    readonly thyCols = input<number | ThyGridResponsiveDescription>(THY_GRID_DEFAULT_COLUMNS);
28✔
61

5✔
62
    /**
63
     * 栅格的水平间隔
64
     */
65
    readonly thyXGap = input<number | ThyGridResponsiveDescription>(0);
20✔
66

20✔
67
    /**
20✔
68
     * 栅格的垂直间隔
69
     */
70
    readonly thyYGap = input<number | ThyGridResponsiveDescription>(0);
71

23✔
72
    /**
23✔
73
     * 栅格的水平和垂直间隔
74
     */
UNCOV
75
    readonly thyGap = input<number | ThyGridResponsiveDescription>(0);
×
UNCOV
76

×
UNCOV
77
    /**
×
78
     * 响应式栅格列数<br/>
79
     * none: 不进行响应式布局。<br/>
80
     * self:根据grid的自身宽度进行响应式布局。<br/>
81
     * screen:根据屏幕断点进行响应式布局,目前预设了5种响应式尺寸:`xs: 0, sm: 576, md: 768, lg: 992, xl: 1200`。
82
     */
83
    readonly thyResponsive = input<ThyGridResponsiveMode>('none');
48✔
84

185✔
85
    private hostRenderer = useHostRenderer();
185✔
86

185✔
87
    private cols: number;
185✔
88

185✔
89
    public xGap: number;
90

48✔
91
    private yGap: number;
92

93
    private numRegex = /^\d+$/;
477✔
94

452✔
95
    private responsiveContainerWidth: number;
96

97
    public gridItemPropValueChange$ = new Subject<void>();
25✔
98

25✔
99
    private takeUntilDestroyed = takeUntilDestroyed();
25✔
100

25✔
101
    constructor() {
18✔
102
        effect(() => {
103
            this.handleGridItems();
7✔
104
        });
5✔
105
    }
106

107
    ngOnInit(): void {
2✔
108
        this.setGridStyle();
109

110
        if (this.thyResponsive() !== 'none') {
111
            this.listenResizeEvent();
112
        }
25✔
113
    }
105✔
114

22✔
115
    private setGridStyle() {
116
        this.cols = this.calculateActualValue(this.thyCols() || THY_GRID_DEFAULT_COLUMNS, THY_GRID_DEFAULT_COLUMNS);
105✔
117
        const xGap = this.thyXGap();
105✔
118
        const yGap = this.thyYGap();
105✔
119
        const gap = this.thyGap();
120
        if (!xGap && !yGap) {
121
            this.xGap = this.calculateActualValue(gap || 0);
122
            this.yGap = this.xGap;
25!
123
        } else {
25✔
124
            this.xGap = this.calculateActualValue(xGap || gap);
25✔
125
            this.yGap = this.calculateActualValue(yGap || gap);
83✔
126
        }
103✔
127

128
        this.hostRenderer.setStyle('display', 'grid');
129
        this.hostRenderer.setStyle('grid-template-columns', `repeat(${this.cols}, minmax(0, 1fr))`);
130
        this.hostRenderer.setStyle('gap', `${this.yGap}px ${this.xGap}px`);
UNCOV
131
    }
×
UNCOV
132

×
UNCOV
133
    private listenResizeEvent() {
×
134
        if (this.thyResponsive() === 'screen') {
×
135
            this.viewportRuler
136
                .change(100)
137
                .pipe(this.takeUntilDestroyed)
138
                .subscribe(() => {
139
                    this.responsiveContainerWidth = this.viewportRuler.getViewportSize().width;
140
                    this.setGridStyle();
23✔
141
                    this.handleGridItems();
23✔
UNCOV
142
                });
×
143
        } else {
144
            this.ngZone.runOutsideAngular(() => {
23✔
145
                this.gridResizeObserver(this.elementRef.nativeElement)
23✔
146
                    .pipe(throttleTime(100), this.takeUntilDestroyed)
23✔
147
                    .subscribe((data: ResizeObserverEntry[]) => {
148
                        this.responsiveContainerWidth = data[0]?.contentRect?.width;
149
                        this.setGridStyle();
150
                        this.handleGridItems();
1✔
151
                    });
1✔
152
            });
153
        }
154
    }
155

156
    private handleGridItems() {
157
        this.gridItems().forEach((gridItem: ThyGridItem) => {
158
            const rawSpan = getRawSpan(gridItem.thySpan());
159
            const span = this.calculateActualValue(rawSpan, THY_GRID_ITEM_DEFAULT_SPAN);
160
            const offset = this.calculateActualValue(gridItem.thyOffset() || 0);
1✔
161

162
            gridItem.span = Math.min(span + offset, this.cols);
163
            gridItem.offset = offset;
164
        });
165

166
        this.gridItemPropValueChange$.next();
167
    }
168

169
    private calculateActualValue(rawValue: number | ThyGridResponsiveDescription, defaultValue?: number): number {
170
        if (this.numRegex.test(rawValue.toString().trim())) {
171
            return Number(rawValue);
172
        } else {
173
            const responsiveValueMap = this.getResponsiveValueMap(rawValue as ThyGridResponsiveDescription);
174
            const breakpointKeys = Object.keys(responsiveValueMap);
175
            const breakpoint = this.calculateBreakPoint(breakpointKeys);
176

177
            if (this.thyResponsive() !== 'none' && breakpoint) {
178
                return responsiveValueMap[breakpoint];
179
            } else if (breakpointKeys.includes('0')) {
180
                return responsiveValueMap['0'];
181
            } else {
1✔
182
                return defaultValue || 0;
183
            }
19✔
184
        }
185
    }
186

1✔
187
    private getResponsiveValueMap(responsiveValue: string): { [key: string]: number } {
188
        return responsiveValue.split(' ').reduce((map: { [key: string]: number }, item: string) => {
189
            if (this.numRegex.test(item.toString())) {
190
                item = `0:${item}`;
191
            }
192
            const [key, value] = item.split(':');
193
            map[key] = Number(value);
194
            return map;
195
        }, {});
196
    }
197

198
    private calculateBreakPoint(breakpointKeys: string[]): string {
199
        if (this.thyResponsive() === 'screen') {
200
            const width = this.responsiveContainerWidth || this.viewportRuler.getViewportSize().width;
201
            return breakpointKeys.find((key: string, index: number) => {
202
                return index < breakpointKeys.length - 1
203
                    ? width >= screenBreakpointsMap[key] && width < screenBreakpointsMap[breakpointKeys[index + 1]]
204
                    : width >= screenBreakpointsMap[key];
205
            });
206
        } else {
207
            const width = this.responsiveContainerWidth || this.elementRef.nativeElement.getBoundingClientRect().width;
208
            return breakpointKeys.find((key: string, index: number) => {
185✔
209
                return index < breakpointKeys.length - 1
210
                    ? width >= Number(key) && width < Number(breakpointKeys[index + 1])
211
                    : width >= Number(key);
212
            });
213
        }
214
    }
215

216
    private gridResizeObserver(element: HTMLElement): Observable<ResizeObserverEntry[]> {
217
        return new Observable(observer => {
218
            const resize = new ResizeObserver((entries: ResizeObserverEntry[]) => {
219
                observer.next(entries);
220
            });
221
            resize.observe(element);
222
            return () => {
223
                resize.disconnect();
224
            };
225
        });
226
    }
227
}
228

229
/**
230
 * @internal
231
 */
232
@Component({
233
    selector: 'thy-grid',
234
    template: '<ng-content></ng-content>',
235
    changeDetection: ChangeDetectionStrategy.OnPush,
236
    imports: [],
237
    providers: [
238
        {
239
            provide: THY_GRID_COMPONENT,
240
            useExisting: ThyGrid
241
        }
242
    ],
243
    hostDirectives: [
244
        {
245
            directive: ThyGrid,
246
            inputs: ['thyCols', 'thyXGap', 'thyYGap', 'thyGap', 'thyResponsive']
247
        }
248
    ]
249
})
250
export class ThyGridComponent {
251
    grid = inject(ThyGrid);
252
}
253

254
function getRawSpan(span: number | ThyGridResponsiveDescription | undefined | null): number | ThyGridResponsiveDescription {
255
    return span === undefined || span === null ? THY_GRID_ITEM_DEFAULT_SPAN : span;
256
}
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