diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 137773c75..e8d01bc02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: uses: ./.github/actions/nodejs - name: Run tests - run: npx nx run-many --target test --all --coverage + run: npx nx run-many --target test --all - uses: codecov/codecov-action@v2 with: diff --git a/projects/angular/src/index.ts b/projects/angular/src/index.ts index 865bea367..1ddb58875 100644 --- a/projects/angular/src/index.ts +++ b/projects/angular/src/index.ts @@ -1,2 +1,4 @@ +export * from './lib/maskito.cva'; export * from './lib/maskito.directive'; export * from './lib/maskito.module'; +export * from './lib/maskito.pipe'; diff --git a/projects/angular/src/lib/maskito.cva.ts b/projects/angular/src/lib/maskito.cva.ts new file mode 100644 index 000000000..9b2aac1e2 --- /dev/null +++ b/projects/angular/src/lib/maskito.cva.ts @@ -0,0 +1,28 @@ +import {Directive, Input} from '@angular/core'; +import {DefaultValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions, maskitoTransform} from '@maskito/core'; + +@Directive({ + selector: 'input[maskito], textarea[maskito]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: MaskitoCva, + }, + ], + host: { + '(input)': '$any(this)._handleInput($event.target.value)', + '(blur)': 'onTouched()', + '(compositionstart)': '$any(this)._compositionStart()', + '(compositionend)': '$any(this)._compositionEnd($event.target.value)', + }, +}) +export class MaskitoCva extends DefaultValueAccessor { + @Input() + maskito: MaskitoOptions = MASKITO_DEFAULT_OPTIONS; + + override writeValue(value: unknown): void { + super.writeValue(maskitoTransform(String(value ?? ''), this.maskito)); + } +} diff --git a/projects/angular/src/lib/maskito.module.ts b/projects/angular/src/lib/maskito.module.ts index 0a728608c..9e041af29 100644 --- a/projects/angular/src/lib/maskito.module.ts +++ b/projects/angular/src/lib/maskito.module.ts @@ -1,9 +1,11 @@ import {NgModule} from '@angular/core'; +import {MaskitoCva} from './maskito.cva'; import {MaskitoDirective} from './maskito.directive'; +import {MaskitoPipe} from './maskito.pipe'; @NgModule({ - declarations: [MaskitoDirective], - exports: [MaskitoDirective], + declarations: [MaskitoDirective, MaskitoCva, MaskitoPipe], + exports: [MaskitoDirective, MaskitoCva, MaskitoPipe], }) export class MaskitoModule {} diff --git a/projects/angular/src/lib/maskito.pipe.ts b/projects/angular/src/lib/maskito.pipe.ts new file mode 100644 index 000000000..1781be337 --- /dev/null +++ b/projects/angular/src/lib/maskito.pipe.ts @@ -0,0 +1,11 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {MaskitoOptions, maskitoTransform} from '@maskito/core'; + +@Pipe({ + name: 'maskito', +}) +export class MaskitoPipe implements PipeTransform { + transform(value: unknown, maskitoOptions: MaskitoOptions): string { + return maskitoTransform(String(value ?? ''), maskitoOptions); + } +} diff --git a/projects/angular/src/lib/maskito.spec.ts b/projects/angular/src/lib/maskito.spec.ts new file mode 100644 index 000000000..b0ad86a59 --- /dev/null +++ b/projects/angular/src/lib/maskito.spec.ts @@ -0,0 +1,57 @@ +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MaskitoModule} from '@maskito/angular'; +import {maskitoNumberOptionsGenerator} from '@maskito/kit'; + +describe(`Maskito Angular package`, () => { + @Component({ + template: ` +
{{ control.value | maskito: options }}
+ + `, + }) + class TestComponent { + readonly control = new FormControl(); + readonly options = maskitoNumberOptionsGenerator({precision: 2}); + } + + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MaskitoModule, ReactiveFormsModule], + declarations: [TestComponent], + }); + + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); + + it(`Null is treated as empty string`, () => { + expect(getText()).toBe(``); + expect(getValue()).toBe(``); + }); + + it(`Formats new control value`, () => { + fixture.componentInstance.control.setValue(12345.67); + fixture.detectChanges(); + + expect(getText()).toBe(`12\u00A0345.67`); + expect(getValue()).toBe(`12\u00A0345.67`); + }); + + function getText(): string { + return fixture.debugElement.nativeElement + .querySelector('#pipe') + .textContent.trim(); + } + + function getValue(): string { + return fixture.debugElement.nativeElement.querySelector('#input').value; + } +}); diff --git a/projects/demo-integrations/cypress/tests/kit/number/number-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/number/number-basic.cy.ts index 02a86a0ac..cb59a7c74 100644 --- a/projects/demo-integrations/cypress/tests/kit/number/number-basic.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/number/number-basic.cy.ts @@ -107,17 +107,17 @@ describe('Number | Basic', () => { it('can type "–123.45" (en-dash)', () => { cy.get('@input') .type('–123.45') - .should('have.value', '−123,45') - .should('have.prop', 'selectionStart', '−123,45'.length) - .should('have.prop', 'selectionEnd', '−123,45'.length); + .should('have.value', '−123.45') + .should('have.prop', 'selectionStart', '−123.45'.length) + .should('have.prop', 'selectionEnd', '−123.45'.length); }); it('can type "—0,12" (em-dash)', () => { cy.get('@input') .type('—0,12') - .should('have.value', '−0,12') - .should('have.prop', 'selectionStart', '−0,12'.length) - .should('have.prop', 'selectionEnd', '−0,12'.length); + .should('have.value', '−0.12') + .should('have.prop', 'selectionStart', '−0.12'.length) + .should('have.prop', 'selectionEnd', '−0.12'.length); }); }); diff --git a/projects/demo-integrations/cypress/tests/kit/number/number-max-validation.cy.ts b/projects/demo-integrations/cypress/tests/kit/number/number-max-validation.cy.ts index b02c30d37..2e36a606a 100644 --- a/projects/demo-integrations/cypress/tests/kit/number/number-max-validation.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/number/number-max-validation.cy.ts @@ -30,17 +30,17 @@ describe('Number | Max validation', () => { it('0,9999', () => { cy.get('@input') .type(',9999') - .should('have.value', '0,9999') - .should('have.prop', 'selectionStart', '0,9999'.length) - .should('have.prop', 'selectionEnd', '0,9999'.length); + .should('have.value', '0.9999') + .should('have.prop', 'selectionStart', '0.9999'.length) + .should('have.prop', 'selectionEnd', '0.9999'.length); }); it('2,777', () => { cy.get('@input') .type('2,777') - .should('have.value', '2,777') - .should('have.prop', 'selectionStart', '2,777'.length) - .should('have.prop', 'selectionEnd', '2,777'.length); + .should('have.value', '2.777') + .should('have.prop', 'selectionStart', '2.777'.length) + .should('have.prop', 'selectionEnd', '2.777'.length); }); }); diff --git a/projects/demo/src/pages/documentation/angular/angular.component.ts b/projects/demo/src/pages/documentation/angular/angular.component.ts index 50c738413..4d6aa6536 100644 --- a/projects/demo/src/pages/documentation/angular/angular.component.ts +++ b/projects/demo/src/pages/documentation/angular/angular.component.ts @@ -26,4 +26,14 @@ export class AngularDocPageComponent { Default: import('./examples/1-nested/template.html?raw'), Custom: import('./examples/2-nested/template.html?raw'), }; + + readonly cvaExample: TuiDocExample = { + TypeScript: import('./examples/3-cva/component.ts?raw'), + HTML: import('./examples/3-cva/template.html?raw'), + }; + + readonly pipeExample: TuiDocExample = { + TypeScript: import('./examples/4-pipe/component.ts?raw'), + HTML: import('./examples/4-pipe/template.html?raw'), + }; } diff --git a/projects/demo/src/pages/documentation/angular/angular.module.ts b/projects/demo/src/pages/documentation/angular/angular.module.ts index b2a2778bf..3e48e8611 100644 --- a/projects/demo/src/pages/documentation/angular/angular.module.ts +++ b/projects/demo/src/pages/documentation/angular/angular.module.ts @@ -1,6 +1,6 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {RouterModule} from '@angular/router'; import {MaskitoModule} from '@maskito/angular'; import {TuiAddonDocModule, tuiGenerateRoutes} from '@taiga-ui/addon-doc'; @@ -10,20 +10,29 @@ import {TuiCheckboxLabeledModule, TuiInputModule} from '@taiga-ui/kit'; import {AngularDocPageComponent} from './angular.component'; import {NestedDocExample1} from './examples/1-nested/component'; import {NestedDocExample2} from './examples/2-nested/component'; +import {CvaDocExample3} from './examples/3-cva/component'; +import {PipeDocExample4} from './examples/4-pipe/component'; @NgModule({ imports: [ CommonModule, FormsModule, + ReactiveFormsModule, MaskitoModule, TuiInputModule, TuiLinkModule, TuiNotificationModule, + TuiCheckboxLabeledModule, TuiAddonDocModule, RouterModule.forChild(tuiGenerateRoutes(AngularDocPageComponent)), - TuiCheckboxLabeledModule, ], - declarations: [AngularDocPageComponent, NestedDocExample1, NestedDocExample2], + declarations: [ + AngularDocPageComponent, + NestedDocExample1, + NestedDocExample2, + CvaDocExample3, + PipeDocExample4, + ], exports: [AngularDocPageComponent], }) export class AngularDocPageModule {} diff --git a/projects/demo/src/pages/documentation/angular/angular.template.html b/projects/demo/src/pages/documentation/angular/angular.template.html index 1145e0987..3bac76b5e 100644 --- a/projects/demo/src/pages/documentation/angular/angular.template.html +++ b/projects/demo/src/pages/documentation/angular/angular.template.html @@ -108,6 +108,22 @@

Nested input element

[maskitoElement]="example.predicate" > + + + + + + + + diff --git a/projects/demo/src/pages/documentation/angular/examples/3-cva/component.ts b/projects/demo/src/pages/documentation/angular/examples/3-cva/component.ts new file mode 100644 index 000000000..60cdc5001 --- /dev/null +++ b/projects/demo/src/pages/documentation/angular/examples/3-cva/component.ts @@ -0,0 +1,18 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {maskitoNumberOptionsGenerator} from '@maskito/kit'; + +@Component({ + selector: 'cva-doc-example-3', + templateUrl: './template.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CvaDocExample3 { + readonly control = new FormControl(); + + readonly maskito = maskitoNumberOptionsGenerator({precision: 2}); + + setValue(): void { + this.control.setValue(12345.67); + } +} diff --git a/projects/demo/src/pages/documentation/angular/examples/3-cva/template.html b/projects/demo/src/pages/documentation/angular/examples/3-cva/template.html new file mode 100644 index 000000000..a4564b679 --- /dev/null +++ b/projects/demo/src/pages/documentation/angular/examples/3-cva/template.html @@ -0,0 +1,5 @@ + + diff --git a/projects/demo/src/pages/documentation/angular/examples/4-pipe/component.ts b/projects/demo/src/pages/documentation/angular/examples/4-pipe/component.ts new file mode 100644 index 000000000..ba4821b7d --- /dev/null +++ b/projects/demo/src/pages/documentation/angular/examples/4-pipe/component.ts @@ -0,0 +1,13 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {maskitoNumberOptionsGenerator} from '@maskito/kit'; + +@Component({ + selector: 'pipe-doc-example-4', + templateUrl: './template.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PipeDocExample4 { + value = 12345.67; + + readonly options = maskitoNumberOptionsGenerator({precision: 2}); +} diff --git a/projects/demo/src/pages/documentation/angular/examples/4-pipe/template.html b/projects/demo/src/pages/documentation/angular/examples/4-pipe/template.html new file mode 100644 index 000000000..999b852d5 --- /dev/null +++ b/projects/demo/src/pages/documentation/angular/examples/4-pipe/template.html @@ -0,0 +1 @@ +Balance: ${{ value | maskito: options }} diff --git a/projects/demo/src/pages/kit/number/number-mask-doc.component.ts b/projects/demo/src/pages/kit/number/number-mask-doc.component.ts index db5f7219b..0a388a1bf 100644 --- a/projects/demo/src/pages/kit/number/number-mask-doc.component.ts +++ b/projects/demo/src/pages/kit/number/number-mask-doc.component.ts @@ -46,7 +46,7 @@ export class NumberMaskDocComponent implements GeneratorOptions { precision = 0; isNegativeAllowed = true; max = Number.MAX_SAFE_INTEGER; - decimalSeparator = ','; + decimalSeparator = '.'; decimalZeroPadding = false; decimalPseudoSeparators = this.decimalPseudoSeparatorsOptions[0]; thousandSeparator = ' '; diff --git a/projects/demo/src/pages/kit/number/number-mask-doc.template.html b/projects/demo/src/pages/kit/number/number-mask-doc.template.html index ee5ba9e41..0a7650185 100644 --- a/projects/demo/src/pages/kit/number/number-mask-doc.template.html +++ b/projects/demo/src/pages/kit/number/number-mask-doc.template.html @@ -166,7 +166,7 @@

Default: - comma. + dot.

char !== thousandSeparator && char !== decimalSeparator, - ); - } - - return []; +export function getDefaultPseudoSeparators( + decimalSeparator: string, + thousandSeparator: string, +): string[] { + return decimalSeparator === ',' || decimalSeparator === '.' + ? ['.', ',', 'б', 'ю'].filter( + char => char !== thousandSeparator && char !== decimalSeparator, + ) + : []; }