├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yaml │ └── npm.yaml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects └── ngx-otp-input │ ├── .eslintrc.json │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── default.config.ts │ │ ├── directives │ │ │ ├── ariaLabels.directive.ts │ │ │ ├── autoBlur.directive.ts │ │ │ ├── autoFocus.directive.ts │ │ │ ├── inputNavigations.directive.ts │ │ │ └── paste.directive.ts │ │ ├── ngx-otp-input.component.html │ │ ├── ngx-otp-input.component.scss │ │ ├── ngx-otp-input.component.spec.ts │ │ └── ngx-otp-input.component.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.config.ts │ └── app.routes.ts ├── assets │ └── .gitkeep ├── components │ ├── BasicInformation.component.ts │ └── Header.component.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.scss ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@angular-eslint/recommended", 11 | "plugin:@angular-eslint/template/process-inline-templates", 12 | "prettier" 13 | ], 14 | "rules": {} 15 | }, 16 | { 17 | "files": ["*.html"], 18 | "extends": [ 19 | "plugin:@angular-eslint/template/recommended", 20 | "plugin:@angular-eslint/template/accessibility" 21 | ], 22 | "rules": {} 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: lint-and-test 2 | 3 | run-name: Lint and Test 4 | 5 | on: [pull_request] 6 | 7 | jobs: 8 | lint-and-test: 9 | name: Lint and Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '20' 16 | - run: npm ci 17 | - run: npm run build-lib 18 | - run: npm run lint-lib 19 | - run: npm run test-lib:ci 20 | -------------------------------------------------------------------------------- /.github/workflows/npm.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | # Setup .npmrc file to publish to npm 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '20.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm ci 19 | - run: npm run build-lib:prod 20 | - name: '🚀 Publish' 21 | run: npm publish ./dist/ngx-otp-input --access public 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | .nx 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | .nx 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "singleAttributePerLine": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## 1.1.4 9 | 10 | - [(#32)](https://github.com/pkovzz/ngx-otp-input/issues/35) Fix iOS paste from quickbar 11 | 12 | ## 1.1.3 13 | 14 | - [(#31)](https://github.com/pkovzz/ngx-otp-input/issues/31) Prevent error when pasting more characters than input boxes 15 | 16 | ## 1.1.2 17 | 18 | ### Fixed 19 | 20 | - [(#28)](https://github.com/pkovzz/ngx-otp-input/issues/29) Paste will fire only one _otpChange_ and one _otpComplete_ event 21 | 22 | ## 1.1.1 23 | 24 | ### Fixed 25 | 26 | - [(#27)](https://github.com/pkovzz/ngx-otp-input/issues/27) Android input events 27 | 28 | ## 1.1.0 29 | 30 | ### Added 31 | 32 | - Add `inputMode` configuration option. 33 | 34 | ## 1.0.0 35 | 36 | - Library is now a standalone component and introduced a new API with several breaking changes. Please check the [Documentation](../README.md) for more information. 37 | 38 | ## 0.11.4 39 | 40 | ### Fixed 41 | 42 | - Bugfix of [module import issue](https://github.com/pkovzz/ngx-otp-input/issues/16) and other improvements 43 | 44 | ## 0.11.1 45 | 46 | ### Fixed 47 | 48 | - Bugfix of [Windows copy paste issue](https://github.com/k2peter/ngx-otp-input/issues/11) 49 | 50 | ## 0.11.0 51 | 52 | ### Added 53 | 54 | - Core refactor 55 | - behavior options 56 | 57 | ## 0.9.1 58 | 59 | ### Fixed 60 | 61 | - `[disabled]` input property 62 | 63 | ## 0.9.0 64 | 65 | ### Added 66 | 67 | - `autoblur` configuration option. 68 | For more information, please see the documentation. 69 | 70 | ## 0.8.0 71 | 72 | ### Added 73 | 74 | - `clear` method. 75 | For more information, please see the documentation. 76 | 77 | ## 0.7.0 78 | 79 | ### Added 80 | 81 | - `numericInputMode` config property. 82 | For more information, please see the documentation. 83 | 84 | ## 0.6.6 85 | 86 | ### Fixed 87 | 88 | - missed inputs 89 | 90 | ## 0.6.3 91 | 92 | ### Fixed 93 | 94 | - ngClass error when no custom class is set 95 | 96 | ## 0.6.2 97 | 98 | ### Fixed 99 | 100 | - Applying workaround regard to this: https://github.com/angular/angular/issues/38391 101 | 102 | ## 0.6.1 103 | 104 | ### Fixed 105 | 106 | - Preventing too fast typing, which could cause unregistered inputs 107 | - The custom ["filled"](https://github.com/pkovzz/ngx-otp-input#inputfilled) 108 | style is now applied on every input after _paste_ event 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | We welcome contributions to the project. To contribute, please follow these steps: 4 | 5 | 1. [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). 6 | 2. Create a new branch for your feature or bug fix. 7 | 3. Make your changes. 8 | 9 | #### To run the project locally: 10 | 11 | First, install the dependencies: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Then, run the library in development mode: 18 | 19 | ```bash 20 | npm run build-lib-watch 21 | ``` 22 | 23 | Finally, in a new terminal window, run the demo app: 24 | 25 | ```bash 26 | npm run start 27 | ``` 28 | 29 | 4. After you have made your changes, run the tests: 30 | ```bash 31 | npm run test-lib 32 | ``` 33 | 5. If all tests pass, commit your changes. 34 | 6. Push your changes to your fork. 35 | 7. [Open a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork). 36 | 8. Wait for the maintainers to review your pull request. 37 | 9. If your pull request is approved and merged, it will be included in the next release. 38 | 39 | If you have any questions or need help, please open an issue. If you find a bug or have a feature request, please open an issue. If you would like to contribute but don't know where to start, please check the issues for any open issues that need help. 40 | 41 | Thank you for contributing to the project! 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024, Kovács Péter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-otp-input 2 | 3 | [![License](https://img.shields.io/github/license/pkovzz/ngx-otp-input?style=flat)](./LICENSE.txt) 4 | 5 | :warning: **Important note:** starting with version 1.0.0, the library has been completely rewritten to use standalone components and introduce breaking changes. As the section "Requirements" states, the library now requires Angular 14 or above. 6 | 7 | ## What is this? 8 | 9 | This is a simple Angular library that allows you to create an OTP (One Time Password) form by providing a set of options. The library is designed to be easy to use and highly customizable, allowing you to configure the form to suit your needs. If you like the library, please consider giving it a star on GitHub. 10 | 11 | ### Demo page 12 | 13 | http://ngx-otp-input.vercel.app 14 | 15 | ## Requirements 16 | 17 | To use this library, your project must be running **Angular 14** or above. This requirement stems from our adoption of standalone components, which are an integral feature in Angular's modern development approach. Standalone components offer a more streamlined and modular way to manage your components and simplify the dependency management, positioning them as the future of Angular development. 18 | 19 | ## Installation 20 | 21 | To install this library, run: 22 | 23 | ```bash 24 | npm install ngx-otp-input --save 25 | ``` 26 | 27 | ## Example Usage 28 | 29 | Since the library uses standalone components, you can directly import and use them in your Angular application without needing to declare them in any module. For more configuration options, refer to the [Configuration options](#configuration-options) section. 30 | 31 | ```typescript 32 | import { Component } from '@angular/core'; 33 | import { NgxOtpInputComponent, NgxOtpInputComponentOptions } from 'ngx-otp-input'; 34 | 35 | @Component({ 36 | selector: 'app-root', 37 | standalone: true, 38 | imports: [NgxOtpInputComponent], 39 | template: ` 40 |

Welcome to My Angular App

41 | 42 | `, 43 | styleUrls: ['./app.component.scss'], 44 | }) 45 | export class AppComponent { 46 | otpOptions: NgxOtpInputComponentOptions = {...}; 47 | } 48 | ``` 49 | 50 | ## Inputs 51 | 52 | ### `options: NgxOtpInputComponentOptions` 53 | 54 | The `options` input is an object that allows you to configure the OTP form. For a list of available options, refer to the [Configuration options](#configuration-options) section. 55 | 56 | ### `otp: string | null | undefined` 57 | 58 | The `otp` input is a string that allows you to set the OTP value of the form. This input is useful when you want to pre-fill the form with an OTP value. If the `otp` input is set to `null` or `undefined`, the form will be empty. The library will match the length of the OTP value with the `otpLength` option and fill the input fields accordingly, in case the OTP value is shorter than the `otpLength` option, the remaining fields will be empty. If the given value will not match the `regexp` option, the library will throw an error. 59 | 60 | ### `status: NgxOtpStatus | null | undefined` 61 | 62 | The `status` input is a string that allows you to set the status of the OTP form. The status can be one of the following values: `null`, `undefined`, `'success'` or `'failed'`. This status is only used to visually indicate the result of the OTP verification process. 63 | 64 | For type safety, you can use the `NgxOtpStatus` enum: 65 | 66 | ```typescript 67 | import { NgxOtpStatus } from 'ngx-otp-input'; 68 | 69 | @Component({ 70 | selector: 'app-root', 71 | standalone: true, 72 | imports: [NgxOtpInputComponent], 73 | template: ` `, 74 | }) 75 | export class AppComponent { 76 | status = NgxOtpStatus; 77 | } 78 | ``` 79 | 80 | ### `disabled: boolean` 81 | 82 | The `disabled` input is a boolean that allows you to disable the OTP form. When set to `true`, the form will be disabled and the user will not be able to interact with it. 83 | 84 | ## Outputs 85 | 86 | ### `otpChange: string[]` 87 | 88 | The `otpChange` output is an event that is emitted whenever the OTP value changes. The event payload is an array of strings, where each string represents a value in the OTP form. 89 | 90 | ### `otpComplete: string` 91 | 92 | The `otpComplete` output is an event that is emitted whenever the OTP form is completed. The event payload is string, which represents the complete OTP value. 93 | 94 | ## Configuration options 95 | 96 | The `NgxOtpInputComponentOptions` interface allows you to configure the OTP form. The following options are available: 97 | 98 | | Option | Type | Default value | Description | 99 | | -------------------- | -------- | ------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 100 | | `otpLength` | number | 6 | The number of inputs in the OTP form | 101 | | `autoFocus` | boolean | true | Whether the first input should be focused automatically | 102 | | `autoBlur` | boolean | true | Whether the form should be blurred on complete | 103 | | `hideInputValues` | boolean | false | Whether the input values should be shown as password fields | 104 | | `regexp` | RegExp | /^[0-9]+$/ | The regular expression that the input values should match | 105 | | `showBlinkingCursor` | boolean | true | Whether the input fields should have a blinking cursor | 106 | | `ariaLabels` | string[] | [] | An array of strings that represent the aria-labels for each input field. For more information, please refer to the [More on aria-labels](#more-on-aria-labels) section. | 107 | | `inputMode` | string | 'numeric' | The `inputmode` attribute of the input fields. For more information, please refer to the [HTML `inputmode` attribute](#html-inputmode-attribute) section. | 108 | 109 | ## Styling 110 | 111 | The library provides a set of CSS classes that you can use to style the OTP form. The following classes are available: 112 | 113 | | Class name | Description | 114 | | ------------------------ | -------------------------------------- | 115 | | `ngx-otp-input-form` | The main container of the OTP form | 116 | | `ngx-otp-input-box` | The input field of the OTP form | 117 | | `ngx-blinking-cursor` | The blinking cursor of the input field | 118 | | `ngx-otp-input-disabled` | The disabled state of the form | 119 | | `ngx-otp-input-filled` | The filled state of an input field | 120 | | `ngx-otp-input-success` | The success state of the form | 121 | | `nngx-otp-input-failed` | The failed state of the form | 122 | 123 | ### How to use the classes 124 | 125 | Styling is quite simple, but you have to use the classes directly in **root** style file, otherwise it will not work: 126 | 127 | ```scss 128 | ngx-otp-input { 129 | .ngx-otp-input-form { 130 | ... 131 | } 132 | .ngx-otp-input-box { 133 | ... 134 | } 135 | ... 136 | } 137 | ``` 138 | 139 | ## Reset the form 140 | 141 | In order to reset the form, you can use the `reset` method of the `NgxOtpInputComponent`: 142 | 143 | First, get a reference to the component in your template: 144 | 145 | ```html 146 | 150 | ``` 151 | 152 | Then, get a reference to the component in your component class: 153 | 154 | ```typescript 155 | import { Component, ViewChild } from '@angular/core'; 156 | import { NgxOtpInputComponent } from 'ngx-otp-input'; 157 | 158 | @Component({ 159 | selector: 'app-root', 160 | standalone: true, 161 | imports: [NgxOtpInputComponent], 162 | template: ` 163 | 167 | `, 168 | }) 169 | export class AppComponent { 170 | @ViewChild('otpInput') otpInput: NgxOtpInputComponent; 171 | 172 | resetForm() { 173 | this.otpInput.reset(); 174 | } 175 | } 176 | ``` 177 | 178 | Under the hood, the `reset` method will clear all the input values and reset the form to its initial state. For more information, refer to the [Angular FormArray reset](https://angular.dev/api/forms/FormArray#reset) documentation. 179 | 180 | ## More on aria-labels 181 | 182 | The `ariaLabels` option allows you to provide a set of strings that represent the aria-labels for each input field. This option is useful for making the form more accessible to users who rely on screen readers. The `aria-label` attribute provides a way to specify a string that labels the current element, which can be read by screen readers to provide additional context to the user. The library will automatically assign the `aria-label` attribute to each input with a default value of `One Time Password Input Number` followed by the input index. However, you can override this default value by providing your own set of labels in the `ariaLabels` option. 183 | 184 | If you provide an array of strings in the `ariaLabels` option, the library will use the values in the array to assign the `aria-label` attribute to each input field. The array should contain the same number of strings as the `otpLength` option, with each string representing the label for the corresponding input field. If the array contains fewer strings than the `otpLength` option, the library will use the default value for the remaining input fields. 185 | 186 | ## HTML `inputmode` attribute 187 | 188 | The `inputMode` option allows you to set the `inputmode` attribute of the input fields. The `inputmode` attribute provides a hint to the browser about the type of data that is expected to be entered by the user. This hint can help the browser provide a more appropriate **virtual keyboard** layout for the input field, making it easier for the user to enter the correct data. The `inputMode` option accepts a string value that represents the input mode of the input fields. For more details, check out this [documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode). 189 | 190 | Please note, that `regexp` option should be set to support the `inputmode` attribute. For example, if you set the `inputMode` option to `text` and the `regexp` option to `/^[a-zA-Z]+$/`, the browser will provide a virtual keyboard layout that is optimized for entering text data, but if the `inputMode` option is set to `numeric` and the `regexp` is still `/^[a-zA-Z]+$/`, the browser may provide a numeric keyboard layout, which may not be suitable for entering text data. 191 | 192 | **Default** options for `inputMode` and `regexp` are set to `'numeric'` and `/^[0-9]+$/` respectively, as these are the most common values for one time password inputs. 193 | 194 | ## Side notes 195 | 196 | If `hideInputValues` is set to `true`, the input values will be hidden by default, using the `password` input type. However certain password managers may place their browser extension icon on the input field, which may interfere with the input field's appearance. 197 | 198 | ## Contributing 199 | 200 | If you would like to contribute to this project, please refer to the [CONTRIBUTING](CONTRIBUTING.md) file for more information. 201 | 202 | ## Code of Conduct 203 | 204 | Please read the [CODE OF CONDUCT](CODE_OF_CONDUCT.md) file for more information. 205 | 206 | ## Changelog 207 | 208 | See the [CHANGELOG](CHANGELOG.md) file for details. 209 | 210 | ## License 211 | 212 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 213 | 214 | _This project is tested with BrowserStack_ 215 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-otp-input-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist/ngx-otp-input-demo", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": ["src/styles.scss"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "budgets": [ 33 | { 34 | "type": "initial", 35 | "maximumWarning": "500kb", 36 | "maximumError": "1mb" 37 | }, 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "2kb", 41 | "maximumError": "4kb" 42 | } 43 | ], 44 | "outputHashing": "all" 45 | }, 46 | "development": { 47 | "optimization": false, 48 | "extractLicenses": false, 49 | "sourceMap": true 50 | } 51 | }, 52 | "defaultConfiguration": "production" 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "configurations": { 57 | "production": { 58 | "buildTarget": "ngx-otp-input-demo:build:production" 59 | }, 60 | "development": { 61 | "buildTarget": "ngx-otp-input-demo:build:development" 62 | } 63 | }, 64 | "defaultConfiguration": "development" 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "buildTarget": "ngx-otp-input-demo:build" 70 | } 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "polyfills": ["zone.js", "zone.js/testing"], 76 | "tsConfig": "tsconfig.spec.json", 77 | "inlineStyleLanguage": "scss", 78 | "assets": ["src/favicon.ico", "src/assets"], 79 | "styles": ["src/styles.scss"], 80 | "scripts": [] 81 | } 82 | } 83 | } 84 | }, 85 | "ngx-otp-input": { 86 | "projectType": "library", 87 | "root": "projects/ngx-otp-input", 88 | "sourceRoot": "projects/ngx-otp-input/src", 89 | "prefix": "lib", 90 | "architect": { 91 | "build": { 92 | "builder": "@angular-devkit/build-angular:ng-packagr", 93 | "options": { 94 | "project": "projects/ngx-otp-input/ng-package.json" 95 | }, 96 | "configurations": { 97 | "production": { 98 | "tsConfig": "projects/ngx-otp-input/tsconfig.lib.prod.json" 99 | }, 100 | "development": { 101 | "tsConfig": "projects/ngx-otp-input/tsconfig.lib.json" 102 | } 103 | }, 104 | "defaultConfiguration": "production" 105 | }, 106 | "test": { 107 | "builder": "@angular-devkit/build-angular:karma", 108 | "options": { 109 | "tsConfig": "projects/ngx-otp-input/tsconfig.spec.json", 110 | "polyfills": ["zone.js", "zone.js/testing"] 111 | } 112 | }, 113 | "lint": { 114 | "builder": "@angular-eslint/builder:lint", 115 | "options": { 116 | "lintFilePatterns": [ 117 | "projects/ngx-otp-input/**/*.ts", 118 | "projects/ngx-otp-input/**/*.html" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "cli": { 126 | "analytics": false 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-otp-input-demo", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build-lib": "ng build ngx-otp-input --configuration development", 9 | "build-lib:prod": "ng build ngx-otp-input", 10 | "build-lib-watch": "ng build ngx-otp-input --watch --configuration development", 11 | "watch": "ng build --watch --configuration development", 12 | "test": "ng test", 13 | "test-lib": "ng test ngx-otp-input", 14 | "test-lib:ci": "ng test ngx-otp-input --no-watch --no-progress --browsers=ChromeHeadless", 15 | "lint": "ng lint", 16 | "lint-lib": "ng lint ngx-otp-input", 17 | "prepare": "husky" 18 | }, 19 | "dependencies": { 20 | "@angular/common": "^18.0.1", 21 | "@angular/compiler": "^18.0.1", 22 | "@angular/core": "^18.0.1", 23 | "@angular/forms": "^18.0.1", 24 | "@angular/platform-browser": "^18.0.1", 25 | "@angular/platform-browser-dynamic": "^18.0.1", 26 | "@angular/router": "^18.0.1", 27 | "@vercel/analytics": "^1.3.1", 28 | "rxjs": "~7.8.0", 29 | "tslib": "^2.3.0", 30 | "zone.js": "~0.14.3" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "^18.0.2", 34 | "@angular-eslint/builder": "^18.0.1", 35 | "@angular-eslint/eslint-plugin": "^18.0.1", 36 | "@angular-eslint/eslint-plugin-template": "^18.0.1", 37 | "@angular-eslint/schematics": "^18.0.1", 38 | "@angular-eslint/template-parser": "^18.0.1", 39 | "@angular/cli": "^18.0.2", 40 | "@angular/compiler-cli": "^18.0.1", 41 | "@types/jasmine": "~5.1.0", 42 | "@typescript-eslint/eslint-plugin": "7.11.0", 43 | "@typescript-eslint/parser": "7.11.0", 44 | "autoprefixer": "^10.4.19", 45 | "eslint": "^8.57.0", 46 | "eslint-config-prettier": "^9.1.0", 47 | "husky": "^9.0.11", 48 | "jasmine-core": "~5.1.0", 49 | "karma": "~6.4.0", 50 | "karma-chrome-launcher": "~3.2.0", 51 | "karma-coverage": "~2.2.0", 52 | "karma-jasmine": "~5.1.0", 53 | "karma-jasmine-html-reporter": "~2.1.0", 54 | "lint-staged": "^15.2.2", 55 | "ng-packagr": "^18.0.0", 56 | "postcss": "^8.4.40", 57 | "prettier": "3.2.5", 58 | "tailwindcss": "^3.4.7", 59 | "typescript": "~5.4.2" 60 | }, 61 | "lint-staged": { 62 | "**/*": "prettier --write --ignore-unknown", 63 | "**/*.{ts,js,html}": "eslint --fix" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.html"], 11 | "rules": {} 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/README.md: -------------------------------------------------------------------------------- 1 | # ngx-otp-input 2 | 3 | [![License](https://img.shields.io/github/license/pkovzz/ngx-otp-input?style=flat)](/../../LICENSE.txt) 4 | 5 | :warning: **Important note:** starting with version 1.0.0, the library has been completely rewritten to use standalone components and introduce breaking changes. As the section "Requirements" states, the library now requires Angular 14 or above. 6 | 7 | ## What is this? 8 | 9 | This is a simple Angular library that allows you to create an OTP (One Time Password) form by providing a set of options. The library is designed to be easy to use and highly customizable, allowing you to configure the form to suit your needs. If you like the library, please consider giving it a star on GitHub. 10 | 11 | ### Demo page 12 | 13 | http://ngx-otp-input.vercel.app 14 | 15 | ## Requirements 16 | 17 | To use this library, your project must be running **Angular 14** or above. This requirement stems from our adoption of standalone components, which are an integral feature in Angular's modern development approach. Standalone components offer a more streamlined and modular way to manage your components and simplify the dependency management, positioning them as the future of Angular development. 18 | 19 | ## Installation 20 | 21 | To install this library, run: 22 | 23 | ```bash 24 | npm install ngx-otp-input --save 25 | ``` 26 | 27 | ## Example Usage 28 | 29 | Since the library uses standalone components, you can directly import and use them in your Angular application without needing to declare them in any module. For more configuration options, refer to the [Configuration options](#configuration-options) section. 30 | 31 | ```typescript 32 | import { Component } from '@angular/core'; 33 | import { NgxOtpInputComponent, NgxOtpInputComponentOptions } from 'ngx-otp-input'; 34 | 35 | @Component({ 36 | selector: 'app-root', 37 | standalone: true, 38 | imports: [NgxOtpInputComponent], 39 | template: ` 40 |

Welcome to My Angular App

41 | 42 | `, 43 | styleUrls: ['./app.component.scss'], 44 | }) 45 | export class AppComponent { 46 | otpOptions: NgxOtpInputComponentOptions = {...}; 47 | } 48 | ``` 49 | 50 | ## Inputs 51 | 52 | ### `options: NgxOtpInputComponentOptions` 53 | 54 | The `options` input is an object that allows you to configure the OTP form. For a list of available options, refer to the [Configuration options](#configuration-options) section. 55 | 56 | ### `otp: string | null | undefined` 57 | 58 | The `otp` input is a string that allows you to set the OTP value of the form. This input is useful when you want to pre-fill the form with an OTP value. If the `otp` input is set to `null` or `undefined`, the form will be empty. The library will match the length of the OTP value with the `otpLength` option and fill the input fields accordingly, in case the OTP value is shorter than the `otpLength` option, the remaining fields will be empty. If the given value will not match the `regexp` option, the library will throw an error. 59 | 60 | ### `status: NgxOtpStatus | null | undefined` 61 | 62 | The `status` input is a string that allows you to set the status of the OTP form. The status can be one of the following values: `null`, `undefined`, `'success'` or `'failed'`. This status is only used to visually indicate the result of the OTP verification process. 63 | 64 | For type safety, you can use the `NgxOtpStatus` enum: 65 | 66 | ```typescript 67 | import { NgxOtpStatus } from 'ngx-otp-input'; 68 | 69 | @Component({ 70 | selector: 'app-root', 71 | standalone: true, 72 | imports: [NgxOtpInputComponent], 73 | template: ` `, 74 | }) 75 | export class AppComponent { 76 | status = NgxOtpStatus; 77 | } 78 | ``` 79 | 80 | ### `disabled: boolean` 81 | 82 | The `disabled` input is a boolean that allows you to disable the OTP form. When set to `true`, the form will be disabled and the user will not be able to interact with it. 83 | 84 | ## Outputs 85 | 86 | ### `otpChange: string[]` 87 | 88 | The `otpChange` output is an event that is emitted whenever the OTP value changes. The event payload is an array of strings, where each string represents a value in the OTP form. 89 | 90 | ### `otpComplete: string` 91 | 92 | The `otpComplete` output is an event that is emitted whenever the OTP form is completed. The event payload is string, which represents the complete OTP value. 93 | 94 | ## Configuration options 95 | 96 | The `NgxOtpInputComponentOptions` interface allows you to configure the OTP form. The following options are available: 97 | 98 | | Option | Type | Default value | Description | 99 | | -------------------- | -------- | ------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 100 | | `otpLength` | number | 6 | The number of inputs in the OTP form | 101 | | `autoFocus` | boolean | true | Whether the first input should be focused automatically | 102 | | `autoBlur` | boolean | true | Whether the form should be blurred on complete | 103 | | `hideInputValues` | boolean | false | Whether the input values should be shown as password fields | 104 | | `regexp` | RegExp | /^[0-9]+$/ | The regular expression that the input values should match | 105 | | `showBlinkingCursor` | boolean | true | Whether the input fields should have a blinking cursor | 106 | | `ariaLabels` | string[] | [] | An array of strings that represent the aria-labels for each input field. For more information, please refer to the [More on aria-labels](#more-on-aria-labels) section. | 107 | 108 | ## Styling 109 | 110 | The library provides a set of CSS classes that you can use to style the OTP form. The following classes are available: 111 | 112 | | Class name | Description | 113 | | ------------------------ | -------------------------------------- | 114 | | `ngx-otp-input-form` | The main container of the OTP form | 115 | | `ngx-otp-input-box` | The input field of the OTP form | 116 | | `ngx-blinking-cursor` | The blinking cursor of the input field | 117 | | `ngx-otp-input-disabled` | The disabled state of the form | 118 | | `ngx-otp-input-filled` | The filled state of an input field | 119 | | `ngx-otp-input-success` | The success state of the form | 120 | | `nngx-otp-input-failed` | The failed state of the form | 121 | 122 | ### How to use the classes 123 | 124 | Styling is quite simple, but you have to use the classes directly in **root** style file, otherwise it will not work: 125 | 126 | ```scss 127 | ngx-otp-input { 128 | .ngx-otp-input-form { 129 | ... 130 | } 131 | .ngx-otp-input-box { 132 | ... 133 | } 134 | ... 135 | } 136 | ``` 137 | 138 | ## Reset the form 139 | 140 | In order to reset the form, you can use the `reset` method of the `NgxOtpInputComponent`: 141 | 142 | First, get a reference to the component in your template: 143 | 144 | ```html 145 | 149 | ``` 150 | 151 | Then, get a reference to the component in your component class: 152 | 153 | ```typescript 154 | import { Component, ViewChild } from '@angular/core'; 155 | import { NgxOtpInputComponent } from 'ngx-otp-input'; 156 | 157 | @Component({ 158 | selector: 'app-root', 159 | standalone: true, 160 | imports: [NgxOtpInputComponent], 161 | template: ` 162 | 166 | `, 167 | }) 168 | export class AppComponent { 169 | @ViewChild('otpInput') otpInput: NgxOtpInputComponent; 170 | 171 | resetForm() { 172 | this.otpInput.reset(); 173 | } 174 | } 175 | ``` 176 | 177 | Under the hood, the `reset` method will clear all the input values and reset the form to its initial state. For more information, refer to the [Angular FormArray reset](https://angular.dev/api/forms/FormArray#reset) documentation. 178 | 179 | ## More on aria-labels 180 | 181 | The `ariaLabels` option allows you to provide a set of strings that represent the aria-labels for each input field. This option is useful for making the form more accessible to users who rely on screen readers. The `aria-label` attribute provides a way to specify a string that labels the current element, which can be read by screen readers to provide additional context to the user. The library will automatically assign the `aria-label` attribute to each input with a default value of `One Time Password Input Number` followed by the input index. However, you can override this default value by providing your own set of labels in the `ariaLabels` option. 182 | 183 | If you provide an array of strings in the `ariaLabels` option, the library will use the values in the array to assign the `aria-label` attribute to each input field. The array should contain the same number of strings as the `otpLength` option, with each string representing the label for the corresponding input field. If the array contains fewer strings than the `otpLength` option, the library will use the default value for the remaining input fields. 184 | 185 | ## HTML `inputmode` attribute 186 | 187 | The `inputMode` option allows you to set the `inputmode` attribute of the input fields. The `inputmode` attribute provides a hint to the browser about the type of data that is expected to be entered by the user. This hint can help the browser provide a more appropriate **virtual keyboard** layout for the input field, making it easier for the user to enter the correct data. The `inputMode` option accepts a string value that represents the input mode of the input fields. For more details, check out this [documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode). 188 | 189 | Please note, that `regexp` option should be set to support the `inputmode` attribute. For example, if you set the `inputMode` option to `text` and the `regexp` option to `/^[a-zA-Z]+$/`, the browser will provide a virtual keyboard layout that is optimized for entering text data, but if the `inputMode` option is set to `numeric` and the `regexp` is still `/^[a-zA-Z]+$/`, the browser may provide a numeric keyboard layout, which may not be suitable for entering text data. 190 | 191 | **Default** options for `inputMode` and `regexp` are set to `'numeric'` and `/^[0-9]+$/` respectively, as these are the most common values for one time password inputs. 192 | 193 | ## Side notes 194 | 195 | If `hideInputValues` is set to `true`, the input values will be hidden by default, using the `password` input type. However certain password managers may place their browser extension icon on the input field, which may interfere with the input field's appearance. 196 | 197 | ## Contributing 198 | 199 | If you would like to contribute to this project, please refer to the [CONTRIBUTING](https://github.com/pkovzz/ngx-otp-input/blob/master/CONTRIBUTING.md) file for more information. 200 | 201 | ## Code of Conduct 202 | 203 | Please read the [CODE OF CONDUCT](https://github.com/pkovzz/ngx-otp-input/blob/master/CODE_OF_CONDUCT.md) file for more information. 204 | 205 | ## Changelog 206 | 207 | See the [CHANGELOG](https://github.com/pkovzz/ngx-otp-input/blob/master/CHANGELOG.md) file for details. 208 | 209 | ## License 210 | 211 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/pkovzz/ngx-otp-input/blob/master/LICENSE) file for details. 212 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-otp-input", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ngx-otp-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-otp-input", 3 | "version": "1.1.4", 4 | "peerDependencies": { 5 | "@angular/common": ">=14.0.0", 6 | "@angular/core": ">=14.0.0" 7 | }, 8 | "dependencies": { 9 | "tslib": "^2.3.0" 10 | }, 11 | "sideEffects": false, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/pkovzz/ngx-otp-input" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/pkovzz/ngx-otp-input/issues" 18 | }, 19 | "homepage": "https://github.com/pkovzz/ngx-otp-input", 20 | "description": "One Time Password input library for Angular (14+)", 21 | "keywords": [ 22 | "one time password", 23 | "otp", 24 | "angular", 25 | "ng2", 26 | "ngx", 27 | "ngx-otp-input", 28 | "sms code", 29 | "sms" 30 | ], 31 | "author": "Péter Kovács", 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/default.config.ts: -------------------------------------------------------------------------------- 1 | export interface NgxOtpInputComponentOptions { 2 | otpLength?: number; 3 | autoFocus?: boolean; 4 | autoBlur?: boolean; 5 | hideInputValues?: boolean; 6 | regexp?: RegExp; 7 | showBlinkingCursor?: boolean; 8 | ariaLabels?: string[]; 9 | inputMode?: string; 10 | } 11 | 12 | export const defaultOptions: NgxOtpInputComponentOptions = { 13 | otpLength: 6, 14 | autoFocus: true, 15 | autoBlur: true, 16 | hideInputValues: false, 17 | regexp: /^[0-9]+$/, 18 | showBlinkingCursor: true, 19 | ariaLabels: [], 20 | inputMode: 'numeric', 21 | }; 22 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/directives/ariaLabels.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | ElementRef, 5 | ContentChildren, 6 | QueryList, 7 | AfterContentInit, 8 | } from '@angular/core'; 9 | 10 | @Directive({ 11 | standalone: true, 12 | selector: '[ngxOtpAriaLabels]', 13 | }) 14 | export class AriaLabelsDirective implements AfterContentInit { 15 | @ContentChildren('otpInputElement', { descendants: true }) 16 | inputs!: QueryList>; 17 | 18 | @Input() ngxOtpAriaLabels!: string[]; 19 | 20 | ngAfterContentInit(): void { 21 | this.setAriaLabelsAttrs(); 22 | } 23 | 24 | private getDefaultAriaLabelText(index: number): string { 25 | return `One Time Password Input Number ${index + 1}`; 26 | } 27 | 28 | private setAriaLabelsAttrs(): void { 29 | this.inputs.forEach((input, index) => { 30 | input.nativeElement.setAttribute( 31 | 'aria-label', 32 | this.ngxOtpAriaLabels[index] ?? this.getDefaultAriaLabelText(index), 33 | ); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/directives/autoBlur.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | ContentChildren, 4 | Directive, 5 | ElementRef, 6 | Input, 7 | OnChanges, 8 | QueryList, 9 | SimpleChanges, 10 | } from '@angular/core'; 11 | 12 | @Directive({ 13 | standalone: true, 14 | selector: '[ngxAutoBlur]', 15 | }) 16 | export class AutoBlurDirective implements OnChanges, AfterContentInit { 17 | private inputHTMLElements: HTMLInputElement[] = []; 18 | 19 | @ContentChildren('otpInputElement', { descendants: true }) 20 | inputs!: QueryList>; 21 | 22 | @Input() 23 | ngxAutoBlur!: boolean; 24 | 25 | @Input() 26 | isFormValid!: boolean; 27 | 28 | ngOnChanges(changes: SimpleChanges) { 29 | if ( 30 | this.ngxAutoBlur && 31 | this.inputHTMLElements.length > 0 && 32 | changes['isFormValid'].currentValue 33 | ) { 34 | this.inputHTMLElements.forEach((input) => { 35 | input.blur(); 36 | }); 37 | } 38 | } 39 | 40 | ngAfterContentInit() { 41 | this.inputs.forEach((input) => { 42 | this.inputHTMLElements.push(input.nativeElement); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/directives/autoFocus.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | ContentChild, 4 | Directive, 5 | ElementRef, 6 | Input, 7 | } from '@angular/core'; 8 | 9 | @Directive({ 10 | standalone: true, 11 | selector: '[ngxAutoFocus]', 12 | }) 13 | export class AutoFocusDirective implements AfterContentInit { 14 | @ContentChild('otpInputElement', { static: false }) 15 | firstInput!: ElementRef; 16 | 17 | @Input() ngxAutoFocus!: boolean; 18 | 19 | ngAfterContentInit(): void { 20 | if (this.ngxAutoFocus && this.firstInput) { 21 | this.firstInput.nativeElement.focus(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/directives/inputNavigations.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | ContentChildren, 4 | Directive, 5 | ElementRef, 6 | EventEmitter, 7 | HostListener, 8 | Input, 9 | Output, 10 | QueryList, 11 | } from '@angular/core'; 12 | 13 | export type OtpValueChangeEvent = [number, string]; 14 | 15 | @Directive({ 16 | standalone: true, 17 | selector: '[ngxInputNavigations]', 18 | }) 19 | export class InputNavigationsDirective implements AfterContentInit { 20 | private inputsArray: ElementRef[] = []; 21 | 22 | @ContentChildren('otpInputElement', { descendants: true }) 23 | inputs!: QueryList>; 24 | 25 | @Input() regexp!: RegExp; 26 | 27 | @Output() valueChange: EventEmitter = 28 | new EventEmitter(); 29 | 30 | ngAfterContentInit() { 31 | this.inputsArray = this.inputs.toArray(); 32 | } 33 | 34 | private findInputIndex(target: HTMLElement): number { 35 | return this.inputsArray.findIndex( 36 | (input) => input.nativeElement === target, 37 | ); 38 | } 39 | 40 | private setFocus(index: number): void { 41 | if (index >= 0 && index < this.inputs.length) { 42 | this.inputsArray[index].nativeElement.focus(); 43 | } 44 | } 45 | 46 | @HostListener('keydown.arrowLeft', ['$event']) 47 | onArrowLeft(event: KeyboardEvent): void { 48 | const index = this.findInputIndex(event.target as HTMLElement); 49 | if (index > 0) { 50 | this.setFocus(index - 1); 51 | } 52 | } 53 | 54 | @HostListener('keydown.arrowRight', ['$event']) 55 | onArrowRight(event: KeyboardEvent): void { 56 | const index = this.findInputIndex(event.target as HTMLElement); 57 | if (index < this.inputs.length - 1) { 58 | this.setFocus(index + 1); 59 | } 60 | } 61 | 62 | @HostListener('keydown.backspace', ['$event']) 63 | onBackspace(event: KeyboardEvent): void { 64 | const index = this.findInputIndex(event.target as HTMLElement); 65 | if (index >= 0) { 66 | this.valueChange.emit([index, '']); 67 | this.setFocus(index - 1); 68 | event.preventDefault(); 69 | } 70 | } 71 | 72 | @HostListener('input', ['$event']) 73 | onKeyUp(event: InputEvent): void { 74 | const index = this.findInputIndex(event.target as HTMLElement); 75 | if ((event.target as HTMLInputElement).value?.match(this.regexp)) { 76 | this.valueChange.emit([index, (event.target as HTMLInputElement).value]); 77 | this.setFocus(index + 1); 78 | } else { 79 | this.inputsArray[index].nativeElement.value = ''; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/directives/paste.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentChildren, 3 | Directive, 4 | ElementRef, 5 | EventEmitter, 6 | HostListener, 7 | Input, 8 | Output, 9 | QueryList, 10 | } from '@angular/core'; 11 | 12 | @Directive({ 13 | standalone: true, 14 | selector: '[ngxOtpPaste]', 15 | }) 16 | export class PasteDirective { 17 | @ContentChildren('otpInputElement', { descendants: true }) 18 | inputs!: QueryList>; 19 | 20 | @Input() regexp!: RegExp; 21 | 22 | @Output() handlePaste: EventEmitter = new EventEmitter(); 23 | 24 | @HostListener('paste', ['$event']) 25 | onPaste(event: ClipboardEvent): void { 26 | event.preventDefault(); 27 | const clipboardData = event.clipboardData?.getData('text'); 28 | if (clipboardData && this.regexp.test(clipboardData)) { 29 | const values = clipboardData.split(''); 30 | this.inputs.forEach((input, index) => { 31 | if (values[index]) { 32 | input.nativeElement.value = values[index]; 33 | } 34 | }); 35 | this.handlePaste.emit(values); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/ngx-otp-input.component.html: -------------------------------------------------------------------------------- 1 |
17 | 38 |
39 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/ngx-otp-input.component.scss: -------------------------------------------------------------------------------- 1 | .ngx-otp-input-form { 2 | display: inline-flex; 3 | gap: 0.5rem; 4 | caret-color: transparent; 5 | } 6 | 7 | .ngx-blinking-cursor { 8 | caret-color: initial; 9 | } 10 | 11 | .ngx-otp-input-box { 12 | width: 30px; 13 | height: 35px; 14 | padding: 0.5rem; 15 | font-size: 1.5rem; 16 | text-align: center; 17 | border: 1px solid #c4c4c4; 18 | border-radius: 0.5rem; 19 | outline: none; 20 | &:focus { 21 | border-color: #007bff; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/ngx-otp-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NgxOtpInputComponent } from './ngx-otp-input.component'; 3 | import { NgxOtpInputComponentOptions } from './default.config'; 4 | import { NgxOtpStatus } from 'ngx-otp-input'; 5 | 6 | /** 7 | * TODO: add many-many more test cases! 8 | */ 9 | 10 | describe('NgxOtpInputComponent with default options', () => { 11 | let component: NgxOtpInputComponent; 12 | let fixture: ComponentFixture; 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | imports: [NgxOtpInputComponent], 17 | }).compileComponents(); 18 | 19 | fixture = TestBed.createComponent(NgxOtpInputComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | 28 | it('should have the default options', () => { 29 | expect(component.ngxOtpOptionsInUse).toBeDefined(); 30 | }); 31 | 32 | it('should have as many inputs as the length of the otp', () => { 33 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 34 | expect(inputElements.length).toEqual( 35 | component.ngxOtpOptionsInUse.otpLength, 36 | ); 37 | }); 38 | 39 | it('should have been focused on the first input', () => { 40 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 41 | expect(document.activeElement).toEqual(inputElements[0]); 42 | }); 43 | 44 | // TODO: Fix the test case 45 | // it('should have been blurred after the otp is completed', () => { 46 | // const inputElements = fixture.nativeElement.querySelectorAll('input'); 47 | // inputElements.forEach((inputElement: HTMLInputElement) => { 48 | // inputElement.value = '1'; 49 | // inputElement.dispatchEvent(new Event('input')); 50 | // }); 51 | // expect(document.activeElement).not.toEqual(inputElements[0]); 52 | // }); 53 | 54 | it('should have the input type as text', () => { 55 | expect(component.inputType).toEqual('text'); 56 | }); 57 | 58 | it('should have default aria labels', () => { 59 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 60 | inputElements.forEach((inputElement: HTMLInputElement, index: number) => { 61 | expect(inputElement.getAttribute('aria-label')).toEqual( 62 | `One Time Password Input Number ${index + 1}`, 63 | ); 64 | }); 65 | }); 66 | 67 | it('should have the css class ngx-otp-input-success if the status is success', () => { 68 | component.status = NgxOtpStatus.SUCCESS; 69 | fixture.detectChanges(); 70 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 71 | inputElements.forEach((inputElement: HTMLInputElement) => { 72 | expect( 73 | inputElement.classList.contains('ngx-otp-input-success'), 74 | ).toBeTrue(); 75 | }); 76 | }); 77 | 78 | it('should have the css class ngx-otp-input-failed if the status is failed', () => { 79 | component.status = NgxOtpStatus.FAILED; 80 | fixture.detectChanges(); 81 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 82 | inputElements.forEach((inputElement: HTMLInputElement) => { 83 | expect( 84 | inputElement.classList.contains('ngx-otp-input-failed'), 85 | ).toBeTrue(); 86 | }); 87 | }); 88 | 89 | it('should have the css class ngx-otp-input-disabled if the input is disabled', () => { 90 | component.disabled = true; 91 | fixture.detectChanges(); 92 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 93 | inputElements.forEach((inputElement: HTMLInputElement) => { 94 | expect( 95 | inputElement.classList.contains('ngx-otp-input-disabled'), 96 | ).toBeTrue(); 97 | }); 98 | }); 99 | 100 | it('should have numeric inputmode by default', () => { 101 | const inputElements = fixture.nativeElement.querySelectorAll('input'); 102 | inputElements.forEach((inputElement: HTMLInputElement) => { 103 | expect(inputElement.getAttribute('inputmode')).toEqual('numeric'); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('NgxOtpInputComponent with custom options', () => { 109 | const options: NgxOtpInputComponentOptions = { 110 | otpLength: 5, 111 | autoFocus: false, 112 | autoBlur: false, 113 | regexp: /^[0-9]+$/, 114 | hideInputValues: true, 115 | showBlinkingCursor: true, 116 | ariaLabels: ['First', 'Second', 'Third', 'Fourth', 'Fifth'], 117 | inputMode: 'text', 118 | }; 119 | 120 | let component: NgxOtpInputComponent; 121 | 122 | beforeEach(async () => { 123 | await TestBed.configureTestingModule({ 124 | imports: [NgxOtpInputComponent], 125 | }).compileComponents(); 126 | 127 | const fixture = TestBed.createComponent(NgxOtpInputComponent); 128 | component = fixture.componentInstance; 129 | component.options = options; 130 | fixture.detectChanges(); 131 | }); 132 | 133 | it('should have custom options', () => { 134 | expect(component.ngxOtpOptionsInUse).toEqual(options); 135 | }); 136 | 137 | it('should have as many inputs as the length of the otp', () => { 138 | const inputElements = document.querySelectorAll('input'); 139 | expect(inputElements.length).toEqual( 140 | component.ngxOtpOptionsInUse.otpLength!, 141 | ); 142 | }); 143 | 144 | it('should not have been focused on the first input', () => { 145 | const inputElements = document.querySelectorAll('input'); 146 | expect(document.activeElement).not.toEqual(inputElements[0]); 147 | }); 148 | 149 | it('should have input type as password', () => { 150 | const inputElements = document.querySelectorAll('input'); 151 | inputElements.forEach((inputElement: HTMLInputElement) => { 152 | expect(inputElement.type).toEqual('password'); 153 | }); 154 | }); 155 | 156 | it('should have custom aria labels', () => { 157 | const inputElements = document.querySelectorAll('input'); 158 | inputElements.forEach((inputElement: HTMLInputElement, index: number) => { 159 | if (options.ariaLabels) { 160 | expect(inputElement.getAttribute('aria-label')).toEqual( 161 | options.ariaLabels[index], 162 | ); 163 | } 164 | }); 165 | }); 166 | 167 | it('should have text inputmode', () => { 168 | const inputElements = document.querySelectorAll('input'); 169 | inputElements.forEach((inputElement: HTMLInputElement) => { 170 | expect(inputElement.getAttribute('inputmode')).toEqual('text'); 171 | }); 172 | }); 173 | 174 | // TODO: Fix the test case 175 | // it('should have the css class ngx-blinking-cursor if the showBlinkingCursor option is true', () => { 176 | // const inputElements = document.querySelectorAll('input'); 177 | // inputElements.forEach((inputElement: HTMLInputElement) => { 178 | // expect( 179 | // inputElement.classList.contains('ngx-blinking-cursor'), 180 | // ).toBeTrue(); 181 | // }); 182 | // }); 183 | }); 184 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/lib/ngx-otp-input.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | OnInit, 7 | Output, 8 | SimpleChanges, 9 | } from '@angular/core'; 10 | import { CommonModule } from '@angular/common'; 11 | import { 12 | FormArray, 13 | FormControl, 14 | ReactiveFormsModule, 15 | Validators, 16 | } from '@angular/forms'; 17 | import { PasteDirective } from './directives/paste.directive'; 18 | import { AutoFocusDirective } from './directives/autoFocus.directive'; 19 | import { 20 | InputNavigationsDirective, 21 | OtpValueChangeEvent, 22 | } from './directives/inputNavigations.directive'; 23 | import { AutoBlurDirective } from './directives/autoBlur.directive'; 24 | import { AriaLabelsDirective } from './directives/ariaLabels.directive'; 25 | import { NgxOtpInputComponentOptions, defaultOptions } from './default.config'; 26 | 27 | export enum NgxOtpStatus { 28 | SUCCESS = 'success', 29 | FAILED = 'failed', 30 | } 31 | 32 | @Component({ 33 | standalone: true, 34 | imports: [ 35 | CommonModule, 36 | ReactiveFormsModule, 37 | PasteDirective, 38 | AutoFocusDirective, 39 | InputNavigationsDirective, 40 | AutoBlurDirective, 41 | AriaLabelsDirective, 42 | ], 43 | selector: 'ngx-otp-input', 44 | templateUrl: 'ngx-otp-input.component.html', 45 | styleUrls: ['ngx-otp-input.component.scss'], 46 | }) 47 | export class NgxOtpInputComponent implements OnInit, OnChanges { 48 | protected ngxOtpInputArray!: FormArray; 49 | protected ngxOtpOptions: NgxOtpInputComponentOptions = defaultOptions; 50 | 51 | @Input() set options(customOptions: NgxOtpInputComponentOptions) { 52 | this.ngxOtpOptions = { ...defaultOptions, ...customOptions }; 53 | } 54 | 55 | @Input() status: NgxOtpStatus | null | undefined; 56 | @Input() disabled = false; 57 | @Input() otp: string | null | undefined; 58 | @Output() otpChange = new EventEmitter(); 59 | @Output() otpComplete = new EventEmitter(); 60 | 61 | // For testing purposes 62 | get ngxOtpOptionsInUse(): NgxOtpInputComponentOptions { 63 | return this.ngxOtpOptions; 64 | } 65 | 66 | get inputType(): string { 67 | return this.ngxOtpOptions.hideInputValues ? 'password' : 'text'; 68 | } 69 | 70 | get isOTPSuccess(): boolean { 71 | return this.status === NgxOtpStatus.SUCCESS; 72 | } 73 | 74 | get isOTPFailed(): boolean { 75 | return this.status === NgxOtpStatus.FAILED; 76 | } 77 | 78 | ngOnInit(): void { 79 | this.initOtpInputArray(); 80 | } 81 | 82 | ngOnChanges(changes: SimpleChanges): void { 83 | const otpChange = changes['otp']; 84 | if (otpChange?.currentValue) { 85 | if (!otpChange.firstChange) { 86 | this.setInitialOtp(otpChange.currentValue); 87 | } else { 88 | this.ngxOtpOptions.autoFocus = false; 89 | } 90 | } 91 | } 92 | 93 | private initOtpInputArray(): void { 94 | this.ngxOtpInputArray = new FormArray( 95 | Array.from( 96 | { length: this.ngxOtpOptions.otpLength! }, 97 | () => new FormControl('', Validators.required), 98 | ), 99 | ); 100 | if (this.otp) { 101 | this.setInitialOtp(this.otp); 102 | } 103 | } 104 | 105 | private setInitialOtp(otp: string): void { 106 | if (this.ngxOtpOptions.regexp!.test(otp)) { 107 | const otpValueArray = otp.split(''); 108 | otpValueArray.forEach((value, index) => { 109 | this.ngxOtpInputArray.controls[index].setValue(value ?? ''); 110 | }); 111 | this.emitOtpValueChange(); 112 | if (otpValueArray.length !== this.ngxOtpOptions.otpLength) { 113 | console.warn( 114 | 'OTP length does not match the provided otpLength option!', 115 | ); 116 | } 117 | } else { 118 | throw new Error('Invalid OTP provided for the component '); 119 | } 120 | } 121 | 122 | protected handleInputChanges($event: OtpValueChangeEvent) { 123 | const [index, value] = $event; 124 | this.ngxOtpInputArray.controls[index].setValue(value); 125 | this.emitOtpValueChange(); 126 | } 127 | 128 | protected handlePasteChange($event: string[]): void { 129 | if ($event.length === this.ngxOtpOptions.otpLength) { 130 | this.ngxOtpInputArray.setValue($event); 131 | } else { 132 | $event.map((value, index) => { 133 | this.ngxOtpInputArray.controls[index]?.setValue?.(value); 134 | }); 135 | } 136 | this.emitOtpValueChange(); 137 | } 138 | 139 | private emitOtpValueChange(): void { 140 | this.otpChange.emit(this.ngxOtpInputArray.value); 141 | if (this.ngxOtpInputArray.valid) { 142 | this.otpComplete.emit(this.ngxOtpInputArray.value.join('')); 143 | } 144 | } 145 | 146 | protected isInputFilled(index: number): boolean { 147 | return !!this.ngxOtpInputArray.controls[index].value; 148 | } 149 | 150 | reset(): void { 151 | this.ngxOtpInputArray.reset(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-otp-input 3 | */ 4 | 5 | export * from './lib/ngx-otp-input.component'; 6 | export { NgxOtpInputComponentOptions } from './lib/default.config'; 7 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-otp-input/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |

Component Options

7 |
11 |
12 | 17 | 25 |
26 |
27 | 32 | 39 |
40 | Regexp pattern to match the accepted characters 41 |
42 |
43 |
44 | 49 | 55 |
56 | For detailed information, check the 57 | documentation 62 |
63 |
64 |
65 | 70 | 77 |
78 | Separate aria-labels with comma 79 |
80 |
81 |
82 |
83 | 89 | 90 |
91 |
92 | 98 | 99 |
100 |
101 | 107 | 108 |
109 |
110 | 116 | 117 |
118 |
119 | 125 | 126 |
127 |
128 | 134 |
135 |
136 |
137 |

Component Preview

138 | 146 |
147 |
150 |
151 | (otpChange) will be triggered when the form value changes: 152 |
153 |
{{ otpChangeValue }}
154 |
155 |
158 |
159 | (otpComplete) will be triggered when the form value is completed: 160 |
161 |
{{ otpCompleteValue }}
162 |
163 |
164 | 171 |
172 |
173 |
174 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkovzz/ngx-otp-input/ddfade9c52acf47fc5307eeb3c277428955285c3/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | // it(`should have the 'ngx-otp-input-demo' title`, () => { 18 | // const fixture = TestBed.createComponent(AppComponent); 19 | // const app = fixture.componentInstance; 20 | // expect(app.title).toEqual('ngx-otp-input-demo'); 21 | // }); 22 | 23 | // it('should render title', () => { 24 | // const fixture = TestBed.createComponent(AppComponent); 25 | // fixture.detectChanges(); 26 | // const compiled = fixture.nativeElement as HTMLElement; 27 | // expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ngx-otp-input-demo'); 28 | // }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@vercel/analytics'; 2 | import { Component, OnInit, ViewChild } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterOutlet } from '@angular/router'; 6 | import { 7 | NgxOtpInputComponent, 8 | NgxOtpStatus, 9 | NgxOtpInputComponentOptions, 10 | } from 'ngx-otp-input'; 11 | import { BasicInformationComponent } from '../components/BasicInformation.component'; 12 | import { HeaderComponent } from '../components/Header.component'; 13 | 14 | @Component({ 15 | selector: 'app-root', 16 | standalone: true, 17 | imports: [ 18 | CommonModule, 19 | RouterOutlet, 20 | FormsModule, 21 | NgxOtpInputComponent, 22 | HeaderComponent, 23 | BasicInformationComponent, 24 | ], 25 | templateUrl: './app.component.html', 26 | styleUrl: './app.component.scss', 27 | }) 28 | export class AppComponent implements OnInit { 29 | @ViewChild('ngxOtpInput') ngxOtpInput!: NgxOtpInputComponent; 30 | otpStatusEnum = NgxOtpStatus; 31 | showNgxOtpInput = true; 32 | otpOptions: NgxOtpInputComponentOptions = { 33 | otpLength: 5, 34 | autoFocus: true, 35 | autoBlur: true, 36 | hideInputValues: false, 37 | showBlinkingCursor: true, 38 | regexp: /^[0-9]+$/, 39 | ariaLabels: ['a', 'b', 'c', 'd', 'e', 'f'], 40 | inputMode: 'numeric', 41 | }; 42 | regexp = '^[0-9]+$'; 43 | ariaLabels = ''; 44 | disabled = false; 45 | otpChangeValue = '-'; 46 | otpCompleteValue = '-'; 47 | 48 | ngOnInit(): void { 49 | inject(); 50 | this.formatAriaLabelsForDisplay(); 51 | } 52 | 53 | onOtpChange(otp: string[]) { 54 | const hasValue = otp.some((value) => value !== ''); 55 | if (hasValue) { 56 | this.otpChangeValue = otp.join(', '); 57 | } else { 58 | this.otpChangeValue = '-'; 59 | this.otpCompleteValue = '-'; 60 | } 61 | } 62 | 63 | onOtpComplete(otp: string) { 64 | this.otpCompleteValue = otp; 65 | } 66 | 67 | formatAriaLabelsForDisplay() { 68 | this.ariaLabels = this.otpOptions.ariaLabels!.join(', '); 69 | } 70 | 71 | formatAriaLabelsForSave() { 72 | const ariaLabelsInputValue = this.ariaLabels.split(','); 73 | this.otpOptions.ariaLabels = ariaLabelsInputValue.map((entry) => 74 | entry.replace(/\s/g, ''), 75 | ); 76 | } 77 | 78 | convertStringToRegexp() { 79 | this.otpOptions.regexp = new RegExp(this.regexp); 80 | } 81 | 82 | handleComponentReload() { 83 | this.showNgxOtpInput = false; 84 | setTimeout(() => { 85 | this.showNgxOtpInput = true; 86 | }); 87 | } 88 | 89 | handleReset() { 90 | this.ngxOtpInput.reset(); 91 | this.otpChangeValue = '-'; 92 | this.otpCompleteValue = '-'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; 6 | 7 | export const appConfig: ApplicationConfig = { 8 | providers: [provideRouter(routes), provideAnimationsAsync()], 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkovzz/ngx-otp-input/ddfade9c52acf47fc5307eeb3c277428955285c3/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/components/BasicInformation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | standalone: true, 5 | selector: 'app-basic-information', 6 | template: ` 7 | 47 | `, 48 | }) 49 | export class BasicInformationComponent {} 50 | -------------------------------------------------------------------------------- /src/components/Header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | standalone: true, 5 | selector: 'app-header', 6 | template: ` 7 |
8 |
11 |

ngx-otp-input

12 |
13 | Star 22 |
23 |
24 |
25 | GitHub License 29 | GitHub Tag 33 | GitHub tag status 37 | NPM Downloads 41 |
42 |
43 | `, 44 | }) 45 | export class HeaderComponent {} 46 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkovzz/ngx-otp-input/ddfade9c52acf47fc5307eeb3c277428955285c3/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngx-otp-input 6 | 7 | 11 | 16 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | body { 7 | font-family: Roboto, 'Helvetica Neue', sans-serif; 8 | } 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,ts}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | corePlugins: { 9 | preflight: false, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "paths": { 13 | "ngx-otp-input": [ 14 | "./dist/ngx-otp-input" 15 | ] 16 | }, 17 | "esModuleInterop": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "node", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "ES2022", 25 | "useDefineForClassFields": false, 26 | "lib": [ 27 | "ES2022", 28 | "dom" 29 | ] 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------