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

atinc / ngx-tethys / #102

26 May 2026 08:11AM UTC coverage: 91.111% (+0.7%) from 90.407%
#102

push

web-flow
build: bump docgeni to 2.8.0-next.5 (#3809)

4571 of 5491 branches covered (83.25%)

Branch coverage included in aggregate %.

13141 of 13949 relevant lines covered (94.21%)

966.75 hits per line

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

88.89
/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';
18
import { ThyGridItem } from './thy-grid-item.component';
19
import { useHostRenderer } from '@tethys/cdk/dom';
20
import {
21
    ThyGridResponsiveMode,
22
    ThyGridResponsiveDescription,
23
    THY_GRID_DEFAULT_COLUMNS,
24
    THY_GRID_ITEM_DEFAULT_SPAN,
25
    screenBreakpointsMap
26
} from './grid.type';
27

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

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

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

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

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

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

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');
19✔
84

85
    private hostRenderer = useHostRenderer();
19✔
86

87
    private cols!: number;
88

89
    public xGap!: number;
90

91
    private yGap!: number;
92

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

95
    private responsiveContainerWidth!: number;
96

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

99
    private takeUntilDestroyed = takeUntilDestroyed();
19✔
100

101
    constructor() {
102
        effect(() => {
19✔
103
            this.handleGridItems();
28✔
104
        });
105
    }
106

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

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

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

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

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

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

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

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

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

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

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

198
    private calculateBreakPoint(breakpointKeys: string[]): string | undefined {
199
        if (this.thyResponsive() === 'screen') {
25!
200
            const width = this.responsiveContainerWidth || this.viewportRuler.getViewportSize().width;
25✔
201
            return breakpointKeys.find((key: string, index: number) => {
25✔
202
                return index < breakpointKeys.length - 1
86✔
203
                    ? width >= screenBreakpointsMap[key] && width < screenBreakpointsMap[breakpointKeys[index + 1]]
107✔
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) => {
×
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 => {
23✔
218
            const resize = new ResizeObserver((entries: ResizeObserverEntry[]) => {
23✔
219
                observer.next(entries);
×
220
            });
221
            resize.observe(element);
23✔
222
            return () => {
23✔
223
                resize.disconnect();
23✔
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 {
1✔
251
    grid = inject(ThyGrid);
19✔
252
}
253

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