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

atinc / ngx-tethys / 2bb461f1-51aa-4bcb-8006-30243e37cb19

16 May 2025 03:32AM UTC coverage: 90.253% (-0.02%) from 90.272%
2bb461f1-51aa-4bcb-8006-30243e37cb19

Pull #3360

circleci

invalid-email-address
fix: fix type
Pull Request #3360: refactor(grid): migration signal #TINFR-1474

5609 of 6876 branches covered (81.57%)

Branch coverage included in aggregate %.

30 of 31 new or added lines in 5 files covered. (96.77%)

11 existing lines in 5 files now uncovered.

13400 of 14186 relevant lines covered (94.46%)

919.97 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
    AfterContentInit,
4
    ChangeDetectionStrategy,
5
    Component,
6
    Directive,
7
    ElementRef,
8
    NgZone,
9
    OnChanges,
10
    OnInit,
11
    SimpleChanges,
12
    inject,
13
    input,
14
    contentChildren,
15
    effect
16
} from '@angular/core';
17
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1✔
18
import { Observable, Subject } from 'rxjs';
19
import { throttleTime } from 'rxjs/operators';
19✔
20
import { ThyGridToken, THY_GRID_COMPONENT } from './grid.token';
19✔
21
import { ThyGridItem } from './thy-grid-item.component';
19✔
22
import { useHostRenderer } from '@tethys/cdk/dom';
19✔
23
import {
19✔
24
    ThyGridResponsiveMode,
19✔
25
    ThyGridResponsiveDescription,
19✔
26
    THY_GRID_DEFAULT_COLUMNS,
19✔
27
    THY_GRID_ITEM_DEFAULT_SPAN,
19✔
28
    screenBreakpointsMap
19✔
29
} from './grid.type';
19✔
30

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

3✔
54
    /**
55
     * @internal
52✔
56
     */
52✔
57
    readonly gridItems = contentChildren(ThyGridItem);
52✔
58

59
    /**
60
     * 栅格的列数
28✔
61
     * @default 24
5✔
62
     */
63
    readonly thyCols = input<number | ThyGridResponsiveDescription>(THY_GRID_DEFAULT_COLUMNS);
64

65
    /**
20✔
66
     * 栅格的水平间隔
20✔
67
     */
20✔
68
    readonly thyXGap = input<number | ThyGridResponsiveDescription>(0);
69

70
    /**
71
     * 栅格的垂直间隔
23✔
72
     */
23✔
73
    readonly thyYGap = input<number | ThyGridResponsiveDescription>(0);
74

UNCOV
75
    /**
×
76
     * 栅格的水平和垂直间隔
×
77
     */
×
78
    readonly thyGap = input<number | ThyGridResponsiveDescription>(0);
79

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

185✔
88
    private hostRenderer = useHostRenderer();
185✔
89

90
    private cols: number;
48✔
91

92
    public xGap: number;
93

477✔
94
    private yGap: number;
452✔
95

96
    private numRegex = /^\d+$/;
97

25✔
98
    private responsiveContainerWidth: number;
25✔
99

25✔
100
    public gridItemPropValueChange$ = new Subject<void>();
25✔
101

18✔
102
    private takeUntilDestroyed = takeUntilDestroyed();
103

7✔
104
    constructor() {
5✔
105
        effect(() => {
106
            this.handleGridItems();
107
        });
2✔
108
    }
109

110
    ngOnInit(): void {
111
        this.setGridStyle();
112

25✔
113
        if (this.thyResponsive() !== 'none') {
105✔
114
            this.listenResizeEvent();
22✔
115
        }
116
    }
105✔
117

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

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

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

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

165
            gridItem.span = Math.min(span + offset, this.cols);
166
            gridItem.offset = offset;
167
        });
168

169
        this.gridItemPropValueChange$.next();
170
    }
171

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

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

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

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

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

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

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