├── .circleci └── config.yml ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── angular.json ├── demo.gif ├── package-lock.json ├── package.json ├── projects ├── angular-5-test │ ├── .gitignore │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── angular-6-test │ ├── .gitignore │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── angular-7-test │ ├── .gitignore │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── angular-8-test │ ├── .gitignore │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ └── material-test │ │ │ │ ├── material-test.component.ts │ │ │ │ └── material-test.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ ├── environment.ts │ │ │ └── index.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json └── ngx-autofocus-fix │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── autofocus-fix-config.spec.ts │ │ ├── autofocus-fix-config.ts │ │ ├── autofocus-fix.directive.spec.ts │ │ ├── autofocus-fix.directive.ts │ │ ├── autofocus-fix.module.spec.ts │ │ ├── autofocus-fix.module.ts │ │ ├── no-autofocus-fix-config.error.spec.ts │ │ ├── no-autofocus-fix-config.error.ts │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tools ├── common.ts ├── prepare-version.ts └── tsconfig.json ├── tsconfig.json ├── tslint.json └── wallaby.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | defaults: &defaults 8 | working_directory: ~/repo 9 | docker: 10 | - image: circleci/node:12-browsers 11 | 12 | jobs: 13 | install: 14 | <<: *defaults 15 | steps: 16 | - checkout 17 | 18 | - restore_cache: 19 | keys: 20 | - lib-dependencies-{{ checksum "package-lock.json" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - lib-dependencies- 23 | 24 | - run: npm install 25 | 26 | - save_cache: 27 | paths: 28 | - node_modules 29 | key: v1-dependencies-{{ checksum "package-lock.json" }} 30 | 31 | - persist_to_workspace: 32 | root: ~/repo 33 | paths: . 34 | 35 | lint: 36 | <<: *defaults 37 | steps: 38 | - attach_workspace: 39 | at: ~/repo 40 | 41 | - run: 42 | name: Run lint 43 | command: npm run lint 44 | 45 | test: 46 | <<: *defaults 47 | steps: 48 | - attach_workspace: 49 | at: ~/repo 50 | 51 | - run: 52 | name: Run tests 53 | command: npm run test:ci 54 | 55 | - store_artifacts: 56 | path: ./coverage/ngx-autofocus-fix 57 | destination: coverage 58 | 59 | - run: 60 | name: Publish coverage to Coveralls 61 | command: npm run coveralls 62 | 63 | build: 64 | <<: *defaults 65 | steps: 66 | - attach_workspace: 67 | at: ~/repo 68 | 69 | - run: 70 | name: Build package 71 | command: npm run build 72 | 73 | - persist_to_workspace: 74 | root: ~/repo 75 | paths: . 76 | 77 | e2e-install: 78 | <<: *defaults 79 | steps: 80 | - attach_workspace: 81 | at: ~/repo 82 | 83 | - restore_cache: 84 | keys: 85 | - e2e-dependencies-ng5-{{ checksum "projects/angular-5-test/package.json" }} 86 | - e2e-dependencies-ng5- 87 | 88 | - restore_cache: 89 | keys: 90 | - e2e-dependencies-ng8-{{ checksum "projects/angular-6-test/package.json" }} 91 | - e2e-dependencies-ng6- 92 | 93 | - restore_cache: 94 | keys: 95 | - e2e-dependencies-ng7-{{ checksum "projects/angular-7-test/package.json" }} 96 | - e2e-dependencies-ng7- 97 | 98 | - restore_cache: 99 | keys: 100 | - e2e-dependencies-ng8-{{ checksum "projects/angular-8-test/package.json" }} 101 | - e2e-dependencies-ng8- 102 | 103 | - run: 104 | name: Install E2E dependencies 105 | command: npm run e2e-install 106 | 107 | - save_cache: 108 | paths: 109 | - projects/angular-5-test/node_modules 110 | key: e2e-dependencies-ng5-{{ checksum "projects/angular-5-test/package.json" }} 111 | 112 | - save_cache: 113 | paths: 114 | - projects/angular-6-test/node_modules 115 | key: e2e-dependencies-ng6-{{ checksum "projects/angular-6-test/package.json" }} 116 | 117 | - save_cache: 118 | paths: 119 | - projects/angular-7-test/node_modules 120 | key: e2e-dependencies-ng7-{{ checksum "projects/angular-7-test/package.json" }} 121 | 122 | - save_cache: 123 | paths: 124 | - projects/angular-8-test/node_modules 125 | key: e2e-dependencies-ng8-{{ checksum "projects/angular-8-test/package.json" }} 126 | 127 | - persist_to_workspace: 128 | root: ~/repo 129 | paths: . 130 | 131 | e2e: 132 | <<: *defaults 133 | steps: 134 | - attach_workspace: 135 | at: ~/repo 136 | 137 | - run: 138 | name: Run E2E tests 139 | command: npm run e2e 140 | 141 | prepare-ver: 142 | <<: *defaults 143 | steps: 144 | - attach_workspace: 145 | at: ~/repo 146 | 147 | - run: 148 | name: Set Git identity 149 | command: | 150 | git config --global user.email "dev@korniychuk.pro" 151 | git config --global user.name "Circle CI" 152 | 153 | - run: 154 | name: Add Github.com Keys to known_hosts 155 | command: | 156 | mkdir -p ~/.ssh 157 | ssh-keyscan -H github.com >> ~/.ssh/known_hosts 158 | 159 | - run: 160 | name: Prepare package version 161 | command: npm run prepare-ver 162 | 163 | publish: 164 | <<: *defaults 165 | steps: 166 | - attach_workspace: 167 | at: ~/repo 168 | 169 | - run: 170 | name: Authenticate with registry 171 | command: echo "//registry.npmjs.org/:_authToken=$npm_TOKEN" > ~/repo/.npmrc 172 | 173 | - run: 174 | name: Publish package 175 | command: npm run publish 176 | 177 | workflows: 178 | version: 2 179 | test-deploy: 180 | jobs: 181 | - install: 182 | filters: 183 | branches: 184 | only: /master/ 185 | 186 | - lint: 187 | requires: 188 | - install 189 | filters: 190 | branches: 191 | only: /master/ 192 | 193 | - test: 194 | requires: 195 | - install 196 | filters: 197 | branches: 198 | only: /master/ 199 | 200 | - build: 201 | requires: 202 | - lint 203 | - test 204 | filters: 205 | branches: 206 | only: /master/ 207 | 208 | - e2e-install: 209 | requires: 210 | - build 211 | filters: 212 | branches: 213 | only: /master/ 214 | 215 | - e2e: 216 | requires: 217 | - e2e-install 218 | filters: 219 | branches: 220 | only: /master/ 221 | 222 | - prepare-ver: 223 | requires: 224 | - e2e 225 | filters: 226 | branches: 227 | only: /master/ 228 | 229 | - publish: 230 | requires: 231 | - e2e 232 | filters: 233 | branches: 234 | only: /master/ 235 | -------------------------------------------------------------------------------- /.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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anton Korniychuk 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-autofocus-fix 2 | 3 | 4 | [![CircleCI](https://img.shields.io/circleci/build/github/korniychuk/angular-autofocus-fix?token=none)](https://circleci.com/gh/korniychuk/angular-autofocus-fix/tree/master) 5 | [![Coverage Status](https://coveralls.io/repos/korniychuk/angular-autofocus-fix/badge.svg?branch=master)](https://coveralls.io/r/korniychuk/angular-autofocus-fix?branch=master) 6 | [![npm version](https://img.shields.io/npm/v/ngx-autofocus-fix)](https://www.npmjs.com/package/ngx-autofocus-fix) 7 | ![npm downloads](https://img.shields.io/npm/dw/ngx-autofocus-fix) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/korniychuk/angular-autofocus-fix/badge.svg)](https://snyk.io/test/github/korniychuk/angular-autofocus-fix) 9 | 10 | 11 | Angular 5+ directive for fix autofocus on dynamically created controls (`*ngIf`, `*ngFor`, etc.). 12 | [legacy version for Angular 2/4](https://github.com/korniychuk/angular-autofocus-fix/tree/legacy-version) 13 | 14 | [Online Demo](https://stackblitz.com/edit/ngx-autofocus-fix-demo) (Stackblitz.com) 15 | 16 | ![Autofocus Demo](demo.gif) 17 | 18 | ## Advantages over the other libraries 19 | 20 | * **Uses native HTML attribute `autofocus` as the selector!** [example](#quick-start) 21 | * There are no custom selectors, no need to change your HTML template. 22 | * Works with native DOM. Doesn't use any dependencies(jQuery, lodash, etc.). 23 | * [Configurable](#configuration). Use can use input attributes or provide global options via `AutofocusFixConfig` 24 | * 100% Coverage, over 60 unit tests. 25 | * E2E tests for 8,7,6 and 5 versions of Angular including e2e test for Angular Material Input. 26 | * The library understands an extensive list of input data. (`null/NaN/'true'/[]/...`). See [Advanced examples](#advanced-examples) 27 | * Supports asynchronous focusing(optionally wrapping `.focus()` execution with `setTimeout()`). 28 | * Works perfectly with Angular Material. (there is an E2E test) 29 | * Works with AOT mode. (tested via E2E test) 30 | 31 | ## Installation 32 | 33 | Notice: npm package renamed `angular-autofocus-fix` -> `ngx-autofocus-fix` 34 | 35 | To install this library, run: 36 | 37 | ```bash 38 | $ npm i ngx-autofocus-fix --save 39 | ``` 40 | or 41 | ```bash 42 | $ yarn add ngx-autofocus-fix 43 | ``` 44 | 45 | ## Quick start 46 | 47 | 1. Import the library in your Angular application, for example in `AppModule`: 48 | 49 | ```typescript 50 | import { BrowserModule } from '@angular/platform-browser'; 51 | import { NgModule } from '@angular/core'; 52 | 53 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; // <--- new code 54 | 55 | import { AppComponent } from './app.component'; 56 | 57 | @NgModule({ 58 | declarations: [ 59 | AppComponent, 60 | ], 61 | imports: [ 62 | BrowserModule, 63 | 64 | AutofocusFixModule.forRoot(), // <--- new code 65 | ], 66 | providers: [], 67 | bootstrap: [ AppComponent ] 68 | }) 69 | export class AppModule { } 70 | ``` 71 | 72 | 2. You can now use autofocus directive in app.component.html 73 | 74 | ```html 75 | 79 | 80 | ``` 81 | 82 | ## Advanced examples 83 | 84 | Ways to **disable autofocus:** any js-falsy value, except an empty string (default `@Input`'s [normalization mode](#inputs-smart-empty-check-normalization-mode)) 85 | 86 | ```html 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | Ways to **enable autofocus:** any js-truthy value and an empty string (default `@Input`'s [normalization mode](#inputs-smart-empty-check-normalization-mode)) 106 | 107 | ```html 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | ``` 125 | 126 | ## Input's Smart Empty Check normalization mode 127 | 128 | All input values are passed through the function: `normalizeInputAsBoolean(value: any, smartEmptyCheck = false): boolean`. 129 | 130 | Smart Empty Check mode changes the behavior so that the following values are treated as falsy: 131 | * An empty string `''` 132 | * An empty object `{}` 133 | * An empty array `[]` 134 | 135 | See [Configuration](#configuration) to understand how to enable the mode. 136 | 137 | **Notes:** 138 | * Smart Empty Check normalization mode available only for `autofocus` attribute. All other directive `@Input`'s always works in the default normalization mode. 139 | * Using attribute `autofocus` without any value doesn't enable autofocusing in Smart Empty Check mode. Because of an empty value means an empty string in terms of Angular templates syntax. 140 | 141 | ## Configuration 142 | 143 | ### Options 144 | 145 | ```typescript 146 | export class AutofocusFixConfig { 147 | ... 148 | 149 | /** 150 | * In case `true` .focus() events will be wrapped by `setTimeout(() => ...)`. 151 | * 152 | * Notice: 153 | * I'm not sure that the action is a good practice, however this ability added because of next issues: 154 | * - https://github.com/korniychuk/angular-autofocus-fix/issues/1 155 | * - https://github.com/spirosikmd/angular2-focus/issues/46 156 | */ 157 | public readonly async: boolean = false; 158 | /** 159 | * In case `true`: treat an empty string, an empty array and an empty object as a falsy value. 160 | * In case `false`(default): each of these values treats as truthy. 161 | */ 162 | public readonly smartEmptyCheck: boolean = false; 163 | /** 164 | * In case `true`: trigger {@link ChangeDetectorRef.detectChanges}() after {@link HTMLElement.focus}(). 165 | * 166 | * This is helpful in the case when the HTMLElement to which {@link AutofocusFixDirective} added 167 | * wrapped by another directive/component that has some binding related to focus of the element. 168 | * In this case without enabling .triggerChangeDetection option Angular throws ExpressionChangedAfterItHasBeenCheckedError. 169 | * 170 | * A striking example is the from the Angular Material that wraps control. 171 | */ 172 | public readonly triggerDetectChanges: boolean = false; 173 | } 174 | ``` 175 | 176 | ### There are four ways to configure the `AutofocusFixDirective`: 177 | 178 | **1. Specify attribute-options for specific HTML Element** 179 | ```html 180 | 186 | ``` 187 | [Normalization](#inputs-smart-empty-check-normalization-mode)(only default) available and binding supported. 188 | ```html 189 | 195 | 196 | 202 | ``` 203 | 204 | **2. Specify global options for the whole application by passing it to `.forRoot({ ... })`** 205 | ```typescript 206 | @NgModule({ 207 | ... 208 | imports: [ 209 | ... 210 | AutofocusFixModule.forRoot({ 211 | async: true, 212 | smartEmptyCheck: true, 213 | triggerDetectChanges: true, 214 | }), 215 | ], 216 | ... 217 | }) 218 | export class AppModule { } 219 | ``` 220 | 221 | **3. Provide Lazy-Route level `AutofocusFixConfig` config** 222 | 223 | ```typescript 224 | import { NgModule } from '@angular/core'; 225 | import { AutofocusFixModule, AutofocusFixConfig } from 'ngx-autofocus-fix'; 226 | 227 | 228 | const autofocusFixConfigProvider: Provider = { 229 | provide: AutofocusFixConfig, 230 | useValue: new AutofocusFixConfig({ 231 | async: true, 232 | smartEmptyCheck: true, 233 | triggerDetectChanges: true, 234 | }), 235 | }; 236 | 237 | @NgModule({ 238 | ... 239 | imports: [ 240 | ... 241 | AutofocusFixModule, 242 | ], 243 | providers: [ autofocusFixConfigProvider ], 244 | }) 245 | export class MyLazyLoadableModule { } 246 | ``` 247 | 248 | **4. Provide Component level `AutofocusFixConfig` config** 249 | ```typescript 250 | import { Component, Provider } from '@angular/core'; 251 | import { AutofocusFixConfig } from 'ngx-autofocus-fix'; 252 | 253 | const autofocusFixConfigProvider: Provider = { 254 | provide: AutofocusFixConfig, 255 | useValue: new AutofocusFixConfig({ 256 | async: true, 257 | smartEmptyCheck: true, 258 | triggerDetectChanges: true, 259 | }), 260 | }; 261 | 262 | @Component({ 263 | ... 264 | providers: [ autofocusFixConfigProvider ], 265 | }) 266 | export class MyComponent {} 267 | ``` 268 | 269 | 270 | ## Development 271 | 272 | Build the library: 273 | 274 | ```bash 275 | $ npm run build 276 | ``` 277 | 278 | Publish the library: 279 | 280 | ```bash 281 | cd dist/ngx-autofocus-fix 282 | npm publish 283 | ``` 284 | 285 | To lint all `*.ts` files: 286 | 287 | ```bash 288 | $ npm run lint 289 | ``` 290 | 291 | To run library unit-tests: 292 | 293 | ```bash 294 | $ npm run test-lib 295 | ``` 296 | 297 | To run e2e tests: 298 | 299 | ```bash 300 | $ npm run e2e -- --prod=true 301 | ``` 302 | 303 | To run local dev server [http://localhost:4200](http://localhost:4200): 304 | 305 | ```bash 306 | $ npm start 307 | $ npm run serve:prod -- angular-8-test # AoT & Prod env 308 | ``` 309 | 310 | ## Author 311 | 312 | | [
Anton Korniychuk](https://korniychuk.pro) | 313 | | :---: | 314 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-autofocus-fix": { 7 | "projectType": "library", 8 | "root": "projects/ngx-autofocus-fix", 9 | "sourceRoot": "projects/ngx-autofocus-fix/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-ng-packagr:build", 14 | "options": { 15 | "tsConfig": "projects/ngx-autofocus-fix/tsconfig.lib.json", 16 | "project": "projects/ngx-autofocus-fix/ng-package.json" 17 | } 18 | }, 19 | "test": { 20 | "builder": "@angular-devkit/build-angular:karma", 21 | "options": { 22 | "codeCoverage": true, 23 | "main": "projects/ngx-autofocus-fix/src/test.ts", 24 | "tsConfig": "projects/ngx-autofocus-fix/tsconfig.spec.json", 25 | "karmaConfig": "projects/ngx-autofocus-fix/karma.conf.js" 26 | } 27 | }, 28 | "lint": { 29 | "builder": "@angular-devkit/build-angular:tslint", 30 | "options": { 31 | "tsConfig": [ 32 | "projects/ngx-autofocus-fix/tsconfig.lib.json", 33 | "projects/ngx-autofocus-fix/tsconfig.spec.json" 34 | ], 35 | "exclude": [ 36 | "**/node_modules/**" 37 | ] 38 | } 39 | } 40 | } 41 | }, 42 | "angular-8-test": { 43 | "projectType": "application", 44 | "schematics": { 45 | "@schematics/angular:component": { 46 | "style": "scss" 47 | } 48 | }, 49 | "root": "projects/angular-8-test", 50 | "sourceRoot": "projects/angular-8-test/src", 51 | "prefix": "app", 52 | "architect": { 53 | "build": { 54 | "builder": "@angular-devkit/build-angular:browser", 55 | "options": { 56 | "outputPath": "dist/angular-8-test", 57 | "preserveSymlinks": true, 58 | "index": "projects/angular-8-test/src/index.html", 59 | "main": "projects/angular-8-test/src/main.ts", 60 | "polyfills": "projects/angular-8-test/src/polyfills.ts", 61 | "tsConfig": "projects/angular-8-test/tsconfig.app.json", 62 | "aot": false, 63 | "assets": [ 64 | "projects/angular-8-test/src/favicon.ico", 65 | "projects/angular-8-test/src/assets" 66 | ], 67 | "styles": [ 68 | "projects/angular-8-test/src/styles.scss" 69 | ], 70 | "scripts": [] 71 | }, 72 | "configurations": { 73 | "production": { 74 | "fileReplacements": [ 75 | { 76 | "replace": "projects/angular-8-test/src/environments/environment.ts", 77 | "with": "projects/angular-8-test/src/environments/environment.prod.ts" 78 | } 79 | ], 80 | "optimization": true, 81 | "outputHashing": "all", 82 | "sourceMap": false, 83 | "extractCss": true, 84 | "namedChunks": false, 85 | "aot": true, 86 | "extractLicenses": true, 87 | "vendorChunk": false, 88 | "buildOptimizer": true, 89 | "budgets": [ 90 | { 91 | "type": "initial", 92 | "maximumWarning": "2mb", 93 | "maximumError": "5mb" 94 | }, 95 | { 96 | "type": "anyComponentStyle", 97 | "maximumWarning": "6kb", 98 | "maximumError": "10kb" 99 | } 100 | ] 101 | } 102 | } 103 | }, 104 | "serve": { 105 | "builder": "@angular-devkit/build-angular:dev-server", 106 | "options": { 107 | "browserTarget": "angular-8-test:build" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "browserTarget": "angular-8-test:build:production" 112 | } 113 | } 114 | }, 115 | "extract-i18n": { 116 | "builder": "@angular-devkit/build-angular:extract-i18n", 117 | "options": { 118 | "browserTarget": "angular-8-test:build" 119 | } 120 | }, 121 | "test": { 122 | "builder": "@angular-devkit/build-angular:karma", 123 | "options": { 124 | "main": "projects/angular-8-test/src/test.ts", 125 | "polyfills": "projects/angular-8-test/src/polyfills.ts", 126 | "tsConfig": "projects/angular-8-test/tsconfig.spec.json", 127 | "karmaConfig": "projects/angular-8-test/karma.conf.js", 128 | "assets": [ 129 | "projects/angular-8-test/src/favicon.ico", 130 | "projects/angular-8-test/src/assets" 131 | ], 132 | "styles": [ 133 | "projects/angular-8-test/src/styles.scss" 134 | ], 135 | "scripts": [] 136 | } 137 | }, 138 | "lint": { 139 | "builder": "@angular-devkit/build-angular:tslint", 140 | "options": { 141 | "tsConfig": [ 142 | "projects/angular-8-test/tsconfig.app.json", 143 | "projects/angular-8-test/tsconfig.spec.json", 144 | "projects/angular-8-test/e2e/tsconfig.json" 145 | ], 146 | "exclude": [ 147 | "**/node_modules/**" 148 | ] 149 | } 150 | }, 151 | "e2e": { 152 | "builder": "@angular-devkit/build-angular:protractor", 153 | "options": { 154 | "protractorConfig": "projects/angular-8-test/e2e/protractor.conf.js", 155 | "devServerTarget": "angular-8-test:serve" 156 | }, 157 | "configurations": { 158 | "production": { 159 | "devServerTarget": "angular-8-test:serve:production" 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | "angular-7-test": { 166 | "projectType": "application", 167 | "schematics": { 168 | "@schematics/angular:component": { 169 | "style": "scss" 170 | } 171 | }, 172 | "root": "projects/angular-7-test", 173 | "sourceRoot": "projects/angular-7-test/src", 174 | "prefix": "app", 175 | "architect": { 176 | "build": { 177 | "builder": "@angular-devkit/build-angular:browser", 178 | "options": { 179 | "preserveSymlinks": true, 180 | "outputPath": "dist/angular-7-test", 181 | "index": "projects/angular-7-test/src/index.html", 182 | "main": "projects/angular-7-test/src/main.ts", 183 | "polyfills": "projects/angular-7-test/src/polyfills.ts", 184 | "tsConfig": "projects/angular-7-test/tsconfig.app.json", 185 | "aot": false, 186 | "assets": [ 187 | "projects/angular-7-test/src/favicon.ico", 188 | "projects/angular-7-test/src/assets" 189 | ], 190 | "styles": [ 191 | "projects/angular-7-test/src/styles.scss" 192 | ], 193 | "scripts": [] 194 | }, 195 | "configurations": { 196 | "production": { 197 | "fileReplacements": [ 198 | { 199 | "replace": "projects/angular-7-test/src/environments/environment.ts", 200 | "with": "projects/angular-7-test/src/environments/environment.prod.ts" 201 | } 202 | ], 203 | "optimization": true, 204 | "outputHashing": "all", 205 | "sourceMap": false, 206 | "extractCss": true, 207 | "namedChunks": false, 208 | "aot": true, 209 | "extractLicenses": true, 210 | "vendorChunk": false, 211 | "buildOptimizer": true, 212 | "budgets": [ 213 | { 214 | "type": "initial", 215 | "maximumWarning": "2mb", 216 | "maximumError": "5mb" 217 | }, 218 | { 219 | "type": "anyComponentStyle", 220 | "maximumWarning": "6kb", 221 | "maximumError": "10kb" 222 | } 223 | ] 224 | } 225 | } 226 | }, 227 | "serve": { 228 | "builder": "@angular-devkit/build-angular:dev-server", 229 | "options": { 230 | "browserTarget": "angular-7-test:build" 231 | }, 232 | "configurations": { 233 | "production": { 234 | "browserTarget": "angular-7-test:build:production" 235 | } 236 | } 237 | }, 238 | "extract-i18n": { 239 | "builder": "@angular-devkit/build-angular:extract-i18n", 240 | "options": { 241 | "browserTarget": "angular-7-test:build" 242 | } 243 | }, 244 | "test": { 245 | "builder": "@angular-devkit/build-angular:karma", 246 | "options": { 247 | "main": "projects/angular-7-test/src/test.ts", 248 | "polyfills": "projects/angular-7-test/src/polyfills.ts", 249 | "tsConfig": "projects/angular-7-test/tsconfig.spec.json", 250 | "karmaConfig": "projects/angular-7-test/karma.conf.js", 251 | "assets": [ 252 | "projects/angular-7-test/src/favicon.ico", 253 | "projects/angular-7-test/src/assets" 254 | ], 255 | "styles": [ 256 | "projects/angular-7-test/src/styles.scss" 257 | ], 258 | "scripts": [] 259 | } 260 | }, 261 | "lint": { 262 | "builder": "@angular-devkit/build-angular:tslint", 263 | "options": { 264 | "tsConfig": [ 265 | "projects/angular-7-test/tsconfig.app.json", 266 | "projects/angular-7-test/tsconfig.spec.json", 267 | "projects/angular-7-test/e2e/tsconfig.json" 268 | ], 269 | "exclude": [ 270 | "**/node_modules/**" 271 | ] 272 | } 273 | }, 274 | "e2e": { 275 | "builder": "@angular-devkit/build-angular:protractor", 276 | "options": { 277 | "protractorConfig": "projects/angular-7-test/e2e/protractor.conf.js", 278 | "devServerTarget": "angular-7-test:serve" 279 | }, 280 | "configurations": { 281 | "production": { 282 | "devServerTarget": "angular-7-test:serve:production" 283 | } 284 | } 285 | } 286 | } 287 | }, 288 | "angular-6-test": { 289 | "projectType": "application", 290 | "schematics": { 291 | "@schematics/angular:component": { 292 | "style": "scss" 293 | } 294 | }, 295 | "root": "projects/angular-6-test", 296 | "sourceRoot": "projects/angular-6-test/src", 297 | "prefix": "app", 298 | "architect": { 299 | "build": { 300 | "builder": "@angular-devkit/build-angular:browser", 301 | "options": { 302 | "preserveSymlinks": true, 303 | "outputPath": "dist/angular-6-test", 304 | "index": "projects/angular-6-test/src/index.html", 305 | "main": "projects/angular-6-test/src/main.ts", 306 | "polyfills": "projects/angular-6-test/src/polyfills.ts", 307 | "tsConfig": "projects/angular-6-test/tsconfig.app.json", 308 | "aot": false, 309 | "assets": [ 310 | "projects/angular-6-test/src/favicon.ico", 311 | "projects/angular-6-test/src/assets" 312 | ], 313 | "styles": [ 314 | "projects/angular-6-test/src/styles.scss" 315 | ], 316 | "scripts": [] 317 | }, 318 | "configurations": { 319 | "production": { 320 | "fileReplacements": [ 321 | { 322 | "replace": "projects/angular-6-test/src/environments/environment.ts", 323 | "with": "projects/angular-6-test/src/environments/environment.prod.ts" 324 | } 325 | ], 326 | "optimization": true, 327 | "outputHashing": "all", 328 | "sourceMap": false, 329 | "extractCss": true, 330 | "namedChunks": false, 331 | "aot": true, 332 | "extractLicenses": true, 333 | "vendorChunk": false, 334 | "buildOptimizer": true, 335 | "budgets": [ 336 | { 337 | "type": "initial", 338 | "maximumWarning": "2mb", 339 | "maximumError": "5mb" 340 | }, 341 | { 342 | "type": "anyComponentStyle", 343 | "maximumWarning": "6kb", 344 | "maximumError": "10kb" 345 | } 346 | ] 347 | } 348 | } 349 | }, 350 | "serve": { 351 | "builder": "@angular-devkit/build-angular:dev-server", 352 | "options": { 353 | "browserTarget": "angular-6-test:build" 354 | }, 355 | "configurations": { 356 | "production": { 357 | "browserTarget": "angular-6-test:build:production" 358 | } 359 | } 360 | }, 361 | "extract-i18n": { 362 | "builder": "@angular-devkit/build-angular:extract-i18n", 363 | "options": { 364 | "browserTarget": "angular-6-test:build" 365 | } 366 | }, 367 | "test": { 368 | "builder": "@angular-devkit/build-angular:karma", 369 | "options": { 370 | "main": "projects/angular-6-test/src/test.ts", 371 | "polyfills": "projects/angular-6-test/src/polyfills.ts", 372 | "tsConfig": "projects/angular-6-test/tsconfig.spec.json", 373 | "karmaConfig": "projects/angular-6-test/karma.conf.js", 374 | "assets": [ 375 | "projects/angular-6-test/src/favicon.ico", 376 | "projects/angular-6-test/src/assets" 377 | ], 378 | "styles": [ 379 | "projects/angular-6-test/src/styles.scss" 380 | ], 381 | "scripts": [] 382 | } 383 | }, 384 | "lint": { 385 | "builder": "@angular-devkit/build-angular:tslint", 386 | "options": { 387 | "tsConfig": [ 388 | "projects/angular-6-test/tsconfig.app.json", 389 | "projects/angular-6-test/tsconfig.spec.json", 390 | "projects/angular-6-test/e2e/tsconfig.json" 391 | ], 392 | "exclude": [ 393 | "**/node_modules/**" 394 | ] 395 | } 396 | }, 397 | "e2e": { 398 | "builder": "@angular-devkit/build-angular:protractor", 399 | "options": { 400 | "protractorConfig": "projects/angular-6-test/e2e/protractor.conf.js", 401 | "devServerTarget": "angular-6-test:serve" 402 | }, 403 | "configurations": { 404 | "production": { 405 | "devServerTarget": "angular-6-test:serve:production" 406 | } 407 | } 408 | } 409 | } 410 | }, 411 | "angular-5-test": { 412 | "projectType": "application", 413 | "schematics": { 414 | "@schematics/angular:component": { 415 | "style": "scss" 416 | } 417 | }, 418 | "root": "projects/angular-5-test", 419 | "sourceRoot": "projects/angular-5-test/src", 420 | "prefix": "app", 421 | "architect": { 422 | "build": { 423 | "builder": "@angular-devkit/build-angular:browser", 424 | "options": { 425 | "preserveSymlinks": true, 426 | "outputPath": "dist/angular-5-test", 427 | "index": "projects/angular-5-test/src/index.html", 428 | "main": "projects/angular-5-test/src/main.ts", 429 | "polyfills": "projects/angular-5-test/src/polyfills.ts", 430 | "tsConfig": "projects/angular-5-test/tsconfig.app.json", 431 | "aot": false, 432 | "assets": [ 433 | "projects/angular-5-test/src/favicon.ico", 434 | "projects/angular-5-test/src/assets" 435 | ], 436 | "styles": [ 437 | "projects/angular-5-test/src/styles.scss" 438 | ], 439 | "scripts": [] 440 | }, 441 | "configurations": { 442 | "production": { 443 | "fileReplacements": [ 444 | { 445 | "replace": "projects/angular-5-test/src/environments/environment.ts", 446 | "with": "projects/angular-5-test/src/environments/environment.prod.ts" 447 | } 448 | ], 449 | "optimization": true, 450 | "outputHashing": "all", 451 | "sourceMap": false, 452 | "extractCss": true, 453 | "namedChunks": false, 454 | "aot": true, 455 | "extractLicenses": true, 456 | "vendorChunk": false, 457 | "buildOptimizer": true, 458 | "budgets": [ 459 | { 460 | "type": "initial", 461 | "maximumWarning": "2mb", 462 | "maximumError": "5mb" 463 | }, 464 | { 465 | "type": "anyComponentStyle", 466 | "maximumWarning": "6kb", 467 | "maximumError": "10kb" 468 | } 469 | ] 470 | } 471 | } 472 | }, 473 | "serve": { 474 | "builder": "@angular-devkit/build-angular:dev-server", 475 | "options": { 476 | "browserTarget": "angular-5-test:build" 477 | }, 478 | "configurations": { 479 | "production": { 480 | "browserTarget": "angular-5-test:build:production" 481 | } 482 | } 483 | }, 484 | "extract-i18n": { 485 | "builder": "@angular-devkit/build-angular:extract-i18n", 486 | "options": { 487 | "browserTarget": "angular-5-test:build" 488 | } 489 | }, 490 | "test": { 491 | "builder": "@angular-devkit/build-angular:karma", 492 | "options": { 493 | "main": "projects/angular-5-test/src/test.ts", 494 | "polyfills": "projects/angular-5-test/src/polyfills.ts", 495 | "tsConfig": "projects/angular-5-test/tsconfig.spec.json", 496 | "karmaConfig": "projects/angular-5-test/karma.conf.js", 497 | "assets": [ 498 | "projects/angular-5-test/src/favicon.ico", 499 | "projects/angular-5-test/src/assets" 500 | ], 501 | "styles": [ 502 | "projects/angular-5-test/src/styles.scss" 503 | ], 504 | "scripts": [] 505 | } 506 | }, 507 | "lint": { 508 | "builder": "@angular-devkit/build-angular:tslint", 509 | "options": { 510 | "tsConfig": [ 511 | "projects/angular-5-test/tsconfig.app.json", 512 | "projects/angular-5-test/tsconfig.spec.json", 513 | "projects/angular-5-test/e2e/tsconfig.json" 514 | ], 515 | "exclude": [ 516 | "**/node_modules/**" 517 | ] 518 | } 519 | }, 520 | "e2e": { 521 | "builder": "@angular-devkit/build-angular:protractor", 522 | "options": { 523 | "protractorConfig": "projects/angular-5-test/e2e/protractor.conf.js", 524 | "devServerTarget": "angular-5-test:serve" 525 | }, 526 | "configurations": { 527 | "production": { 528 | "devServerTarget": "angular-5-test:serve:production" 529 | } 530 | } 531 | } 532 | } 533 | } 534 | }, 535 | "defaultProject": "ngx-autofocus-fix" 536 | } 537 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-autofocus-fix--workspace", 3 | "version": "0.0.0", 4 | "engines": { 5 | "node": ">=8.0.0 <13.0.0" 6 | }, 7 | "scripts": { 8 | "ng": "ng", 9 | "start": "npm run serve", 10 | "serve": "ng serve angular-8-test", 11 | "serve:prod": "npm run serve -- --aot --prod=true", 12 | "build": "ng build ngx-autofocus-fix", 13 | "test": "ng test ngx-autofocus-fix --browsers=ChromeHeadlessCI", 14 | "test:ci": "npm run test -- --no-watch --no-progress", 15 | "coveralls": "cat ./coverage/ngx-autofocus-fix/lcov.info | ./node_modules/.bin/coveralls", 16 | "lint": "ng lint", 17 | "e2e": "ng e2e", 18 | "e2e:prod": "npm run e2e -- --prod=true", 19 | "e2e-install": "cd projects/angular-8-test && npm i && cd ../angular-7-test && npm i && cd ../angular-6-test && npm i && cd ../angular-5-test && npm i", 20 | "prepare-ver": "ts-node -P ./tools/tsconfig.json ./tools/prepare-version.ts", 21 | "publish": "npm publish dist/ngx-autofocus-fix" 22 | }, 23 | "private": true, 24 | "dependencies": { 25 | "@angular/animations": "~8.2.0", 26 | "@angular/common": "~8.2.0", 27 | "@angular/compiler": "~8.2.0", 28 | "@angular/core": "~8.2.0", 29 | "@angular/forms": "~8.2.0", 30 | "@angular/platform-browser": "~8.2.0", 31 | "@angular/platform-browser-dynamic": "~8.2.0", 32 | "@angular/router": "~8.2.0", 33 | "rxjs": "~6.4.0", 34 | "tslib": "^1.10.0", 35 | "zone.js": "~0.9.1" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "~0.802.2", 39 | "@angular-devkit/build-ng-packagr": "~0.802.2", 40 | "@angular/cli": "~8.2.2", 41 | "@angular/compiler-cli": "~8.2.0", 42 | "@angular/language-service": "~8.2.0", 43 | "@types/jasmine": "~3.3.8", 44 | "@types/jasminewd2": "~2.0.3", 45 | "@types/node": "~8.9.4", 46 | "codelyzer": "^5.0.0", 47 | "coveralls": "~3.0.6", 48 | "jasmine-core": "~3.4.0", 49 | "jasmine-spec-reporter": "~4.2.1", 50 | "karma": "~4.1.0", 51 | "karma-chrome-launcher": "~2.2.0", 52 | "karma-coverage-istanbul-reporter": "~2.0.1", 53 | "karma-jasmine": "~2.0.1", 54 | "karma-jasmine-html-reporter": "^1.4.0", 55 | "ng-packagr": "^5.3.0", 56 | "protractor": "~5.4.0", 57 | "ts-node": "~7.0.0", 58 | "tsickle": "^0.36.0", 59 | "tslint": "~5.15.0", 60 | "typescript": "~3.5.3", 61 | "wallaby-webpack": "^3.9.15" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /projects/angular-5-test/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | -------------------------------------------------------------------------------- /projects/angular-5-test/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/angular-5-test/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome', 17 | chromeOptions: { 18 | args: [ "--headless", "--disable-gpu", "--window-size=800x600" ] 19 | }, 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:4200/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /projects/angular-5-test/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging, protractor } from 'protractor'; 3 | 4 | const expectedAngularMajorVersion = 5; 5 | 6 | describe(`Angular ${expectedAngularMajorVersion}`, () => { 7 | let page: AppPage; 8 | 9 | beforeEach(() => { 10 | page = new AppPage(); 11 | }); 12 | 13 | describe('SCENARIO: App initialization', () => { 14 | describe('WHEN: Page opened', () => { 15 | it('THEN: should display welcome message', () => { 16 | page.navigateTo(); 17 | expect(page.getTitleText()).toEqual('Welcome to angular-*-test!'); 18 | }); 19 | }); 20 | it(`Angular version should be ${expectedAngularMajorVersion}.*.*`, () => { 21 | expect(page.getAngularVersion()).toMatch(new RegExp(`^${expectedAngularMajorVersion}\\.\\d+\.\\d+$`)); 22 | }); 23 | }); 24 | 25 | describe('SCENARIO: Autofocusing', () => { 26 | 27 | beforeAll( () => { 28 | browser.waitForAngularEnabled(false); 29 | }); 30 | 31 | afterAll(() => { 32 | browser.waitForAngularEnabled(true); 33 | }); 34 | 35 | it('Should move focus to the each newly create input', async () => { 36 | page.getRunButton().click(); 37 | expect(page.getInputsCount()).toBe(0); 38 | 39 | await browser.sleep(25); 40 | expect(page.getInputsCount()).toBe(0); 41 | 42 | const until = protractor.ExpectedConditions; 43 | for (let i = 0; i < 5; i++) { 44 | await browser.wait(until.presenceOf(page.getElementByE2eAttr('input-' + i)), 150, 'Element taking too long to appear in the DOM'); 45 | await browser.waitForAngular(); 46 | 47 | expect(page.getInputsCount()).toBe(i + 1); 48 | const focusIndex = i - i % 2; 49 | expect(page.getE2eAttrOfFocusedElement()).toBe('input-' + focusIndex); 50 | } 51 | }); 52 | }); 53 | 54 | afterEach(async () => { 55 | // Assert that there are no errors emitted from the browser 56 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 57 | expect(logs).not.toContain(jasmine.objectContaining({ 58 | level: logging.Level.SEVERE, 59 | } as logging.Entry)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /projects/angular-5-test/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | import { AppComponent } from '../../src/app/app.component'; 3 | 4 | export class AppPage { 5 | public navigateTo() { 6 | return browser.get(browser.baseUrl); 7 | } 8 | 9 | public getTitleText() { 10 | return element(by.css('app-root h1')).getText(); 11 | } 12 | 13 | public getE2eAttrOfFocusedElement() { 14 | return element(by.css(':focus')).getAttribute('e2e-attr') as Promise; 15 | } 16 | 17 | public getInputsCount() { 18 | return browser.$$('.autofocus-input').count(); 19 | } 20 | 21 | public getRunButton() { 22 | return element(by.css('[e2e-attr=run]')); 23 | } 24 | 25 | public getElementByE2eAttr(value: string) { 26 | return element(by.css(`[e2e-attr=${value}]`)); 27 | } 28 | 29 | public getAngularVersion() { 30 | return element(by.css('app-root')).getAttribute('ng-version'); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /projects/angular-5-test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-5-test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular-8-test'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/angular-5-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix--test", 3 | "license": "UNLICENSED", 4 | "description": "Test with Angular", 5 | "repository": { 6 | "type": "https", 7 | "url": "https://github.com/korniychuk/angular-autofocus-fix" 8 | }, 9 | "dependencies": { 10 | "@angular/animations": "^5.0.0", 11 | "@angular/common": "^5.0.0", 12 | "@angular/compiler": "^5.0.0", 13 | "@angular/core": "^5.0.0", 14 | "@angular/forms": "^5.0.0", 15 | "@angular/platform-browser": "^5.0.0", 16 | "@angular/platform-browser-dynamic": "^5.0.0", 17 | "@angular/router": "^5.0.0", 18 | "rxjs": "~5.5.12", 19 | "zone.js": "~0.8.26", 20 | "ngx-autofocus-fix": "file:../../dist/ngx-autofocus-fix" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to {{ title }}!

3 |
4 | 5 |
6 | 12 |
13 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-8-test'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-8-test'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-8-test!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Subject } from 'rxjs/Subject'; 5 | import { interval } from 'rxjs/observable/interval'; 6 | import { scan, switchMap, take } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | }) 12 | export class AppComponent implements OnInit { 13 | public title = 'angular-*-test'; 14 | public numbers$!: Observable; 15 | 16 | private run$ = new Subject(); 17 | 18 | public ngOnInit() { 19 | this.numbers$ = this.run$.pipe( 20 | switchMap(() => interval(100)), 21 | take(5), 22 | scan((acc: number[], i: number) => (acc.push(i), acc), []), 23 | ); 24 | } 25 | 26 | public run(): void { 27 | this.run$.next(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; 6 | 7 | import { AppComponent } from './app.component'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | AppComponent 12 | ], 13 | imports: [ 14 | BrowserModule, 15 | CommonModule, 16 | AutofocusFixModule.forRoot(), 17 | ], 18 | providers: [], 19 | bootstrap: [AppComponent] 20 | }) 21 | export class AppModule { } 22 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-5-test/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/angular-5-test/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-5-test/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-5-test/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularTest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-5-test/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/angular-5-test/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-5-test/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-5-test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/angular-6-test/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | -------------------------------------------------------------------------------- /projects/angular-6-test/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/angular-6-test/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome', 17 | chromeOptions: { 18 | args: [ "--headless", "--disable-gpu", "--window-size=800x600" ] 19 | }, 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:4200/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /projects/angular-6-test/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging, protractor } from 'protractor'; 3 | 4 | const expectedAngularMajorVersion = 6; 5 | 6 | describe(`Angular ${expectedAngularMajorVersion}`, () => { 7 | let page: AppPage; 8 | 9 | beforeEach(() => { 10 | page = new AppPage(); 11 | }); 12 | 13 | describe('SCENARIO: App initialization', () => { 14 | describe('WHEN: Page opened', () => { 15 | it('THEN: should display welcome message', () => { 16 | page.navigateTo(); 17 | expect(page.getTitleText()).toEqual('Welcome to angular-*-test!'); 18 | }); 19 | }); 20 | it(`Angular version should be ${expectedAngularMajorVersion}.*.*`, () => { 21 | expect(page.getAngularVersion()).toMatch(new RegExp(`^${expectedAngularMajorVersion}\\.\\d+\.\\d+$`)); 22 | }); 23 | }); 24 | 25 | describe('SCENARIO: Autofocusing', () => { 26 | 27 | beforeAll( () => { 28 | browser.waitForAngularEnabled(false); 29 | }); 30 | 31 | afterAll(() => { 32 | browser.waitForAngularEnabled(true); 33 | }); 34 | 35 | it('Should move focus to the each newly create input', async () => { 36 | page.getRunButton().click(); 37 | expect(page.getInputsCount()).toBe(0); 38 | 39 | await browser.sleep(25); 40 | expect(page.getInputsCount()).toBe(0); 41 | 42 | const until = protractor.ExpectedConditions; 43 | for (let i = 0; i < 5; i++) { 44 | await browser.wait(until.presenceOf(page.getElementByE2eAttr('input-' + i)), 150, 'Element taking too long to appear in the DOM'); 45 | await browser.waitForAngular(); 46 | 47 | expect(page.getInputsCount()).toBe(i + 1); 48 | const focusIndex = i - i % 2; 49 | expect(page.getE2eAttrOfFocusedElement()).toBe('input-' + focusIndex); 50 | } 51 | }); 52 | }); 53 | 54 | afterEach(async () => { 55 | // Assert that there are no errors emitted from the browser 56 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 57 | expect(logs).not.toContain(jasmine.objectContaining({ 58 | level: logging.Level.SEVERE, 59 | } as logging.Entry)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /projects/angular-6-test/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | import { AppComponent } from '../../src/app/app.component'; 3 | 4 | export class AppPage { 5 | public navigateTo() { 6 | return browser.get(browser.baseUrl); 7 | } 8 | 9 | public getTitleText() { 10 | return element(by.css('app-root h1')).getText(); 11 | } 12 | 13 | public getE2eAttrOfFocusedElement() { 14 | return element(by.css(':focus')).getAttribute('e2e-attr') as Promise; 15 | } 16 | 17 | public getInputsCount() { 18 | return browser.$$('.autofocus-input').count(); 19 | } 20 | 21 | public getRunButton() { 22 | return element(by.css('[e2e-attr=run]')); 23 | } 24 | 25 | public getElementByE2eAttr(value: string) { 26 | return element(by.css(`[e2e-attr=${value}]`)); 27 | } 28 | 29 | public getAngularVersion() { 30 | return element(by.css('app-root')).getAttribute('ng-version'); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /projects/angular-6-test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-6-test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular-8-test'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/angular-6-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix--test", 3 | "license": "UNLICENSED", 4 | "description": "Test with Angular", 5 | "repository": { 6 | "type": "https", 7 | "url": "https://github.com/korniychuk/angular-autofocus-fix" 8 | }, 9 | "dependencies": { 10 | "@angular/animations": "^6.0.0", 11 | "@angular/common": "^6.0.0", 12 | "@angular/compiler": "^6.0.0", 13 | "@angular/core": "^6.0.0", 14 | "@angular/forms": "^6.0.0", 15 | "@angular/platform-browser": "^6.0.0", 16 | "@angular/platform-browser-dynamic": "^6.0.0", 17 | "@angular/router": "^6.0.0", 18 | "rxjs": "~6.0.0", 19 | "zone.js": "~0.8.26", 20 | "ngx-autofocus-fix": "file:../../dist/ngx-autofocus-fix" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to {{ title }}!

3 |
4 | 5 |
6 | 12 |
13 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-8-test'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-8-test'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-8-test!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable, Subject, interval } from 'rxjs'; 4 | import { scan, switchMap, take } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | }) 10 | export class AppComponent implements OnInit { 11 | public title = 'angular-*-test'; 12 | public numbers$!: Observable; 13 | 14 | private run$ = new Subject(); 15 | 16 | public ngOnInit() { 17 | this.numbers$ = this.run$.pipe( 18 | switchMap(() => interval(100)), 19 | take(5), 20 | scan((acc: number[], i: number) => (acc.push(i), acc), []), 21 | ); 22 | } 23 | 24 | public run(): void { 25 | this.run$.next(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | CommonModule, 15 | AutofocusFixModule.forRoot(), 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule { } 21 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-6-test/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/angular-6-test/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-6-test/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-6-test/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularTest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-6-test/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/angular-6-test/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-6-test/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-6-test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/angular-7-test/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | -------------------------------------------------------------------------------- /projects/angular-7-test/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/angular-7-test/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome', 17 | chromeOptions: { 18 | args: [ "--headless", "--disable-gpu", "--window-size=800x600" ] 19 | }, 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:4200/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /projects/angular-7-test/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging, protractor } from 'protractor'; 3 | 4 | const expectedAngularMajorVersion = 7; 5 | 6 | describe(`Angular ${expectedAngularMajorVersion}`, () => { 7 | let page: AppPage; 8 | 9 | beforeEach(() => { 10 | page = new AppPage(); 11 | }); 12 | 13 | describe('SCENARIO: App initialization', () => { 14 | describe('WHEN: Page opened', () => { 15 | it('THEN: should display welcome message', () => { 16 | page.navigateTo(); 17 | expect(page.getTitleText()).toEqual('Welcome to angular-*-test!'); 18 | }); 19 | }); 20 | it(`Angular version should be ${expectedAngularMajorVersion}.*.*`, () => { 21 | expect(page.getAngularVersion()).toMatch(new RegExp(`^${expectedAngularMajorVersion}\\.\\d+\.\\d+$`)); 22 | }); 23 | }); 24 | 25 | describe('SCENARIO: Autofocusing', () => { 26 | 27 | beforeAll( () => { 28 | browser.waitForAngularEnabled(false); 29 | }); 30 | 31 | afterAll(() => { 32 | browser.waitForAngularEnabled(true); 33 | }); 34 | 35 | it('Should move focus to the each newly create input', async () => { 36 | page.getRunButton().click(); 37 | expect(page.getInputsCount()).toBe(0); 38 | 39 | await browser.sleep(25); 40 | expect(page.getInputsCount()).toBe(0); 41 | 42 | const until = protractor.ExpectedConditions; 43 | for (let i = 0; i < 5; i++) { 44 | await browser.wait(until.presenceOf(page.getElementByE2eAttr('input-' + i)), 150, 'Element taking too long to appear in the DOM'); 45 | await browser.waitForAngular(); 46 | 47 | expect(page.getInputsCount()).toBe(i + 1); 48 | const focusIndex = i - i % 2; 49 | expect(page.getE2eAttrOfFocusedElement()).toBe('input-' + focusIndex); 50 | } 51 | }); 52 | }); 53 | 54 | afterEach(async () => { 55 | // Assert that there are no errors emitted from the browser 56 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 57 | expect(logs).not.toContain(jasmine.objectContaining({ 58 | level: logging.Level.SEVERE, 59 | } as logging.Entry)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /projects/angular-7-test/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | import { AppComponent } from '../../src/app/app.component'; 3 | 4 | export class AppPage { 5 | public navigateTo() { 6 | return browser.get(browser.baseUrl); 7 | } 8 | 9 | public getTitleText() { 10 | return element(by.css('app-root h1')).getText(); 11 | } 12 | 13 | public getE2eAttrOfFocusedElement() { 14 | return element(by.css(':focus')).getAttribute('e2e-attr') as Promise; 15 | } 16 | 17 | public getInputsCount() { 18 | return browser.$$('.autofocus-input').count(); 19 | } 20 | 21 | public getRunButton() { 22 | return element(by.css('[e2e-attr=run]')); 23 | } 24 | 25 | public getElementByE2eAttr(value: string) { 26 | return element(by.css(`[e2e-attr=${value}]`)); 27 | } 28 | 29 | public getAngularVersion() { 30 | return element(by.css('app-root')).getAttribute('ng-version'); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /projects/angular-7-test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-7-test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular-8-test'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/angular-7-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix--test", 3 | "license": "UNLICENSED", 4 | "description": "Test with Angular", 5 | "repository": { 6 | "type": "https", 7 | "url": "https://github.com/korniychuk/angular-autofocus-fix" 8 | }, 9 | "dependencies": { 10 | "@angular/animations": "^7.0.0", 11 | "@angular/common": "^7.0.0", 12 | "@angular/compiler": "^7.0.0", 13 | "@angular/core": "^7.0.0", 14 | "@angular/forms": "^7.0.0", 15 | "@angular/platform-browser": "^7.0.0", 16 | "@angular/platform-browser-dynamic": "^7.0.0", 17 | "@angular/router": "^7.0.0", 18 | "rxjs": "~6.0.0", 19 | "zone.js": "~0.8.26", 20 | "ngx-autofocus-fix": "file:../../dist/ngx-autofocus-fix" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to {{ title }}!

3 |
4 | 5 |
6 | 12 |
13 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-8-test'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-8-test'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-8-test!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable, Subject, interval } from 'rxjs'; 4 | import { scan, switchMap, take } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | }) 10 | export class AppComponent implements OnInit { 11 | public title = 'angular-*-test'; 12 | public numbers$!: Observable; 13 | 14 | private run$ = new Subject(); 15 | 16 | public ngOnInit() { 17 | this.numbers$ = this.run$.pipe( 18 | switchMap(() => interval(100)), 19 | take(5), 20 | scan((acc: number[], i: number) => (acc.push(i), acc), []), 21 | ); 22 | } 23 | 24 | public run(): void { 25 | this.run$.next(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | CommonModule, 15 | AutofocusFixModule.forRoot(), 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule { } 21 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-7-test/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/angular-7-test/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-7-test/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-7-test/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularTest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-7-test/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/angular-7-test/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-7-test/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-7-test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/angular-8-test/.gitignore: -------------------------------------------------------------------------------- 1 | /package-lock.json 2 | -------------------------------------------------------------------------------- /projects/angular-8-test/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/angular-8-test/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome', 17 | chromeOptions: { 18 | args: [ "--headless", "--disable-gpu", "--window-size=800x600" ] 19 | }, 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:4200/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /projects/angular-8-test/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging, protractor } from 'protractor'; 2 | import { environment } from '../../src/environments/environment'; 3 | import { AppPage } from './app.po'; 4 | 5 | const expectedAngularMajorVersion = 8; 6 | 7 | describe(`Angular ${expectedAngularMajorVersion}`, () => { 8 | let page: AppPage; 9 | 10 | beforeEach(() => { 11 | page = new AppPage(); 12 | }); 13 | 14 | describe('SCENARIO: App initialization', () => { 15 | describe('WHEN: Page opened', () => { 16 | it('THEN: should display welcome message', () => { 17 | page.navigateTo(); 18 | expect(page.getTitleText()).toEqual('Welcome to angular-*-test!'); 19 | }); 20 | }); 21 | it(`Angular version should be ${expectedAngularMajorVersion}.*.*`, () => { 22 | expect(page.getAngularVersion()).toMatch(new RegExp(`^${expectedAngularMajorVersion}\\.\\d+\.\\d+$`)); 23 | }); 24 | }); 25 | 26 | describe('SCENARIO: Autofocusing simple inputs', () => { 27 | 28 | beforeAll( () => { 29 | browser.waitForAngularEnabled(false); 30 | }); 31 | 32 | afterAll(() => { 33 | browser.waitForAngularEnabled(true); 34 | }); 35 | 36 | it('Should move focus to the each even newly create input', async () => { 37 | page.getSimpleRunButton().click(); 38 | expect(page.getSimpleInputCount()).toBe(0); 39 | 40 | await browser.sleep(25); 41 | expect(page.getSimpleInputCount()).toBe(0); 42 | 43 | const until = protractor.ExpectedConditions; 44 | for (let i = 0; i < 5; i++) { 45 | await browser.wait( 46 | until.presenceOf(page.getElementByE2eAttr('input-' + i)), 47 | environment.inputsGenerationIntervalMs + 50, 48 | 'Element taking too long to appear in the DOM', 49 | ); 50 | await browser.waitForAngular(); 51 | 52 | expect(page.getSimpleInputCount()).toBe(i + 1); 53 | const focusIndex = i - i % 2; 54 | expect(page.getE2eAttrOfFocusedElement()).toBe('input-' + focusIndex); 55 | } 56 | }); 57 | }); 58 | 59 | describe('SCENARIO: Autofocusing Angular Material inputs', () => { 60 | 61 | beforeAll( () => { 62 | browser.waitForAngularEnabled(false); 63 | }); 64 | 65 | afterAll(() => { 66 | browser.waitForAngularEnabled(true); 67 | }); 68 | 69 | it('Should move focus to the each even newly create input', async () => { 70 | page.getMaterialRunButton().click(); 71 | expect(page.getMaterialInputCount()).toBe(0); 72 | 73 | await browser.sleep(25); 74 | expect(page.getMaterialInputCount()).toBe(0); 75 | 76 | const until = protractor.ExpectedConditions; 77 | for (let i = 0; i < 5; i++) { 78 | await browser.wait( 79 | until.presenceOf(page.getElementByE2eAttr('material-input-' + i)), 80 | environment.inputsGenerationIntervalMs + 50, 81 | 'Element taking too long to appear in the DOM', 82 | ); 83 | await browser.waitForAngular(); 84 | 85 | expect(page.getMaterialInputCount()).toBe(i + 1); 86 | const focusIndex = i - i % 2; 87 | expect(page.getE2eAttrOfFocusedElement()).toBe('material-input-' + focusIndex); 88 | } 89 | }); 90 | }); 91 | 92 | afterEach(async () => { 93 | // Assert that there are no errors emitted from the browser 94 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 95 | expect(logs).not.toContain(jasmine.objectContaining({ 96 | level: logging.Level.SEVERE, 97 | } as logging.Entry)); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /projects/angular-8-test/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | import { AppComponent } from '../../src/app/app.component'; 3 | 4 | export class AppPage { 5 | public navigateTo() { 6 | return browser.get(browser.baseUrl); 7 | } 8 | 9 | public getAngularVersion() { 10 | return element(by.css('app-root')).getAttribute('ng-version'); 11 | } 12 | 13 | public getTitleText() { 14 | return element(by.css('app-root h1')).getText(); 15 | } 16 | 17 | public getE2eAttrOfFocusedElement() { 18 | return element(by.css(':focus')).getAttribute('e2e-attr') as Promise; 19 | } 20 | 21 | public getElementByE2eAttr(value: string) { 22 | return element(by.css(`[e2e-attr=${value}]`)); 23 | } 24 | 25 | public getElementsCount(selector: string) { 26 | return browser.$$(selector).count(); 27 | } 28 | 29 | // 30 | // Simple Inputs 31 | // 32 | 33 | public getSimpleInputCount() { 34 | return this.getElementsCount('.autofocus-input'); 35 | } 36 | 37 | public getSimpleRunButton() { 38 | return element(by.css('[e2e-attr=run]')); 39 | } 40 | 41 | // 42 | // Material Inputs 43 | // 44 | 45 | public getMaterialInputCount() { 46 | return this.getElementsCount('.autofocus-material-input'); 47 | } 48 | 49 | public getMaterialRunButton() { 50 | return element(by.css('[e2e-attr=run-material]')); 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /projects/angular-8-test/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-8-test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular-8-test'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/angular-8-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix--test", 3 | "license": "UNLICENSED", 4 | "description": "Test with Angular", 5 | "repository": { 6 | "type": "https", 7 | "url": "https://github.com/korniychuk/angular-autofocus-fix" 8 | }, 9 | "dependencies": { 10 | "@angular/cdk": "^8.1.3", 11 | "@angular/material": "^8.1.3", 12 | "@angular/animations": "^8.0.0", 13 | "@angular/common": "^8.0.0", 14 | "@angular/compiler": "^8.0.0", 15 | "@angular/core": "^8.0.0", 16 | "@angular/forms": "^8.0.0", 17 | "@angular/platform-browser": "^8.0.0", 18 | "@angular/platform-browser-dynamic": "^8.0.0", 19 | "@angular/router": "^8.0.0", 20 | "rxjs": "~6.4.0", 21 | "zone.js": "~0.9.1", 22 | "ngx-autofocus-fix": "file:../../dist/ngx-autofocus-fix" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to {{ title }}!

3 |
4 |
5 | 6 | 7 |
8 |

Default inputs:

9 | 10 |
11 | 17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-8-test'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-8-test'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-8-test!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable, Subject, interval } from 'rxjs'; 4 | import * as $ from 'rxjs/operators'; 5 | 6 | import { environment } from '../environments'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styles: [` 12 | .autofocus-input { 13 | width: 50px; 14 | } 15 | .autofocus-input + .autofocus-input { 16 | margin-left: 10px; 17 | } 18 | `], 19 | }) 20 | export class AppComponent implements OnInit { 21 | public title = 'angular-*-test'; 22 | public showInput = false; 23 | public numbers$!: Observable; 24 | 25 | private run$ = new Subject(); 26 | 27 | public ngOnInit() { 28 | this.numbers$ = this.run$.pipe( 29 | $.switchMap(() => interval(environment.inputsGenerationIntervalMs)), 30 | $.take(5), 31 | $.scan((acc: number[], i: number) => (acc.push(i), acc), []), 32 | ); 33 | } 34 | 35 | public run(): void { 36 | this.run$.next(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { MaterialTestModule } from './material-test/material-test.module'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | AppComponent 13 | ], 14 | imports: [ 15 | BrowserModule, 16 | CommonModule, 17 | 18 | AutofocusFixModule.forRoot({ smartEmptyCheck: true }), 19 | 20 | MaterialTestModule, 21 | ], 22 | bootstrap: [AppComponent], 23 | providers: [], 24 | }) 25 | export class AppModule { } 26 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/material-test/material-test.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable, Subject, interval } from 'rxjs'; 4 | import * as $ from 'rxjs/operators'; 5 | 6 | import { environment } from '../../environments'; 7 | 8 | @Component({ 9 | selector: 'app-material-test', 10 | styles: [ 11 | ` 12 | input { 13 | border: 1px solid #CCC; 14 | } 15 | input:focus { 16 | outline: #18E auto 5px; 17 | } 18 | `, 19 | ], 20 | template: ` 21 |

Angular Material inputs:

22 | 23 |
24 |
25 | 26 | 35 | 36 |
37 |
38 | `, 39 | }) 40 | export class MaterialTestComponent implements OnInit { 41 | 42 | public numbers$!: Observable; 43 | 44 | private run$ = new Subject(); 45 | 46 | public ngOnInit() { 47 | this.numbers$ = this.run$.pipe( 48 | $.switchMap(() => interval(environment.inputsGenerationIntervalMs)), 49 | $.take(5), 50 | $.scan((acc: number[], i: number) => (acc.push(i), acc), []), 51 | ); 52 | } 53 | 54 | public run(): void { 55 | this.run$.next(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/app/material-test/material-test.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { AutofocusFixModule } from 'ngx-autofocus-fix'; 8 | 9 | import { MaterialTestComponent } from './material-test.component'; 10 | 11 | @NgModule({ 12 | imports: [ 13 | CommonModule, 14 | NoopAnimationsModule, 15 | FormsModule, 16 | ReactiveFormsModule, 17 | 18 | MatInputModule, 19 | 20 | AutofocusFixModule, 21 | ], 22 | declarations: [MaterialTestComponent], 23 | exports: [MaterialTestComponent], 24 | }) 25 | export class MaterialTestModule { } 26 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-8-test/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/angular-8-test/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | inputsGenerationIntervalMs: 100, 4 | }; 5 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | inputsGenerationIntervalMs: 1000, 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/environments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './environment'; 2 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/korniychuk/angular-autofocus-fix/60eb36792206c1538353afe58881d5a068820a83/projects/angular-8-test/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-8-test/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularTest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-8-test/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /projects/angular-8-test/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-8-test/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-8-test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/ngx-autofocus-fix'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | thresholds: { 23 | statements: 100, 24 | lines: 100, 25 | branches: 100, 26 | functions: 100, 27 | }, 28 | }, 29 | reporters: ['progress', 'kjhtml'], 30 | port: 9876, 31 | colors: true, 32 | logLevel: config.LOG_INFO, 33 | autoWatch: true, 34 | browsers: ['Chrome'], 35 | customLaunchers: { 36 | ChromeHeadlessCI: { 37 | base: 'ChromeHeadless', 38 | flags: ['--no-sandbox'] 39 | } 40 | }, 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-autofocus-fix", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix", 3 | "version": "1.0.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ngx-autofocus-fix": { 8 | "version": "file:../../dist/ngx-autofocus-fix", 9 | "requires": { 10 | "tslib": "^1.9.0" 11 | }, 12 | "dependencies": { 13 | "tslib": { 14 | "version": "1.10.0", 15 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 16 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-autofocus-fix", 3 | "version": "1.0.4", 4 | "repository": { 5 | "type": "git", 6 | "url": "git@github.com:korniychuk/angular-autofocus-fix.git" 7 | }, 8 | "author": { 9 | "name": "Anton Korniychuk", 10 | "email": "dev@korniychuk.pro", 11 | "url": "https://korniychuk.pro" 12 | }, 13 | "engines": { 14 | "node": ">=8.0.0 <13.0.0" 15 | }, 16 | "keywords": [ 17 | "angular-autofocus-fix", 18 | "angular-autofocus", 19 | "angular2-autofocus", 20 | "angular2-focus", 21 | "ng-autofocus", 22 | "ng-focus", 23 | "ng2-autofocus", 24 | "ng5-autofocus", 25 | "ng6-autofocus", 26 | "ng7-autofocus", 27 | "ng8-autofocus", 28 | "ng9-autofocus", 29 | "autofocus", 30 | "focus", 31 | "autofocus-directive", 32 | "angular", 33 | "angular5", 34 | "angular6", 35 | "angular7", 36 | "angular8", 37 | "angular9", 38 | "ng", 39 | "ngx" 40 | ], 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/korniychuk/angular-autofocus-fix/issues" 44 | }, 45 | "peerDependencies": { 46 | "@angular/common": ">=5.0.0", 47 | "@angular/core": ">=5.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix-config.spec.ts: -------------------------------------------------------------------------------- 1 | import { AutofocusFixConfig } from './autofocus-fix-config'; 2 | 3 | describe('AutofocusFixConfig', () => { 4 | describe('SCENARIO: Instantiating', () => { 5 | 6 | describe('GIVEN: Have default values', () => { 7 | describe('WHEN: Instance created with empty options', () => { 8 | it('THEN: All fields should have default values', () => { 9 | // arrange 10 | const expected: {[key in keyof AutofocusFixConfig]: any} = { 11 | async: jasmine.any(Boolean), 12 | smartEmptyCheck: jasmine.any(Boolean), 13 | triggerDetectChanges: jasmine.any(Boolean), 14 | }; 15 | 16 | // act 17 | const ins = new AutofocusFixConfig({}); 18 | 19 | // assert 20 | expect(ins).toEqual(jasmine.objectContaining(expected)); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('GIVEN: Partial options overriding', () => { 26 | describe('WHEN: Instance created with not all options', () => { 27 | it('THEN: Custom options should override default', () => { 28 | // arrange 29 | const expected: {[key in keyof AutofocusFixConfig]: any} = { 30 | async: jasmine.any(Boolean), 31 | smartEmptyCheck: jasmine.any(Boolean), 32 | triggerDetectChanges: jasmine.any(Boolean), 33 | }; 34 | 35 | // act 36 | const insDefault = new AutofocusFixConfig({}); 37 | const ins = new AutofocusFixConfig({ async: true }); 38 | 39 | // assert 40 | expect(ins).toEqual(jasmine.objectContaining(expected)); 41 | expect(insDefault.async).toBe(false); 42 | expect(ins.async).toBe(true); 43 | }); 44 | }); 45 | }); 46 | 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix-config.ts: -------------------------------------------------------------------------------- 1 | export class AutofocusFixConfig { 2 | public static readonly keys: (keyof AutofocusFixConfig)[] = ['async', 'smartEmptyCheck', 'triggerDetectChanges']; 3 | 4 | public constructor(config: AutofocusFixOptions) { 5 | AutofocusFixConfig.keys 6 | .filter(name => config[name] !== undefined) 7 | // @ts-ignore 8 | .forEach(name => this[name] = config[name]); 9 | } 10 | 11 | /** 12 | * In case `true` .focus() events will be wrapped by `setTimeout(() => ...)`. 13 | * 14 | * Notice: 15 | * I'm not sure that the action is a good practice, however this ability added because of next issues: 16 | * - https://github.com/korniychuk/angular-autofocus-fix/issues/1 17 | * - https://github.com/spirosikmd/angular2-focus/issues/46 18 | */ 19 | public readonly async: boolean = false; 20 | /** 21 | * In case `true`: treat an empty string, an empty array and an empty object as a falsy value. 22 | * In case `false`(default): each of these values treats as truthy. 23 | */ 24 | public readonly smartEmptyCheck: boolean = false; 25 | /** 26 | * In case `true`: trigger {@link ChangeDetectorRef.detectChanges}() after {@link HTMLElement.focus}(). 27 | * 28 | * This is helpful in the case when the HTMLElement to which {@link AutofocusFixDirective} added 29 | * wrapped by another directive/component that has some binding related to focus of the element. 30 | * In this case without enabling .triggerChangeDetection option Angular throws ExpressionChangedAfterItHasBeenCheckedError. 31 | * 32 | * A striking example is the from the Angular Material that wraps control. 33 | */ 34 | public readonly triggerDetectChanges: boolean = false; 35 | } 36 | 37 | export type AutofocusFixOptions = Partial; 38 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, Directive, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; 2 | import { CommonModule, DOCUMENT } from '@angular/common'; 3 | import { ComponentFixture, inject, TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { AutofocusFixDirective } from './autofocus-fix.directive'; 7 | import { AutofocusFixConfig } from './autofocus-fix-config'; 8 | import { Mutable, MutablePartial } from './utils'; 9 | 10 | class TestAutofocusFixDirective extends AutofocusFixDirective { 11 | public localConfig!: MutablePartial; 12 | } 13 | 14 | @Component({ selector: 'no-focusable', template: `` }) 15 | class NoFocusableComponent implements OnInit { 16 | public constructor(private $element: ElementRef) {} 17 | 18 | public ngOnInit() { 19 | (this.$element.nativeElement as HTMLElement).focus = undefined as any; 20 | } 21 | } 22 | 23 | @Directive({ selector: '[focus-binding]', exportAs: 'focusBinding' }) 24 | class FocusBindingDirective { 25 | 26 | public constructor( 27 | @Inject(DOCUMENT) 28 | private readonly $document: Document, 29 | private readonly $el: ElementRef, 30 | ) {} 31 | 32 | public get isFocused(): boolean { 33 | return this.$el.nativeElement === this.$document.activeElement; 34 | } 35 | } 36 | 37 | @Directive({ selector: '[self-focusing]' }) 38 | class SelfFocusingDirective implements OnInit { 39 | 40 | public readonly element: HTMLElement; 41 | 42 | public constructor($er: ElementRef) { 43 | this.element = $er.nativeElement; 44 | } 45 | 46 | public ngOnInit(): void { 47 | this.element.focus(); 48 | spyOn(this.element, 'focus'); 49 | } 50 | 51 | } 52 | 53 | @Component({ 54 | selector: 'wrapper', 55 | template: ` 56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 |
71 |
72 | 73 | {{ focusBindingDir?.isFocused }} 74 |
75 |
76 | 77 | {{ focusBindingDir?.isFocused }} 78 |
79 |
80 | 81 |
82 |
83 | 84 |
85 | `, 86 | }) 87 | class TestWrapperComponent { 88 | @ViewChild(FocusBindingDirective, { static: false }) 89 | public focusBindingDir!: FocusBindingDirective; 90 | 91 | @ViewChild(SelfFocusingDirective, { static: false }) 92 | public selfFocusingDir!: SelfFocusingDirective; 93 | 94 | @ViewChild(AutofocusFixDirective, { static: false }) 95 | public dir!: TestAutofocusFixDirective; 96 | 97 | public showNoFocusable = false; 98 | public show: boolean[] = Array(4).fill(false); 99 | public autofocusValue: any = true; 100 | public smartEmptyCheck = false; 101 | 102 | public showFocusBinding = false; 103 | public showFocusBindingWithTriggerChangeDetection = false; 104 | 105 | public showAsync = false; 106 | 107 | public showSelfFocusing = false; 108 | } 109 | 110 | const configMock = (): Mutable => ({ 111 | async: false, 112 | triggerDetectChanges: false, 113 | smartEmptyCheck: false, 114 | }); 115 | 116 | describe(AutofocusFixDirective.prototype.constructor.name, () => { 117 | let comp: TestWrapperComponent; 118 | let fixture: ComponentFixture; 119 | 120 | function getInput(num: number): HTMLElement | undefined | null { 121 | const debugElement = fixture.debugElement.query(By.css('.input-' + num)); 122 | return debugElement && debugElement.nativeElement; 123 | } 124 | 125 | function getFocused(): HTMLElement | undefined | null { 126 | const debugElement = fixture.debugElement.query(By.css(':focus')); 127 | return debugElement && debugElement.nativeElement; 128 | } 129 | 130 | beforeEach(async () => { 131 | await TestBed 132 | .configureTestingModule({ 133 | imports: [CommonModule], 134 | declarations: [ 135 | // For testing 136 | TestWrapperComponent, 137 | NoFocusableComponent, 138 | FocusBindingDirective, 139 | SelfFocusingDirective, 140 | 141 | // The directive 142 | AutofocusFixDirective, 143 | ], 144 | providers: [ 145 | { 146 | provide: AutofocusFixConfig, 147 | useFactory: configMock, 148 | }, 149 | ], 150 | }) 151 | .compileComponents(); 152 | 153 | fixture = TestBed.createComponent(TestWrapperComponent); 154 | comp = fixture.componentInstance; 155 | fixture.detectChanges(); 156 | }); 157 | 158 | describe('SCENARIO: Testing TestWrapperComponent', () => { 159 | describe('GIVEN: Initialization', () => { 160 | it('should create', () => { 161 | expect(comp).toBeTruthy(); 162 | }); 163 | it('should have correct default values', () => { 164 | comp.show.forEach(v => expect(v).toBe(false)); 165 | expect(comp.autofocusValue).toBe(true); 166 | expect(comp.smartEmptyCheck).toBe(false); 167 | }); 168 | }); 169 | 170 | for (let i = 0; i < 4; i++) { 171 | describe(`GIVEN: The should be inserted and deleted from HTML depend of .show${ i }`, () => { 172 | 173 | describe(`WHEN: .show${ i } === false`, () => { 174 | it('THEN: must be absent', () => { 175 | expect(getInput(i)).toBeFalsy(); 176 | }); 177 | }); 178 | 179 | describe(`WHEN: .show${ i } become false`, () => { 180 | it('THEN: must be inserted', () => { 181 | // act 182 | comp.show[i] = true; 183 | fixture.detectChanges(); 184 | 185 | // assert 186 | expect(getInput(i)).toBeTruthy(); 187 | }); 188 | }); 189 | }); 190 | } 191 | }); // end :: SCENARIO: Testing TestWrapperComponent 192 | 193 | describe('SCENARIO: Edge cases', () => { 194 | describe('GIVEN: No .focus() method on the HTMLElement', () => { 195 | describe('WHEN: Initialize autofocus', () => { 196 | it('THEN: Print console warning', () => { 197 | // arrange 198 | spyOn(console, 'warn'); 199 | 200 | // act 201 | comp.showNoFocusable = true; 202 | fixture.detectChanges(); 203 | 204 | // assert 205 | const noFocusable = fixture.debugElement.query(By.directive(NoFocusableComponent)); 206 | expect(noFocusable).toBeTruthy(); 207 | expect(console.warn).toHaveBeenCalled(); 208 | }); 209 | }); 210 | }); 211 | }); 212 | 213 | describe('SCENARIO: Input autofocus on creation', () => { 214 | 215 | describe('GIVEN: Autofocus in case one input', () => { 216 | describe('WHEN: Input created', () => { 217 | it('THEN: Should be autofocused', () => { 218 | // act 219 | comp.show[0] = true; 220 | fixture.detectChanges(); 221 | 222 | // assert 223 | const input = getInput(0); 224 | expect(input).toBeTruthy(); 225 | expect(input).toBe(getFocused()); 226 | }); 227 | }); 228 | 229 | describe('WHEN: Input created with no value for @Input(\'autofocus\')', () => { 230 | it('THEN: Should be autofocused', () => { 231 | // act 232 | comp.show[2] = true; 233 | fixture.detectChanges(); 234 | 235 | // assert 236 | const input = getInput(2); 237 | expect(input).toBeTruthy(); 238 | expect(input).toBe(getFocused()); 239 | }); 240 | }); 241 | }); 242 | 243 | describe('GIVEN: Opposite autofocus behavior for an empty string in case Smart Empty Check', () => { 244 | describe('WHEN: Input created', () => { 245 | it('THEN: Should be autofocused', () => { 246 | // arrange 247 | comp.smartEmptyCheck = true; 248 | comp.autofocusValue = ''; 249 | 250 | // act 251 | comp.show[0] = true; 252 | fixture.detectChanges(); 253 | 254 | // assert 255 | const input = getInput(0); 256 | expect(input).toBeTruthy(); 257 | expect(getFocused()).toBeFalsy(); 258 | }); 259 | }); 260 | 261 | describe('WHEN: Input created with no value for @Input(\'autofocus\')', () => { 262 | it('THEN: Should be autofocused', () => { 263 | // arrange 264 | comp.smartEmptyCheck = true; 265 | 266 | // act 267 | comp.show[3] = true; 268 | fixture.detectChanges(); 269 | 270 | // assert 271 | const input = getInput(3); 272 | expect(input).toBeTruthy(); 273 | expect(getFocused()).toBeFalsy(); 274 | }); 275 | }); 276 | }); 277 | 278 | describe('GIVEN: Disable autofocus on creation in case @Input(\'autofocus\') falsy', () => { 279 | describe('WHEN: Input created with @Input(\'autofocus\') === false', () => { 280 | it('THEN: Should not be autofocused', () => { 281 | // arrange 282 | comp.autofocusValue = false; 283 | 284 | // act 285 | comp.show[0] = true; 286 | fixture.detectChanges(); 287 | 288 | // assert 289 | const input = getInput(0); 290 | expect(input).toBeTruthy(); 291 | expect(getFocused()).toBeFalsy(); 292 | }); 293 | }); 294 | 295 | describe('WHEN: Input created with @Input(\'autofocus\') === undefined', () => { 296 | it('THEN: Should not be autofocused', () => { 297 | // arrange 298 | comp.autofocusValue = undefined; 299 | 300 | // act 301 | comp.show[0] = true; 302 | fixture.detectChanges(); 303 | 304 | // assert 305 | const input = getInput(0); 306 | expect(input).toBeTruthy(); 307 | expect(getFocused()).toBeFalsy(); 308 | }); 309 | }); 310 | }); 311 | 312 | describe('GIVEN: Autofocus in case multiple inputs', () => { 313 | describe('WHEN: Second input created', () => { 314 | it('THEN: Second input should be autofocused', () => { 315 | // arrange 316 | comp.show[0] = true; 317 | fixture.detectChanges(); 318 | 319 | // act 320 | comp.show[1] = true; 321 | fixture.detectChanges(); 322 | 323 | // assert 324 | const input1 = getInput(0); 325 | const input2 = getInput(1); 326 | expect(input1).toBeTruthy(); 327 | expect(input2).toBeTruthy(); 328 | expect(input2).toBe(getFocused()); 329 | }); 330 | }); 331 | }); 332 | 333 | describe('GIVEN: Input params changes after directive initialized', () => { 334 | describe('WHEN: Change autofocusFixSmartEmptyCheck after directive initialized', () => { 335 | it('THEN: .localConfig should have the previous value', () => { 336 | // arrange 337 | comp.show[0] = true; 338 | comp.smartEmptyCheck = true; 339 | fixture.detectChanges(); 340 | 341 | // pre assert 342 | expect(comp.dir.autofocusFixSmartEmptyCheck).toBe(true); 343 | expect(comp.dir.localConfig.smartEmptyCheck).toBe(true); 344 | 345 | // act 346 | comp.smartEmptyCheck = false; 347 | fixture.detectChanges(); 348 | 349 | // assert 350 | expect(comp.dir.autofocusFixSmartEmptyCheck).toBe(false); 351 | expect(comp.dir.localConfig.smartEmptyCheck).toBe(true); 352 | }); 353 | }); 354 | }); 355 | 356 | describe('GIVEN: Triggering .focus() by others before the autofocus directive', () => { 357 | describe('WHEN: Component triggers by the component on the which [autofocus] added', () => { 358 | it('THEN: .focus() should not be triggered by [autofocus] directive', () => { 359 | // act 360 | comp.showSelfFocusing = true; 361 | fixture.detectChanges(); 362 | 363 | // pre assert 364 | expect(comp.selfFocusingDir).toBeTruthy(); 365 | const focusedEl = getFocused(); 366 | expect(focusedEl).toBeTruthy(); 367 | 368 | // assert 369 | expect(comp.selfFocusingDir.element.focus).not.toHaveBeenCalled(); 370 | }); 371 | }); 372 | }); 373 | }); // end :: SCENARIO: Autofocus on creation 374 | 375 | describe('SCENARIO: Triggering Change Detection after focusing', () => { 376 | describe('GIVEN: Multiple directives on the same HTMLElement', () => { 377 | 378 | describe('WHEN: triggerChangeDetection === false (default)', () => { 379 | it('THEN: Should throw ExpressionChangedAfterItHasBeenCheckedError', () => { 380 | // act 381 | comp.showFocusBinding = true; 382 | const cb = () => fixture.detectChanges(); 383 | 384 | // assert 385 | expect(cb).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/); 386 | }); 387 | }); 388 | 389 | describe('WHEN: With autofocusFixTriggerDetectChanges attribute', () => { 390 | it('THEN: Should NOT throw ExpressionChangedAfterItHasBeenCheckedError', () => { 391 | comp.showFocusBindingWithTriggerChangeDetection = true; 392 | fixture.detectChanges(); 393 | }); 394 | }); 395 | 396 | describe('WHEN: .triggerDetectChanges enabled via global config', () => { 397 | it( 398 | 'THEN: Should NOT throw ExpressionChangedAfterItHasBeenCheckedError', 399 | inject([AutofocusFixConfig], (config: Mutable) => { 400 | config.smartEmptyCheck = true; 401 | fixture.detectChanges(); 402 | }), 403 | ); 404 | }); 405 | 406 | }); 407 | }); 408 | 409 | describe('SCENARIO: Asynchronous focusing', () => { 410 | describe('GIVEN: Input should not have focus in the main execution flow', () => { 411 | 412 | describe('WHEN: Async enabled via attribute autofocusFixAsync', () => { 413 | it('THEN: Input should not be focused immediately', async () => { 414 | // act 415 | comp.showAsync = true; 416 | fixture.detectChanges(); 417 | 418 | // assert 419 | expect(getFocused()).toBeFalsy(); 420 | await fixture.whenStable(); 421 | expect(getFocused()).toBeTruthy(); 422 | }); 423 | }); 424 | 425 | describe('WHEN: .async enabled via global config', () => { 426 | it( 427 | 'THEN: Input should not be focused immediately', 428 | inject([AutofocusFixConfig], async (config: Mutable) => { 429 | // arrange 430 | config.async = true; 431 | 432 | // act 433 | comp.show[0] = true; 434 | fixture.detectChanges(); 435 | 436 | // assert 437 | expect(getFocused()).toBeFalsy(); 438 | await fixture.whenStable(); 439 | expect(getFocused()).toBeTruthy(); 440 | }), 441 | ); 442 | }); 443 | 444 | }); 445 | }); 446 | }); 447 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterContentInit, 3 | ChangeDetectorRef, 4 | Directive, 5 | ElementRef, 6 | Inject, 7 | Input, 8 | OnChanges, OnInit, 9 | SimpleChange, 10 | } from '@angular/core'; 11 | import { DOCUMENT } from '@angular/common'; 12 | 13 | import { normalizeInputAsBoolean, MutablePartial, Mutable } from './utils'; 14 | import { AutofocusFixConfig } from './autofocus-fix-config'; 15 | 16 | // @todo: check configuration 17 | 18 | /** 19 | * ## Ways to turn off autofocus: any js-falsely value, except empty string 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | * 27 | * 28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 38 | * 39 | * ## Ways to enable autofocus: any js-true value and empty string 40 | * 41 | * 42 | * 43 | * 44 | * 45 | * 46 | * 47 | * 48 | * 49 | * 50 | * 51 | * @dynamic 52 | * Notice: @dynamic used for correctly Document inject 53 | * https://github.com/angular/angular/issues/20351 54 | */ 55 | @Directive({ 56 | selector: '[autofocus]', 57 | }) 58 | export class AutofocusFixDirective implements OnChanges, OnInit, AfterContentInit { 59 | 60 | /** Raw value. Always have default value: '' */ 61 | @Input() 62 | public autofocus: any; 63 | 64 | /** @see {@link AutofocusFixConfig.smartEmptyCheck} */ 65 | @Input() 66 | public autofocusFixSmartEmptyCheck?: boolean | any; 67 | 68 | /** @see {@link AutofocusFixConfig.triggerDetectChanges} */ 69 | @Input() 70 | public autofocusFixTriggerDetectChanges?: boolean | any; 71 | 72 | /** @see {@link AutofocusFixConfig.async} */ 73 | @Input() 74 | public autofocusFixAsync?: boolean | any; 75 | 76 | private wasInitialized = false; 77 | /** Notice: protected for unit testing */ 78 | protected localConfig: MutablePartial = {}; 79 | private config!: Mutable; 80 | private autofocusEnabled = false; 81 | private readonly element: HTMLElement; 82 | 83 | public constructor( 84 | $er: ElementRef, 85 | private readonly $cdr: ChangeDetectorRef, 86 | @Inject(DOCUMENT) 87 | private readonly $document: Document, 88 | private readonly $config: AutofocusFixConfig, 89 | ) { 90 | this.element = $er.nativeElement; 91 | } 92 | 93 | public ngOnChanges(changes: { [key in keyof AutofocusFixDirective]?: SimpleChange }): void { 94 | // Autofocus works only once. No need to do the initialization on each change detection cycle. 95 | if (this.wasInitialized) { return; } 96 | 97 | this.normalizeLocalConfigItem('async', changes.autofocusFixAsync); 98 | this.normalizeLocalConfigItem('smartEmptyCheck', changes.autofocusFixSmartEmptyCheck); 99 | this.normalizeLocalConfigItem('triggerDetectChanges', changes.autofocusFixTriggerDetectChanges); 100 | } 101 | 102 | public ngOnInit(): void { 103 | if (!this.element.focus) { 104 | return console.warn( 105 | 'AutofocusFixDirective: There is no .focus() method on the element: %O', 106 | this.element, 107 | ); 108 | } 109 | 110 | this.config = {} as AutofocusFixConfig; 111 | AutofocusFixConfig.keys.forEach(key => { 112 | const local = this.localConfig[key]; 113 | this.config[key] = local !== undefined ? local : this.$config[key]; 114 | }); 115 | 116 | this.autofocusEnabled = normalizeInputAsBoolean(this.autofocus, this.config.smartEmptyCheck); 117 | } 118 | 119 | public ngAfterContentInit(): void { 120 | this.wasInitialized = true; 121 | if (!this.element.focus) { return; } 122 | 123 | this.checkFocus(); 124 | } 125 | 126 | private checkFocus(): void { 127 | this.config.async ? setTimeout(this.checkFocusInternal.bind(this)) : this.checkFocusInternal(); 128 | } 129 | 130 | private checkFocusInternal(): void { 131 | if (!this.autofocusEnabled || this.amIFocused) { return; } 132 | 133 | this.element.focus(); 134 | if (this.config.triggerDetectChanges) { 135 | this.$cdr.detectChanges(); 136 | } 137 | } 138 | 139 | private get amIFocused(): boolean { 140 | return this.$document.activeElement === this.element; 141 | } 142 | 143 | private normalizeLocalConfigItem(configKey: keyof AutofocusFixConfig, change?: SimpleChange): void { 144 | if (change) { 145 | this.localConfig[configKey] = normalizeInputAsBoolean(change.currentValue); 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AutofocusFixConfig } from './autofocus-fix-config'; 3 | import { AutofocusFixModule } from './autofocus-fix.module'; 4 | 5 | const moduleName = AutofocusFixModule.prototype.constructor.name; 6 | 7 | describe(moduleName, () => { 8 | describe('SCENARIO: Module importing', () => { 9 | 10 | describe(`WHEN: Importing just '${moduleName}'`, () => { 11 | it('THEN: Should throw an error that can\'t inject config', () => { 12 | TestBed.configureTestingModule({ 13 | imports: [AutofocusFixModule], 14 | }); 15 | const cb = () => TestBed.get(AutofocusFixConfig); 16 | 17 | expect(cb).toThrow(); 18 | }); 19 | }); 20 | 21 | describe(`WHEN: Importing '${moduleName}'.forRoot()`, () => { 22 | it('THEN: Should get instance', () => { 23 | TestBed.configureTestingModule({ 24 | imports: [AutofocusFixModule.forRoot()], 25 | }); 26 | const ins = TestBed.get(AutofocusFixConfig); 27 | 28 | expect(ins instanceof AutofocusFixConfig).toBeTruthy(); 29 | }); 30 | }); 31 | 32 | describe(`WHEN: Importing just '${moduleName}' and providing config manually`, () => { 33 | it('THEN: Should get instance', () => { 34 | TestBed.configureTestingModule({ 35 | imports: [AutofocusFixModule], 36 | providers: [ 37 | { 38 | provide: AutofocusFixConfig, 39 | useValue: new AutofocusFixConfig({}), 40 | }] 41 | }); 42 | const ins = TestBed.get(AutofocusFixConfig); 43 | 44 | expect(ins instanceof AutofocusFixConfig).toBeTruthy(); 45 | }); 46 | }); 47 | 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/autofocus-fix.module.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, ModuleWithProviders, NgModule, Optional } from '@angular/core'; 2 | 3 | import { AutofocusFixConfig, AutofocusFixOptions } from './autofocus-fix-config'; 4 | import { AutofocusFixDirective } from './autofocus-fix.directive'; 5 | import { noAutofocusFixConfigError } from './no-autofocus-fix-config.error'; 6 | 7 | // Exists for AoT support 8 | export function configFactory(options: AutofocusFixOptions) { 9 | return new AutofocusFixConfig(options); 10 | } 11 | // Exists for AoT support 12 | const AutofocusFixOptionsInternalToken = new InjectionToken('AutofocusFixOptions'); 13 | 14 | @NgModule({ 15 | declarations: [AutofocusFixDirective], 16 | exports: [AutofocusFixDirective] 17 | }) 18 | export class AutofocusFixModule { 19 | 20 | public constructor(@Optional() $config: AutofocusFixConfig) { 21 | if (!$config) { 22 | noAutofocusFixConfigError(); 23 | } 24 | } 25 | 26 | public static forRoot(options: AutofocusFixOptions = {}): ModuleWithProviders { 27 | 28 | return { 29 | ngModule: AutofocusFixModule, 30 | providers: [ 31 | { 32 | provide: AutofocusFixOptionsInternalToken, 33 | useValue: options, 34 | }, 35 | { 36 | provide: AutofocusFixConfig, 37 | useFactory: configFactory, 38 | deps: [AutofocusFixOptionsInternalToken], 39 | }, 40 | ], 41 | }; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/no-autofocus-fix-config.error.spec.ts: -------------------------------------------------------------------------------- 1 | import { noAutofocusFixConfigError } from './no-autofocus-fix-config.error'; 2 | 3 | describe('noAutofocusFixConfigError()', () => { 4 | it('should throw the error', () => { 5 | const cb = () => noAutofocusFixConfigError(); 6 | 7 | expect(cb).toThrow(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/no-autofocus-fix-config.error.ts: -------------------------------------------------------------------------------- 1 | export function noAutofocusFixConfigError() { 2 | const moduleName = 'AutofocusFixModule'; 3 | const configName = 'AutofocusFixConfig'; 4 | 5 | throw new Error(`${ moduleName }: Can't inject ${ configName }. 6 | 7 | Option 1: Use .forRoot() when you importing the module: 8 | Do it in case you import ${ moduleName } to the root module of your application. 9 | 10 | @NgModule({ 11 | ... 12 | imports: [ 13 | ... 14 | ${ moduleName }.forRoot(), // <--- new code 15 | ], 16 | ... 17 | }) 18 | export class AppModule {} 19 | 20 | 21 | Option 2: Provide ${ configName } manually providing ${ configName }: 22 | Do it in case you want to provide specific config to the one of your lazy loadable modules. 23 | 24 | @NgModule({ 25 | ... 26 | providers: [ 27 | ... 28 | { // <--- new code 29 | provide: ${ configName } // <--- 30 | useValue: new ${configName}({ ... }), // <--- 31 | }, // <--- 32 | ], 33 | ... 34 | }) 35 | export class AppModule {} 36 | `); 37 | } 38 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { normalizeInputAsBoolean } from './utils'; 2 | 3 | describe('Utils -> normalizeInputAsBoolean()', () => { 4 | 5 | describe('SCENARIO: .smartEmptyCheck === false', () => { 6 | describe('GIVEN: Should be false', () => { 7 | it('false', () => { 8 | expect(normalizeInputAsBoolean(false)).toBe(false); 9 | }); 10 | it('null', () => { 11 | expect(normalizeInputAsBoolean(null)).toBe(false); 12 | }); 13 | it('undefined', () => { 14 | expect(normalizeInputAsBoolean(undefined)).toBe(false); 15 | }); 16 | it('0', () => { 17 | expect(normalizeInputAsBoolean(0)).toBe(false); 18 | }); 19 | 20 | it(`'false'`, () => { 21 | expect(normalizeInputAsBoolean('false')).toBe(false); 22 | }); 23 | it(`'null'`, () => { 24 | expect(normalizeInputAsBoolean('null')).toBe(false); 25 | }); 26 | it(`'undefined'`, () => { 27 | expect(normalizeInputAsBoolean('undefined')).toBe(false); 28 | }); 29 | it(`'0'`, () => { 30 | expect(normalizeInputAsBoolean('0')).toBe(false); 31 | }); 32 | 33 | it('NaN', () => { 34 | expect(normalizeInputAsBoolean(NaN)).toBe(false); 35 | }); 36 | it(`'NaN'`, () => { 37 | expect(normalizeInputAsBoolean('NaN')).toBe(false); 38 | }); 39 | }); 40 | 41 | describe('GIVEN: Should be true', () => { 42 | it(`'a string'`, () => { 43 | expect(normalizeInputAsBoolean('a string')).toBe(true); 44 | }); 45 | it(`''`, () => { 46 | expect(normalizeInputAsBoolean('')).toBe(true); 47 | }); 48 | it('[]', () => { 49 | expect(normalizeInputAsBoolean([])).toBe(true); 50 | }); 51 | it('{}', () => { 52 | expect(normalizeInputAsBoolean({})).toBe(true); 53 | }); 54 | }); 55 | }); // end :: SCENARIO: .smartEmptyCheck === false 56 | 57 | describe('SCENARIO: .smartEmptyCheck === true', () => { 58 | describe('GIVEN: Should be false', () => { 59 | it('false', () => { 60 | expect(normalizeInputAsBoolean(false, true)).toBe(false); 61 | }); 62 | it('null', () => { 63 | expect(normalizeInputAsBoolean(null, true)).toBe(false); 64 | }); 65 | it('undefined', () => { 66 | expect(normalizeInputAsBoolean(undefined, true)).toBe(false); 67 | }); 68 | it('0', () => { 69 | expect(normalizeInputAsBoolean(0, true)).toBe(false); 70 | }); 71 | 72 | it(`'false'`, () => { 73 | expect(normalizeInputAsBoolean('false', true)).toBe(false); 74 | }); 75 | it(`'null'`, () => { 76 | expect(normalizeInputAsBoolean('null', true)).toBe(false); 77 | }); 78 | it(`'undefined'`, () => { 79 | expect(normalizeInputAsBoolean('undefined', true)).toBe(false); 80 | }); 81 | it(`'0'`, () => { 82 | expect(normalizeInputAsBoolean('0', true)).toBe(false); 83 | }); 84 | 85 | it('NaN', () => { 86 | expect(normalizeInputAsBoolean(NaN, true)).toBe(false); 87 | }); 88 | it(`'NaN'`, () => { 89 | expect(normalizeInputAsBoolean('NaN', true)).toBe(false); 90 | }); 91 | 92 | it(`''`, () => { 93 | expect(normalizeInputAsBoolean('', true)).toBe(false); 94 | }); 95 | it('[]', () => { 96 | expect(normalizeInputAsBoolean([], true)).toBe(false); 97 | }); 98 | it('{}', () => { 99 | expect(normalizeInputAsBoolean({}, true)).toBe(false); 100 | }); 101 | }); 102 | 103 | describe('GIVEN: Should be true', () => { 104 | it(`'a string'`, () => { 105 | expect(normalizeInputAsBoolean('a string', true)).toBe(true); 106 | }); 107 | it('[1]', () => { 108 | expect(normalizeInputAsBoolean([1], true)).toBe(true); 109 | }); 110 | it('{ a: 1 }', () => { 111 | expect(normalizeInputAsBoolean({ a: 1 }, true)).toBe(true); 112 | }); 113 | }); 114 | }); // end :: SCENARIO: .smartEmptyCheck === false 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export type MutablePartial = { -readonly [K in keyof T]+?: T[K] }; 2 | export type Mutable = { -readonly [K in keyof T]: T[K] }; 3 | 4 | export function normalizeInputAsBoolean(value: any, smartEmptyCheck: boolean = false): boolean { 5 | const isFalse = value === false 6 | || value === null 7 | || value === undefined 8 | || value === 0 9 | || value === 'false' 10 | || value === 'null' 11 | || value === 'undefined' 12 | || value === '0' 13 | || (typeof value === 'number' && isNaN(value)) 14 | || value === 'NaN' 15 | || smartEmptyCheck && ( 16 | value === '' // Notice: opposite default behavior! 17 | || value instanceof Array && !value.length 18 | || value !== null && typeof value === 'object' && !Object.keys(value).length 19 | ); 20 | 21 | return !isFalse; 22 | } 23 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-autofocus-fix 3 | */ 4 | 5 | export { AutofocusFixModule } from './lib/autofocus-fix.module'; 6 | export { AutofocusFixConfig, AutofocusFixOptions } from './lib/autofocus-fix-config'; 7 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true 21 | }, 22 | "exclude": [ 23 | "src/test.ts", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-autofocus-fix/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": false, 5 | "component-selector": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tools/common.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { execSync } from 'child_process'; 3 | 4 | export const libName = 'ngx-autofocus-fix'; 5 | export const root = path.join.bind(path, __dirname, '..'); 6 | export const libRoot = root.bind(path, 'projects', libName); 7 | 8 | export function run(command: string): Buffer { 9 | console.log(`$> ${command}`); 10 | return execSync(command); 11 | } 12 | -------------------------------------------------------------------------------- /tools/prepare-version.ts: -------------------------------------------------------------------------------- 1 | import { libRoot, run } from './common'; 2 | 3 | /** @example v1.0.12 */ 4 | type StrVersion = string; 5 | 6 | interface Version { 7 | major: number; 8 | minor: number; 9 | patch: number; 10 | } 11 | 12 | process.chdir(libRoot()); 13 | 14 | const localVer = getLocalVersion(); 15 | const publicVers = getPublicVersions(); 16 | 17 | const isLocalVerPublished = publicVers.some(v => _compareVersion(v, localVer)); 18 | if (isLocalVerPublished) { 19 | const v = incrementVersion(localVer); 20 | gitPush(); 21 | console.log('Version updated: ', _formatVersion(v)); 22 | } else { 23 | console.log('Current version is actual'); 24 | } 25 | 26 | function gitPush() { 27 | run(`git push --no-verify --follow-tags`); 28 | // run(`git push --no-verify --tags`); 29 | } 30 | 31 | function incrementVersion(prevVer: Version, type: keyof Version = 'patch'): Version { 32 | const newStrVer = run(`npm version ${type}`).toString().trim(); 33 | const prevStrVer = _formatVersion(prevVer); 34 | run(`git add package.json package-lock.json`); 35 | run(`git commit -m 'Update package version: ${prevStrVer} -> ${newStrVer}'`); 36 | run(`git tag -a ${newStrVer} -m 'new version: ${newStrVer}'`); 37 | 38 | return _parseVersion(newStrVer); 39 | } 40 | 41 | function getLocalVersion(): Version | undefined { 42 | return _parseVersion(require(libRoot('package.json')).version); 43 | } 44 | 45 | function getPublicVersions(): Version[] { 46 | const output = run('npm view . versions --json'); 47 | const allVersions = JSON.parse(output.toString()); 48 | 49 | return allVersions 50 | .map((v: string) => _parseVersion(v)) 51 | .filter(Boolean); 52 | } 53 | 54 | // function getGitVersions(): Version[] { 55 | // const output = run(`git tag -l 'v*'`); 56 | // 57 | // return String(output) 58 | // .split('\n') 59 | // .slice(0, -1) 60 | // .map((v: string) => _parseVersion(v)) 61 | // .filter(Boolean) as Version[]; 62 | // } 63 | 64 | function _parseVersion(strVersion: StrVersion): Version | undefined { 65 | const res = strVersion.match(/^v?(?\d+)\.(?\d+)\.(?\d+)$/); 66 | if (!res || !res.groups) { return; } 67 | 68 | return { 69 | major: +res.groups.major, 70 | minor: +res.groups.minor, 71 | patch: +res.groups.patch, 72 | }; 73 | } 74 | 75 | function _formatVersion(v: Version): StrVersion { 76 | return `v${v.major}.${v.minor}.${v.patch}`; 77 | } 78 | 79 | function _compareVersion(a?: Version, b?: Version): boolean { 80 | return !!(a && b) 81 | && a.major === b.major 82 | && a.minor === b.minor 83 | && a.patch === b.patch 84 | ; 85 | } 86 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2018" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "strict": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "paths": { 23 | "angular-autofocus-fix": [ 24 | "dist/angular-autofocus-fix" 25 | ], 26 | "angular-autofocus-fix/*": [ 27 | "dist/angular-autofocus-fix/*" 28 | ] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "fullTemplateTypeCheck": true, 33 | "strictInjectionParameters": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warning" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "component-class-suffix": true, 65 | "contextual-lifecycle": true, 66 | "directive-class-suffix": true, 67 | "no-conflicting-lifecycle": true, 68 | "no-host-metadata-property": true, 69 | "no-input-rename": true, 70 | "no-inputs-metadata-property": true, 71 | "no-output-native": true, 72 | "no-output-on-prefix": true, 73 | "no-output-rename": true, 74 | "no-outputs-metadata-property": true, 75 | "template-banana-in-box": true, 76 | "template-no-negated-async": true, 77 | "use-lifecycle-interface": true, 78 | "use-pipe-transform-interface": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(wallaby) { 2 | const wallabyWebpack = require('wallaby-webpack'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const specPattern = '/**/*spec.ts'; 7 | const angularConfig = require('./angular.json'); 8 | 9 | const projects = Object.keys(angularConfig.projects).map(key => { 10 | return { name: key, ...angularConfig.projects[key] }; 11 | }).filter(project => project.sourceRoot) 12 | .filter(project => project.projectType === 'library'); 13 | // Notice: uncomment if you want to test applications 14 | // .filter(project => project.projectType !== 'application' || 15 | // (project.architect && 16 | // project.architect.test && 17 | // project.architect.test.builder === '@angular-devkit/build-angular:karma')); 18 | 19 | const applications = projects.filter(project => project.projectType === 'application'); 20 | const libraries = projects.filter(project => project.projectType === 'library'); 21 | 22 | const tsConfigFile = projects 23 | .map(project => path.join(__dirname, project.root, 'tsconfig.spec.json')) 24 | .find(tsConfig => fs.existsSync(tsConfig)); 25 | 26 | const tsConfigSpec = tsConfigFile ? JSON.parse(fs.readFileSync(tsConfigFile)) : {}; 27 | 28 | const compilerOptions = Object.assign(require('./tsconfig.json').compilerOptions, tsConfigSpec.compilerOptions); 29 | compilerOptions.emitDecoratorMetadata = true; 30 | 31 | return { 32 | files: [ 33 | { pattern: path.basename(__filename), load: false, instrument: false }, 34 | ...projects.map(project => ({ 35 | pattern: project.sourceRoot + '/**/*.+(ts|js|css|less|scss|sass|styl|html|json|svg)', 36 | load: false 37 | })), 38 | ...projects.map(project => ({ 39 | pattern: project.sourceRoot + specPattern, 40 | ignore: true 41 | })), 42 | ...projects.map(project => ({ 43 | pattern: project.sourceRoot + '/**/*.d.ts', 44 | ignore: true 45 | })), 46 | ...projects.map(project => ({ 47 | pattern: project.sourceRoot + '/**/test.ts', 48 | ignore: true 49 | })), 50 | ], 51 | 52 | tests: [ 53 | ...projects.map(project => ({ 54 | pattern: project.sourceRoot + specPattern, 55 | load: false 56 | })) 57 | ], 58 | 59 | testFramework: 'jasmine', 60 | 61 | compilers: { 62 | '**/*.ts': wallaby.compilers.typeScript({ 63 | typescript: require('typescript'), 64 | ...compilerOptions, 65 | getCustomTransformers: program => { 66 | return { 67 | before: [ 68 | require('@ngtools/webpack/src/transformers/replace_resources').replaceResources( 69 | path => true, 70 | () => program.getTypeChecker(), 71 | false 72 | ) 73 | ] 74 | }; 75 | } 76 | }) 77 | }, 78 | 79 | preprocessors: { 80 | /* Initialize Test Environment for Wallaby */ 81 | [path.basename(__filename)]: file => ` 82 | import 'zone.js/dist/zone'; 83 | import 'zone.js/dist/zone-testing'; 84 | import '@angular-devkit/build-angular/src/angular-cli-files/models/jit-polyfills'; 85 | import { getTestBed } from '@angular/core/testing'; 86 | import { 87 | BrowserDynamicTestingModule, 88 | platformBrowserDynamicTesting 89 | } from '@angular/platform-browser-dynamic/testing'; 90 | 91 | 92 | getTestBed().initTestEnvironment( 93 | BrowserDynamicTestingModule, 94 | platformBrowserDynamicTesting() 95 | ); 96 | ` 97 | }, 98 | 99 | middleware: function(app, express) { 100 | const path = require('path'); 101 | 102 | applications.forEach(application => { 103 | if ( 104 | !application.architect || 105 | !application.architect.test || 106 | !application.architect.test.options || 107 | !application.architect.test.options.assets 108 | ) { 109 | return; 110 | } 111 | 112 | application.architect.test.options.assets.forEach(asset => { 113 | if (asset && !asset.glob) { 114 | // Only works for file assets (not globs) 115 | // (https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/asset-configuration.md#project-assets) 116 | app.use(asset.slice(application.sourceRoot.length), express.static(path.join(__dirname, asset))); 117 | } 118 | }); 119 | }); 120 | }, 121 | 122 | env: { 123 | kind: 'chrome' 124 | }, 125 | 126 | postprocessor: wallabyWebpack({ 127 | entryPatterns: [ 128 | ...applications 129 | .map(project => project.sourceRoot + '/polyfills.js') 130 | .filter(polyfills => fs.existsSync(path.join(__dirname, polyfills.replace(/js$/, 'ts')))), 131 | path.basename(__filename), 132 | ...projects.map(project => project.sourceRoot + specPattern.replace(/ts$/, 'js')) 133 | ], 134 | 135 | module: { 136 | rules: [ 137 | { test: /\.css$/, loader: ['raw-loader'] }, 138 | { test: /\.html$/, loader: 'raw-loader' }, 139 | { 140 | test: /\.ts$/, 141 | loader: '@ngtools/webpack', 142 | include: /node_modules/, 143 | query: { tsConfigPath: 'tsconfig.json' } 144 | }, 145 | { test: /\.styl$/, loaders: ['raw-loader', 'stylus-loader'] }, 146 | { test: /\.less$/, loaders: ['raw-loader', { loader: 'less-loader' }] }, 147 | { 148 | test: /\.scss$|\.sass$/, 149 | loaders: [{ loader: 'raw-loader' }, { loader: 'sass-loader', options: { implementation: require('sass') } }] 150 | }, 151 | { test: /\.(jpg|png|svg)$/, loader: 'raw-loader' } 152 | ] 153 | }, 154 | 155 | resolve: { 156 | extensions: ['.js', '.ts'], 157 | modules: [ 158 | wallaby.projectCacheDir, 159 | ...(projects.length ? projects.filter(project => project.root) 160 | .map(project => path.join(wallaby.projectCacheDir, project.root)) : []), 161 | ...(projects.length ? projects.filter(project => project.sourceRoot) 162 | .map(project => path.join(wallaby.projectCacheDir,project.sourceRoot)) : []), 163 | 'node_modules' 164 | ], 165 | alias: libraries.reduce((result, project) => { 166 | result[project.name] = path.join(wallaby.projectCacheDir, project.sourceRoot, 'public-api'); 167 | return result; 168 | }, {}) 169 | } 170 | }), 171 | 172 | setup: function() { 173 | window.__moduleBundler.loadTests(); 174 | } 175 | }; 176 | }; 177 | --------------------------------------------------------------------------------