mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(input-otp): add new input-otp component (#30386)
Adds a new component `ion-input-otp` which provides the OTP input functionality - Displays as an input group with multiple boxes accepting a single character - Accepts `type` which determines whether the boxes accept numbers or text/numbers and determines the keyboard to display - Supports changing the displayed keyboard using the `inputmode` property - Accepts a `length` property to control the number of input boxes - Accepts the following properties to change the design: `fill`, `shape`, `size`, `color` - Accepts a `separators` property to show a separator between 1 or more input boxes - Supports the `disabled`, `readonly` and invalid states - Supports limiting the accepted input via the `pattern` property - Emits the following events: `ionInput`, `ionChange`, `ionComplete`, `ionBlur`, `ionFocus` - Exposes the following method: `setFocus` --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Co-authored-by: Shane <shane@shanessite.net>
This commit is contained in:
@@ -7,6 +7,8 @@ describe('Form', () => {
|
||||
it('should update Ionic form classes when calling form methods programmatically', async () => {
|
||||
cy.get('#input-touched').click();
|
||||
cy.get('#touched-input-test').should('have.class', 'ion-touched');
|
||||
cy.get('#input-otp-touched').click();
|
||||
cy.get('#touched-input-otp-number-test').should('have.class', 'ion-touched');
|
||||
});
|
||||
|
||||
describe('markAllAsTouched', () => {
|
||||
@@ -32,6 +34,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: null
|
||||
});
|
||||
@@ -41,6 +46,15 @@ describe('Form', () => {
|
||||
cy.get('ion-input.required').type('Some value');
|
||||
cy.get('ion-input.required input').blur();
|
||||
|
||||
// Test number OTP input
|
||||
cy.get('#touched-input-otp-number-test input').first().type('5678');
|
||||
cy.get('#touched-input-otp-number-test input').last().focus().blur();
|
||||
|
||||
|
||||
// Test text OTP input
|
||||
cy.get('#touched-input-otp-text-test input').first().type('ABCD');
|
||||
cy.get('#touched-input-otp-text-test input').last().focus().blur();
|
||||
|
||||
testStatus('INVALID');
|
||||
|
||||
cy.get('ion-select').click();
|
||||
@@ -60,6 +74,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: 5678,
|
||||
inputOtpText: 'ABCD',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: null
|
||||
});
|
||||
@@ -75,6 +92,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: null
|
||||
});
|
||||
@@ -90,6 +110,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: true,
|
||||
radio: null
|
||||
});
|
||||
@@ -105,8 +128,36 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: 'nes'
|
||||
radio: 'nes',
|
||||
});
|
||||
});
|
||||
|
||||
it('ion-input-otp should change', () => {
|
||||
// Test number OTP input
|
||||
cy.get('#touched-input-otp-number-test input').first().type('5678');
|
||||
cy.get('#touched-input-otp-number-test input').last().focus().blur();
|
||||
|
||||
// Test text OTP input
|
||||
cy.get('#touched-input-otp-text-test input').first().type('ABCD');
|
||||
cy.get('#touched-input-otp-text-test input').last().focus().blur();
|
||||
|
||||
testData({
|
||||
datetime: '2010-08-20',
|
||||
select: null,
|
||||
toggle: false,
|
||||
input: '',
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: 5678,
|
||||
inputOtpText: 'ABCD',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: null
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,6 +166,38 @@ describe('Form', () => {
|
||||
cy.get('#submit-button').click();
|
||||
cy.get('#submit').should('have.text', 'true');
|
||||
});
|
||||
|
||||
it('ion-input-otp should validate both number and text types', () => {
|
||||
// Test number OTP validation
|
||||
cy.get('#touched-input-otp-number-test').should('have.class', 'ng-invalid');
|
||||
cy.get('#touched-input-otp-number-test input').first().type('5678');
|
||||
cy.get('#touched-input-otp-number-test input').last().focus().blur();
|
||||
cy.get('#touched-input-otp-number-test').should('have.class', 'ng-valid');
|
||||
|
||||
// Test text OTP validation
|
||||
cy.get('#touched-input-otp-text-test').should('have.class', 'ng-invalid');
|
||||
cy.get('#touched-input-otp-text-test input').first().type('ABCD');
|
||||
cy.get('#touched-input-otp-text-test input').last().focus().blur();
|
||||
cy.get('#touched-input-otp-text-test').should('have.class', 'ng-valid');
|
||||
});
|
||||
|
||||
// Add test for partial OTP input validation
|
||||
it('ion-input-otp should remain invalid when partially filled', () => {
|
||||
// Test number OTP with only first digit
|
||||
cy.get('#touched-input-otp-number-test').should('have.class', 'ng-invalid');
|
||||
cy.get('#touched-input-otp-number-test input').first().type('5');
|
||||
cy.get('#touched-input-otp-number-test input').eq(1).focus().blur();
|
||||
cy.get('#touched-input-otp-number-test').should('have.class', 'ng-invalid');
|
||||
|
||||
// Test text OTP with only first character
|
||||
cy.get('#touched-input-otp-text-test').should('have.class', 'ng-invalid');
|
||||
cy.get('#touched-input-otp-text-test input').first().type('A');
|
||||
cy.get('#touched-input-otp-text-test input').eq(1).focus().blur();
|
||||
cy.get('#touched-input-otp-text-test').should('have.class', 'ng-invalid');
|
||||
|
||||
// Verify form status is still invalid
|
||||
testStatus('INVALID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blur', () => {
|
||||
@@ -128,6 +211,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: false,
|
||||
radio: null
|
||||
});
|
||||
@@ -140,6 +226,9 @@ describe('Form', () => {
|
||||
input2: 'Default Value',
|
||||
inputMin: 1,
|
||||
inputMax: 1,
|
||||
inputOtp: null,
|
||||
inputOtpText: '',
|
||||
inputOtp2: 1234,
|
||||
checkbox: true,
|
||||
radio: null
|
||||
});
|
||||
|
||||
@@ -3,17 +3,18 @@ describe('Inputs', () => {
|
||||
cy.visit('/lazy/inputs');
|
||||
})
|
||||
|
||||
it('should have default value', () => {
|
||||
it('should have default values', () => {
|
||||
cy.get('ion-checkbox').should('have.prop', 'checked').and('equal', true);
|
||||
cy.get('ion-radio-group').should('have.prop', 'value').and('equal', 'nes');
|
||||
cy.get('ion-toggle').should('have.prop', 'checked').and('equal', true);
|
||||
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
|
||||
cy.get('ion-input-otp').should('have.prop', 'value').and('equal', '1234');
|
||||
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
||||
cy.get('ion-range').should('have.prop', 'value').and('equal', 50);
|
||||
});
|
||||
|
||||
it('should have reset value', () => {
|
||||
it('should reset values', () => {
|
||||
cy.get('#reset-button').click();
|
||||
|
||||
cy.get('ion-checkbox').should('have.prop', 'checked').and('equal', false);
|
||||
@@ -26,12 +27,13 @@ describe('Inputs', () => {
|
||||
* value property is undefined.
|
||||
*/
|
||||
cy.get('ion-input').should('not.have.prop', 'value');
|
||||
cy.get('ion-input-otp').should('not.have.prop', 'value');
|
||||
cy.get('ion-datetime').should('not.have.prop', 'value');
|
||||
cy.get('ion-select').should('not.have.prop', 'value');
|
||||
cy.get('ion-range').should('not.have.prop', 'value');
|
||||
});
|
||||
|
||||
it('should get some value', () => {
|
||||
it('should set values', () => {
|
||||
cy.get('#reset-button').click();
|
||||
cy.get('#set-button').click();
|
||||
|
||||
@@ -39,12 +41,13 @@ describe('Inputs', () => {
|
||||
cy.get('ion-radio-group').should('have.prop', 'value').and('equal', 'nes');
|
||||
cy.get('ion-toggle').should('have.prop', 'checked').and('equal', true);
|
||||
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some text');
|
||||
cy.get('ion-input-otp').should('have.prop', 'value').and('equal', '1234');
|
||||
cy.get('ion-datetime').should('have.prop', 'value').and('equal', '1994-03-15');
|
||||
cy.get('ion-select').should('have.prop', 'value').and('equal', 'nes');
|
||||
cy.get('ion-range').should('have.prop', 'value').and('equal', 50);
|
||||
});
|
||||
|
||||
it('change values should update angular', () => {
|
||||
it('should update angular when values change', () => {
|
||||
cy.get('#reset-button').click();
|
||||
|
||||
cy.get('ion-checkbox#first-checkbox').click();
|
||||
@@ -52,7 +55,10 @@ describe('Inputs', () => {
|
||||
cy.get('ion-toggle').first().click();
|
||||
|
||||
cy.get('ion-input').eq(0).type('hola');
|
||||
cy.get('ion-input input').eq(0).blur();
|
||||
cy.focused().blur();
|
||||
|
||||
cy.get('ion-input-otp input').eq(0).type('1234');
|
||||
cy.focused().blur();
|
||||
|
||||
// Set date to 1994-03-14
|
||||
cy.get('ion-datetime').first().shadow().find('.calendar-day:not([disabled])').first().click();
|
||||
@@ -68,7 +74,18 @@ describe('Inputs', () => {
|
||||
cy.get('#radio-note').should('have.text', 'nes');
|
||||
cy.get('#toggle-note').should('have.text', 'true');
|
||||
cy.get('#input-note').should('have.text', 'hola');
|
||||
cy.get('#input-otp-note').should('have.text', '1234');
|
||||
cy.get('#datetime-note').should('have.text', '1994-03-14');
|
||||
cy.get('#select-note').should('have.text', 'ps');
|
||||
});
|
||||
|
||||
it('should update values when erasing input', () => {
|
||||
cy.get('ion-input').eq(0).type('{backspace}');
|
||||
cy.get('ion-input').should('have.prop', 'value').and('equal', 'some tex');
|
||||
cy.get('#input-note').should('have.text', 'some tex');
|
||||
|
||||
cy.get('ion-input-otp input:last').eq(0).type('{backspace}');
|
||||
cy.get('ion-input-otp').should('have.prop', 'value').and('equal', '123');
|
||||
cy.get('#input-otp-note').should('have.text', '123');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
describe('Value Accessors', () => {
|
||||
|
||||
describe('Checkbox', () => {
|
||||
@@ -147,4 +146,79 @@ describe('Value Accessors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input OTP', () => {
|
||||
beforeEach(() => cy.visit('/standalone/value-accessors/input-otp'));
|
||||
|
||||
it('should update the form value', () => {
|
||||
cy.get('#formValue').should('have.text', JSON.stringify({
|
||||
inputOtpString: '',
|
||||
inputOtpNumber: ''
|
||||
}, null, 2));
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-pristine');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-pristine');
|
||||
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-invalid');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-invalid');
|
||||
|
||||
// Type into the string OTP input
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(0).type('a');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(1).type('b');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(2).type('c');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(3).type('d');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(3).blur();
|
||||
|
||||
// Type into the number OTP input
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(0).type('1');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(1).type('2');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(2).type('3');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(3).type('4');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(3).blur();
|
||||
|
||||
cy.get('#formValue').should('have.text', JSON.stringify({
|
||||
inputOtpString: 'abcd',
|
||||
inputOtpNumber: 1234
|
||||
}, null, 2));
|
||||
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-dirty');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-dirty');
|
||||
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-valid');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-valid');
|
||||
});
|
||||
|
||||
it('should remain invalid when partially filled', () => {
|
||||
cy.get('#formValue').should('have.text', JSON.stringify({
|
||||
inputOtpString: '',
|
||||
inputOtpNumber: ''
|
||||
}, null, 2));
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-pristine');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-pristine');
|
||||
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-invalid');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-invalid');
|
||||
|
||||
// Type only 2 characters into the string OTP input
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(0).type('a');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(1).type('b');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"] input').eq(2).blur();
|
||||
|
||||
// Type only 2 characters into the number OTP input
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(0).type('1');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(1).type('2');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"] input').eq(2).blur();
|
||||
|
||||
cy.get('#formValue').should('have.text', JSON.stringify({
|
||||
inputOtpString: 'ab',
|
||||
inputOtpNumber: 12
|
||||
}, null, 2));
|
||||
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-dirty');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-dirty');
|
||||
|
||||
// Verify both inputs remain invalid when partially filled
|
||||
cy.get('ion-input-otp[formControlName="inputOtpString"]').should('have.class', 'ion-invalid');
|
||||
cy.get('ion-input-otp[formControlName="inputOtpNumber"]').should('have.class', 'ion-invalid');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -44,6 +44,20 @@
|
||||
<ion-input label="Input" formControlName="input2"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-input-otp id="touched-input-otp-number-test" formControlName="inputOtp" class="required">Input OTP (required)</ion-input-otp>
|
||||
</ion-item>
|
||||
|
||||
<ion-button id="input-otp-touched" (click)="setOtpTouched()">Set Input OTP Touched</ion-button>
|
||||
|
||||
<ion-item>
|
||||
<ion-input-otp id="touched-input-otp-text-test" type="text" formControlName="inputOtpText" class="required">Input OTP Text (required)</ion-input-otp>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-input-otp formControlName="inputOtp2">Input OTP</ion-input-otp>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-checkbox formControlName="checkbox"> Checkbox </ion-checkbox>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl, AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
|
||||
function otpRequiredLength(length: number) {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
if (!value || value.toString().length !== length) {
|
||||
return { otpLength: true };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-form',
|
||||
templateUrl: './form.component.html',
|
||||
@@ -19,6 +28,9 @@ export class FormComponent {
|
||||
toggle: [false],
|
||||
input: ['', Validators.required],
|
||||
input2: ['Default Value'],
|
||||
inputOtp: [null, [Validators.required, otpRequiredLength(4)]],
|
||||
inputOtpText: ['', [Validators.required, otpRequiredLength(4)]],
|
||||
inputOtp2: [1234],
|
||||
inputMin: [1, Validators.min(1)],
|
||||
inputMax: [1, Validators.max(1)],
|
||||
checkbox: [false],
|
||||
@@ -35,6 +47,13 @@ export class FormComponent {
|
||||
}
|
||||
}
|
||||
|
||||
setOtpTouched() {
|
||||
const formControl = this.profileForm.get('inputOtp');
|
||||
if (formControl) {
|
||||
formControl.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = 'true';
|
||||
}
|
||||
@@ -46,6 +65,9 @@ export class FormComponent {
|
||||
toggle: true,
|
||||
input: 'Some value',
|
||||
input2: 'Another values',
|
||||
inputOtp: 5678,
|
||||
inputOtpText: 'ABCD',
|
||||
inputOtp2: 1234,
|
||||
checkbox: true,
|
||||
radio: 'nes'
|
||||
});
|
||||
|
||||
@@ -71,6 +71,16 @@
|
||||
<ion-note slot="end">{{input}}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-input-otp [(ngModel)]="inputOtp">Input OTP</ion-input-otp>
|
||||
<ion-note slot="end" id="input-otp-note">{{inputOtp}}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item color="dark">
|
||||
<ion-input-otp [(ngModel)]="inputOtp">Input OTP Mirror</ion-input-otp>
|
||||
<ion-note slot="end">{{inputOtp}}</ion-note>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-checkbox [(ngModel)]="checkbox" slot="start" id="first-checkbox">
|
||||
Checkbox
|
||||
|
||||
@@ -9,6 +9,7 @@ export class InputsComponent {
|
||||
|
||||
datetime? = '1994-03-15';
|
||||
input? = 'some text';
|
||||
inputOtp? = '1234';
|
||||
checkbox = true;
|
||||
radio? = 'nes';
|
||||
toggle = true;
|
||||
@@ -20,6 +21,7 @@ export class InputsComponent {
|
||||
console.log('set values');
|
||||
this.datetime = '1994-03-15';
|
||||
this.input = 'some text';
|
||||
this.inputOtp = '1234';
|
||||
this.checkbox = true;
|
||||
this.radio = 'nes';
|
||||
this.toggle = true;
|
||||
@@ -31,6 +33,7 @@ export class InputsComponent {
|
||||
console.log('reset values');
|
||||
this.datetime = undefined;
|
||||
this.input = undefined;
|
||||
this.inputOtp = undefined;
|
||||
this.checkbox = false;
|
||||
this.radio = undefined;
|
||||
this.toggle = false;
|
||||
|
||||
@@ -44,6 +44,7 @@ export const routes: Routes = [
|
||||
{ path: 'checkbox', loadComponent: () => import('../value-accessors/checkbox/checkbox.component').then(c => c.CheckboxComponent) },
|
||||
{ path: 'datetime', loadComponent: () => import('../value-accessors/datetime/datetime.component').then(c => c.DatetimeComponent) },
|
||||
{ path: 'input', loadComponent: () => import('../value-accessors/input/input.component').then(c => c.InputComponent) },
|
||||
{ path: 'input-otp', loadComponent: () => import('../value-accessors/input-otp/input-otp.component').then(c => c.InputOtpComponent) },
|
||||
{ path: 'radio-group', loadComponent: () => import('../value-accessors/radio-group/radio-group.component').then(c => c.RadioGroupComponent) },
|
||||
{ path: 'range', loadComponent: () => import('../value-accessors/range/range.component').then(c => c.RangeComponent) },
|
||||
{ path: 'searchbar', loadComponent: () => import('../value-accessors/searchbar/searchbar.component').then(c => c.SearchbarComponent) },
|
||||
|
||||
@@ -116,6 +116,11 @@
|
||||
Input Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/value-accessors/input-otp">
|
||||
<ion-label>
|
||||
Input OTP Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/value-accessors/radio-group">
|
||||
<ion-label>
|
||||
Radio Group Test
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<h1>IonInputOtp Value Accessors</h1>
|
||||
<p>
|
||||
This test checks the form integrations with ion-input-otp to make sure values are correctly assigned to the form group.
|
||||
</p>
|
||||
|
||||
<app-value-accessor-test [formGroup]="form">
|
||||
<ion-input-otp type="text" formControlName="inputOtpString">String</ion-input-otp>
|
||||
<ion-input-otp type="number" formControlName="inputOtpNumber">Number</ion-input-otp>
|
||||
</app-value-accessor-test>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { IonInputOtp } from "@ionic/angular/standalone";
|
||||
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
|
||||
|
||||
function otpRequiredLength(length: number) {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
if (!value || value.toString().length !== length) {
|
||||
return { otpLength: true };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-otp',
|
||||
templateUrl: 'input-otp.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
IonInputOtp,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
ValueAccessorTestComponent
|
||||
]
|
||||
})
|
||||
export class InputOtpComponent {
|
||||
form = this.fb.group({
|
||||
inputOtpString: ['', [Validators.required, otpRequiredLength(4)]],
|
||||
inputOtpNumber: ['', [Validators.required, otpRequiredLength(4)]],
|
||||
});
|
||||
|
||||
constructor(private fb: FormBuilder) { }
|
||||
}
|
||||
@@ -1,41 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# The directory where the source
|
||||
# for each specific application is.
|
||||
# Directory containing test application versions
|
||||
# (e.g. ng18, ng19, etc.)
|
||||
APPS_DIR="apps"
|
||||
|
||||
# The directory where the
|
||||
# base application logic is
|
||||
# Base application template that all test apps are built from
|
||||
BASE_DIR="base"
|
||||
|
||||
# Output directory for built applications
|
||||
BUILD_DIR="build"
|
||||
|
||||
# The specific application
|
||||
# we are building
|
||||
# Application version to build (passed as first argument)
|
||||
APP_DIR="${1}"
|
||||
|
||||
# The full path to the specific application.
|
||||
# Full paths for source and destination
|
||||
FULL_APP_DIR="${APPS_DIR}/${APP_DIR}/."
|
||||
|
||||
# The full path to the base application.
|
||||
FULL_BASE_DIR="${BASE_DIR}/."
|
||||
|
||||
# The full path to the built application.
|
||||
BUILD_APP_DIR="${BUILD_DIR}/${APP_DIR}/"
|
||||
|
||||
# Make sure the full app directory exists.
|
||||
# Verify application version exists
|
||||
if [ ! -d $FULL_APP_DIR ]; then
|
||||
echo "Could not find test app: ${FULL_APP_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make the build directory if it does not already exist.
|
||||
# Create build directory if needed
|
||||
mkdir -p $BUILD_DIR
|
||||
|
||||
# First we need to copy the base application
|
||||
# Copy base template first
|
||||
echo "Copying base application..."
|
||||
cp -R $FULL_BASE_DIR $BUILD_APP_DIR
|
||||
|
||||
# Then we can copy the specific app which
|
||||
# will override any files in the base application.
|
||||
# Copy version-specific files (overrides base template)
|
||||
echo "Copying application version..."
|
||||
cp -R $FULL_APP_DIR $BUILD_APP_DIR
|
||||
|
||||
echo "Copied test app files for ${APP_DIR}"
|
||||
|
||||
Reference in New Issue
Block a user