diff --git a/projects/cdk/pipes/index.ts b/projects/cdk/pipes/index.ts index 69882befd8836..da4ac658ec1cd 100644 --- a/projects/cdk/pipes/index.ts +++ b/projects/cdk/pipes/index.ts @@ -2,5 +2,6 @@ export * from '@taiga-ui/cdk/pipes/filter'; export * from '@taiga-ui/cdk/pipes/is-present'; export * from '@taiga-ui/cdk/pipes/keys'; export * from '@taiga-ui/cdk/pipes/mapper'; +export * from '@taiga-ui/cdk/pipes/repeat-times'; export * from '@taiga-ui/cdk/pipes/replace'; export * from '@taiga-ui/cdk/pipes/to-array'; diff --git a/projects/cdk/pipes/repeat-times/index.ts b/projects/cdk/pipes/repeat-times/index.ts new file mode 100644 index 0000000000000..99193635e19a4 --- /dev/null +++ b/projects/cdk/pipes/repeat-times/index.ts @@ -0,0 +1,14 @@ +import {Pipe, type PipeTransform} from '@angular/core'; +import type {TuiIterationRange} from '@taiga-ui/cdk/types'; + +type Range = TuiIterationRange; + +@Pipe({ + standalone: true, + name: 'tuiRepeatTimes', +}) +export class TuiRepeatTimesPipe implements PipeTransform { + public transform(length: N): Range<1, N> { + return Array.from({length}, (_, i) => ++i) as unknown as Range<1, N>; + } +} diff --git a/projects/cdk/pipes/repeat-times/ng-package.json b/projects/cdk/pipes/repeat-times/ng-package.json new file mode 100644 index 0000000000000..bebf62dcb5e51 --- /dev/null +++ b/projects/cdk/pipes/repeat-times/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/projects/cdk/pipes/repeat-times/test/repeat-times.pipe.spec.ts b/projects/cdk/pipes/repeat-times/test/repeat-times.pipe.spec.ts new file mode 100644 index 0000000000000..d179bdde781c9 --- /dev/null +++ b/projects/cdk/pipes/repeat-times/test/repeat-times.pipe.spec.ts @@ -0,0 +1,21 @@ +import {TuiRepeatTimesPipe} from '@taiga-ui/cdk'; + +describe('TuiRepeatTimes pipe', () => { + const pipe = new TuiRepeatTimesPipe(); + + it('repeat works', () => { + const arr = pipe.transform(5); + + expect(arr).toEqual([1, 2, 3, 4, 5]); + + expect(pipe.transform(0)).toEqual([]); + expect(pipe.transform(1)).toEqual([1]); + expect(pipe.transform(2)).toEqual([1, 2]); + expect(pipe.transform(3)).toEqual([1, 2, 3]); + }); + + it('repeat pipe should not compile and cannot be less than minimum value', () => { + // @ts-expect-error + expect(pipe.transform(-1)).toEqual([]); + }); +}); diff --git a/projects/cdk/types/index.ts b/projects/cdk/types/index.ts index 5fd11e797a865..a6f162ce5bb31 100644 --- a/projects/cdk/types/index.ts +++ b/projects/cdk/types/index.ts @@ -3,6 +3,7 @@ export * from './handler'; export * from './loose-union'; export * from './mapper'; export * from './matcher'; +export * from './range'; export * from './rounding'; export * from './safe-html'; export * from './values-of'; diff --git a/projects/cdk/types/range.ts b/projects/cdk/types/range.ts new file mode 100644 index 0000000000000..9e954d6bef750 --- /dev/null +++ b/projects/cdk/types/range.ts @@ -0,0 +1,44 @@ +type ComputeRange< + N extends number, + Result extends unknown[] = [], +> = Result['length'] extends N ? Result : ComputeRange; + +type Add = [ + ...ComputeRange, + ...ComputeRange, +]['length']; + +type IsGreater = + IsLiteralNumber<[...ComputeRange][Last<[...ComputeRange]>]> extends true + ? false + : true; + +type Last = T extends [ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ...infer _never, + infer Last, +] + ? Last extends number + ? Last + : never + : never; + +type RemoveLast = T extends [ + ...infer Rest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer _never, +] + ? Rest + : never; + +type IsLiteralNumber = N extends number ? (number extends N ? false : true) : false; + +export type TuiIterationRange< + Min extends number, + Max extends number, + ScaleBy extends number, + Result extends unknown[] = [Min], +> = + IsGreater, Max> extends true + ? RemoveLast + : TuiIterationRange, ScaleBy>]>; diff --git a/projects/demo/src/modules/app/app.routes.ts b/projects/demo/src/modules/app/app.routes.ts index da38a03588ea4..b618ff9d2e7b5 100644 --- a/projects/demo/src/modules/app/app.routes.ts +++ b/projects/demo/src/modules/app/app.routes.ts @@ -970,6 +970,11 @@ export const ROUTES: Routes = [ loadComponent: async () => import('../pipes/mapper'), title: 'Mapper', }), + route({ + path: DemoRoute.RepeatTimes, + loadComponent: async () => import('../pipes/repeat-times'), + title: 'RepeatTimes', + }), route({ path: DemoRoute.Stringify, loadComponent: async () => import('../pipes/stringify'), diff --git a/projects/demo/src/modules/app/demo-routes.ts b/projects/demo/src/modules/app/demo-routes.ts index e2f330554d80d..4614207f9ac80 100644 --- a/projects/demo/src/modules/app/demo-routes.ts +++ b/projects/demo/src/modules/app/demo-routes.ts @@ -192,6 +192,7 @@ export const DemoRoute = { FormatNumber: '/pipes/format-number', IsPresent: '/pipes/is-present', Mapper: '/pipes/mapper', + RepeatTimes: '/pipes/repeat-times', Stringify: '/pipes/stringify', StringifyContent: '/pipes/stringify-content', Alert: '/components/alert', diff --git a/projects/demo/src/modules/app/pages.ts b/projects/demo/src/modules/app/pages.ts index bfbcb7df8e916..2b0dc33def72a 100644 --- a/projects/demo/src/modules/app/pages.ts +++ b/projects/demo/src/modules/app/pages.ts @@ -1368,6 +1368,12 @@ export const pages: TuiDocRoutePages = [ keywords: 'mapper, мап, преобразование, пайп, pipe', route: DemoRoute.Mapper, }, + { + section: 'Tools', + title: 'RepeatTimes', + keywords: 'повторение, repeat, пайп, pipe', + route: DemoRoute.RepeatTimes, + }, { section: 'Tools', title: 'FieldError', diff --git a/projects/demo/src/modules/pipes/repeat-times/examples/1/index.html b/projects/demo/src/modules/pipes/repeat-times/examples/1/index.html new file mode 100644 index 0000000000000..9e018494fc336 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/examples/1/index.html @@ -0,0 +1,3 @@ + + {{ item }} + diff --git a/projects/demo/src/modules/pipes/repeat-times/examples/1/index.ts b/projects/demo/src/modules/pipes/repeat-times/examples/1/index.ts new file mode 100644 index 0000000000000..2931819c12ce3 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/examples/1/index.ts @@ -0,0 +1,14 @@ +import {NgForOf} from '@angular/common'; +import {Component} from '@angular/core'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {encapsulation} from '@demo/emulate/encapsulation'; +import {TuiRepeatTimesPipe} from '@taiga-ui/cdk'; + +@Component({ + standalone: true, + imports: [NgForOf, TuiRepeatTimesPipe], + templateUrl: './index.html', + encapsulation, + changeDetection, +}) +export default class Example {} diff --git a/projects/demo/src/modules/pipes/repeat-times/examples/import/import.md b/projects/demo/src/modules/pipes/repeat-times/examples/import/import.md new file mode 100644 index 0000000000000..2c19517060723 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/examples/import/import.md @@ -0,0 +1,15 @@ +```ts +import {TuiRepeatTimesPipe} from '@taiga-ui/cdk'; + +//... + +@Component({ + standalone: true, + imports: [ + // ... + TuiRepeatTimesPipe, + ], + // ... +}) +export class Example {} +``` diff --git a/projects/demo/src/modules/pipes/repeat-times/examples/import/template.md b/projects/demo/src/modules/pipes/repeat-times/examples/import/template.md new file mode 100644 index 0000000000000..e1f5cb87c40e4 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/examples/import/template.md @@ -0,0 +1,5 @@ +```html +@for (item of 3 | tuiRepeatTimes; track item) { +
+} +``` diff --git a/projects/demo/src/modules/pipes/repeat-times/index.html b/projects/demo/src/modules/pipes/repeat-times/index.html new file mode 100644 index 0000000000000..4eb6ac61ace41 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/index.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/projects/demo/src/modules/pipes/repeat-times/index.ts b/projects/demo/src/modules/pipes/repeat-times/index.ts new file mode 100644 index 0000000000000..a6af1afa2f681 --- /dev/null +++ b/projects/demo/src/modules/pipes/repeat-times/index.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; +import {changeDetection} from '@demo/emulate/change-detection'; +import {TuiDemo} from '@demo/utils'; + +@Component({ + standalone: true, + imports: [TuiDemo], + templateUrl: './index.html', + changeDetection, +}) +export default class Page {}