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

atinc / ngx-tethys / #55

30 Jul 2025 07:08AM UTC coverage: 9.866% (-80.4%) from 90.297%
#55

push

why520crazy
feat(empty): add setMessage for update display text #TINFR-2616

92 of 6794 branches covered (1.35%)

Branch coverage included in aggregate %.

2014 of 14552 relevant lines covered (13.84%)

6.15 hits per line

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

4.55
/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';
×
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 {
×
47
    private elementRef = inject(ElementRef);
×
48
    private viewportRuler = inject(ViewportRuler);
×
49
    private ngZone = inject(NgZone);
×
50

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

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

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

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

×
72
    /**
×
73
     * 栅格的水平和垂直间隔
74
     */
75
    readonly thyGap = input<number | ThyGridResponsiveDescription>(0);
×
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');
×
84

×
85
    private hostRenderer = useHostRenderer();
×
86

×
87
    private cols: number;
×
88

×
89
    public xGap: number;
90

×
91
    private yGap: number;
92

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

×
95
    private responsiveContainerWidth: number;
96

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

×
99
    private takeUntilDestroyed = takeUntilDestroyed();
×
100

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

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

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

×
115
    private setGridStyle() {
116
        this.cols = this.calculateActualValue(this.thyCols() || THY_GRID_DEFAULT_COLUMNS, THY_GRID_DEFAULT_COLUMNS);
×
117
        const xGap = this.thyXGap();
×
118
        const yGap = this.thyYGap();
×
119
        const gap = this.thyGap();
120
        if (!xGap && !yGap) {
121
            this.xGap = this.calculateActualValue(gap || 0);
122
            this.yGap = this.xGap;
×
123
        } else {
×
124
            this.xGap = this.calculateActualValue(xGap || gap);
×
125
            this.yGap = this.calculateActualValue(yGap || gap);
×
126
        }
×
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`);
131
    }
×
132

×
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();
×
141
                    this.handleGridItems();
×
142
                });
×
143
        } else {
144
            this.ngZone.runOutsideAngular(() => {
×
145
                this.gridResizeObserver(this.elementRef.nativeElement)
×
146
                    .pipe(throttleTime(100), this.takeUntilDestroyed)
×
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
            }
×
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) => {
×
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

© 2025 Coveralls, Inc