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

atinc / ngx-tethys / 5a4bb0a7-cdd2-4658-8820-cbbe9ed107be

31 Aug 2023 09:12AM UTC coverage: 90.2% (+0.003%) from 90.197%
5a4bb0a7-cdd2-4658-8820-cbbe9ed107be

push

circleci

luxiaobei
feat(grid): add thyFlex, thyFlexItem and thyGrid directives and refactor related components use hostDirectives #INFR-9461

5169 of 6389 branches covered (0.0%)

Branch coverage included in aggregate %.

26 of 26 new or added lines in 4 files covered. (100.0%)

13019 of 13775 relevant lines covered (94.51%)

971.51 hits per line

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

89.47
/src/grid/thy-grid.component.ts
1
import { ViewportRuler } from '@angular/cdk/scrolling';
2
import {
3
    AfterContentInit,
4
    ChangeDetectionStrategy,
5
    Component,
6
    ContentChildren,
7
    Directive,
8
    ElementRef,
9
    Input,
10
    NgZone,
11
    OnChanges,
1✔
12
    OnInit,
1✔
13
    QueryList,
1✔
14
    SimpleChanges,
15
    inject
16
} from '@angular/core';
17
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
18
import { Observable, Subject } from 'rxjs';
19
import { throttleTime } from 'rxjs/operators';
20
import { ThyGridToken, THY_GRID_COMPONENT } from './grid.token';
21
import { ThyGridItemComponent } from './thy-grid-item.component';
22
import { useHostRenderer } from '@tethys/cdk/dom';
23
import { hasLaterChange } from 'ngx-tethys/util';
24

25
export type ThyGridResponsiveMode = 'none' | 'self' | 'screen';
1✔
26

27
export type ThyGridResponsiveDescription = string;
19✔
28

19✔
29
export const THY_GRID_DEFAULT_COLUMNS = 24;
19✔
30

19✔
31
export const THY_GRID_ITEM_DEFAULT_SPAN = 1;
19✔
32

19✔
33
export const screenBreakpointsMap = {
19✔
34
    xs: 0,
19✔
35
    sm: 576,
19✔
36
    md: 768,
19✔
37
    lg: 992,
19✔
38
    xl: 1200
19✔
39
};
40

41
/**
32✔
42
 * 栅格组件
32✔
43
 * @name thy-grid, [thyGrid]
28✔
44
 * @order 10
45
 */
46
@Directive({
47
    selector: '[thyGrid]',
48
    providers: [
27✔
49
        {
27✔
50
            provide: THY_GRID_COMPONENT,
1✔
51
            useExisting: ThyGrid
1✔
52
        }
53
    ],
54
    host: {
55
        class: 'thy-grid'
56
    },
52✔
57
    standalone: true
52✔
58
})
49✔
59
// eslint-disable-next-line @angular-eslint/directive-class-suffix
49✔
60
export class ThyGrid implements ThyGridToken, OnInit, OnChanges, AfterContentInit {
61
    /**
62
     * @internal
3✔
63
     */
3✔
64
    @ContentChildren(ThyGridItemComponent) gridItems!: QueryList<ThyGridItemComponent>;
65

52✔
66
    /**
52✔
67
     * 栅格的列数
52✔
68
     * @default 24
69
     */
70
    @Input() thyCols: number | ThyGridResponsiveDescription = THY_GRID_DEFAULT_COLUMNS;
28✔
71

5✔
72
    /**
73
     * 栅格的水平间隔
74
     */
75
    @Input() thyXGap: number | ThyGridResponsiveDescription = 0;
20✔
76

20✔
77
    /**
20✔
78
     * 栅格的垂直间隔
79
     */
80
    @Input() thyYGap: number | ThyGridResponsiveDescription = 0;
81

23✔
82
    /**
23✔
83
     * 栅格的水平和垂直间隔
84
     */
85
    @Input() thyGap: number | ThyGridResponsiveDescription = 0;
×
86

×
87
    /**
×
88
     * 响应式栅格列数<br/>
89
     * none: 不进行响应式布局。<br/>
90
     * self:根据grid的自身宽度进行响应式布局。<br/>
91
     * screen:根据屏幕断点进行响应式布局,目前预设了5种响应式尺寸:`xs: 0, sm: 576, md: 768, lg: 992, xl: 1200`。
92
     */
93
    @Input() thyResponsive: ThyGridResponsiveMode = 'none';
48✔
94

185✔
95
    private hostRenderer = useHostRenderer();
185✔
96

185✔
97
    private cols: number;
185✔
98

185✔
99
    public xGap: number;
100

48✔
101
    private yGap: number;
102

103
    private numRegex = /^\d+$/;
477✔
104

452✔
105
    private responsiveContainerWidth: number;
106

107
    public gridItemPropValueChange$ = new Subject<void>();
25✔
108

25✔
109
    private takeUntilDestroyed = takeUntilDestroyed();
25✔
110

25✔
111
    constructor(private elementRef: ElementRef, private viewportRuler: ViewportRuler, private ngZone: NgZone) {}
18✔
112

113
    ngOnInit(): void {
7✔
114
        this.setGridStyle();
5✔
115

116
        if (this.thyResponsive !== 'none') {
117
            this.listenResizeEvent();
2✔
118
        }
119
    }
120

121
    ngOnChanges(changes: SimpleChanges): void {}
122

25✔
123
    ngAfterContentInit(): void {
105✔
124
        this.handleGridItems();
22✔
125

126
        this.gridItems.changes.pipe(this.takeUntilDestroyed).subscribe(() => {
105✔
127
            Promise.resolve().then(() => {
105✔
128
                this.handleGridItems();
105✔
129
            });
130
        });
131
    }
132

25!
133
    private setGridStyle() {
25✔
134
        this.cols = this.calculateActualValue(this.thyCols || THY_GRID_DEFAULT_COLUMNS, THY_GRID_DEFAULT_COLUMNS);
25✔
135
        if (!this.thyXGap && !this.thyYGap) {
84✔
136
            this.xGap = this.calculateActualValue(this.thyGap || 0);
105✔
137
            this.yGap = this.xGap;
138
        } else {
139
            this.xGap = this.calculateActualValue(this.thyXGap || this.thyGap);
140
            this.yGap = this.calculateActualValue(this.thyYGap || this.thyGap);
141
        }
×
142

×
143
        this.hostRenderer.setStyle('display', 'grid');
×
144
        this.hostRenderer.setStyle('grid-template-columns', `repeat(${this.cols}, minmax(0, 1fr))`);
×
145
        this.hostRenderer.setStyle('gap', `${this.yGap}px ${this.xGap}px`);
146
    }
147

148
    private listenResizeEvent() {
149
        if (this.thyResponsive === 'screen') {
150
            this.viewportRuler
23✔
151
                .change(100)
23✔
152
                .pipe(this.takeUntilDestroyed)
×
153
                .subscribe(() => {
154
                    this.responsiveContainerWidth = this.viewportRuler.getViewportSize().width;
23✔
155
                    this.setGridStyle();
23✔
156
                    this.handleGridItems();
23✔
157
                });
158
        } else {
159
            this.ngZone.runOutsideAngular(() => {
160
                this.gridResizeObserver(this.elementRef.nativeElement)
1✔
161
                    .pipe(throttleTime(100), this.takeUntilDestroyed)
162
                    .subscribe(data => {
163
                        this.responsiveContainerWidth = data[0]?.contentRect?.width;
164
                        this.setGridStyle();
165
                        this.handleGridItems();
1✔
166
                    });
167
            });
168
        }
169
    }
170

171
    private handleGridItems() {
172
        this.gridItems.forEach((gridItem: ThyGridItemComponent) => {
173
            const rawSpan = getRawSpan(gridItem.thySpan);
174
            const span = this.calculateActualValue(rawSpan, THY_GRID_ITEM_DEFAULT_SPAN);
1✔
175
            const offset = this.calculateActualValue(gridItem.thyOffset || 0);
176

177
            gridItem.span = Math.min(span + offset, this.cols);
178
            gridItem.offset = offset;
179
        });
180

181
        this.gridItemPropValueChange$.next();
182
    }
183

184
    private calculateActualValue(rawValue: number | ThyGridResponsiveDescription, defaultValue?: number): number {
185
        if (this.numRegex.test(rawValue.toString().trim())) {
186
            return Number(rawValue);
187
        } else {
188
            const responsiveValueMap = this.getResponsiveValueMap(rawValue as ThyGridResponsiveDescription);
189
            const breakpointKeys = Object.keys(responsiveValueMap);
190
            const breakpoint = this.calculateBreakPoint(breakpointKeys);
191

192
            if (this.thyResponsive !== 'none' && breakpoint) {
193
                return responsiveValueMap[breakpoint];
194
            } else if (breakpointKeys.includes('0')) {
195
                return responsiveValueMap['0'];
196
            } else {
1✔
197
                return defaultValue || 0;
198
            }
19✔
199
        }
200
    }
201

1✔
202
    private getResponsiveValueMap(responsiveValue: string): { [key: string]: number } {
203
        return responsiveValue.split(' ').reduce((map: { [key: string]: number }, item: string) => {
204
            if (this.numRegex.test(item.toString())) {
205
                item = `0:${item}`;
206
            }
207
            const [key, value] = item.split(':');
208
            map[key] = Number(value);
209
            return map;
210
        }, {});
211
    }
212

213
    private calculateBreakPoint(breakpointKeys: string[]): string {
214
        if (this.thyResponsive === 'screen') {
215
            const width = this.responsiveContainerWidth || this.viewportRuler.getViewportSize().width;
216
            return breakpointKeys.find((key: string, index: number) => {
217
                return index < breakpointKeys.length - 1
218
                    ? width >= screenBreakpointsMap[key] && width < screenBreakpointsMap[breakpointKeys[index + 1]]
219
                    : width >= screenBreakpointsMap[key];
220
            });
221
        } else {
222
            const width = this.responsiveContainerWidth || this.elementRef.nativeElement.getBoundingClientRect().width;
223
            return breakpointKeys.find((key: string, index: number) => {
224
                return index < breakpointKeys.length - 1
185✔
225
                    ? width >= Number(key) && width < Number(breakpointKeys[index + 1])
226
                    : width >= Number(key);
227
            });
228
        }
229
    }
230

231
    private gridResizeObserver(element: HTMLElement): Observable<ResizeObserverEntry[]> {
232
        return new Observable(observer => {
233
            const resize = new ResizeObserver((entries: ResizeObserverEntry[]) => {
234
                observer.next(entries);
235
            });
236
            resize.observe(element);
237
            return () => {
238
                resize.disconnect();
239
            };
240
        });
241
    }
242
}
243

244
/**
245
 * @internal
246
 */
247
@Component({
248
    selector: 'thy-grid',
249
    template: '<ng-content></ng-content>',
250
    changeDetection: ChangeDetectionStrategy.OnPush,
251
    standalone: true,
252
    imports: [ThyGrid],
253
    providers: [
254
        {
255
            provide: THY_GRID_COMPONENT,
256
            useExisting: ThyGrid
257
        }
258
    ],
259
    hostDirectives: [
260
        {
261
            directive: ThyGrid,
262
            inputs: ['thyCols', 'thyXGap', 'thyYGap', 'thyGap', 'thyResponsive']
263
        }
264
    ]
265
})
266
export class ThyGridComponent {
267
    grid = inject(ThyGrid);
268
}
269

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